
Beyond Auto-Replies: Building an AI-Powered E-commerce Support system
Implementation of an AI-driven multi-agent system for automated e-commerce customer email support. It covers the architecture and setup of specialized AI agents using the Multi-Agent Orchestrator framework, integrating automated processing with human-in-the-loop oversight. The guide explores email ingestion, intelligent routing, automated response generation, and human verification, providing a comprehensive approach to balancing AI efficiency with human expertise in customer support
- Incoming emails are ingested into an SQS queue
- Lakechain's EmailTextProcessor extracts the text and forwards it to another SQS queue
- A Lambda function processes this queue using the Multi-Agent Orchestrator, which:
- Determines the most appropriate AI Agent for each query
- Generates a response using the chosen agent
- The response is then sent to a final SQS queue for further processing or direct customer communication
- Inbound emails first land in a queue for raw emails.
- The Email Content Extraction step processes these raw emails, extracting the relevant text.
- Processed emails are then placed in another queue, ready for AI analysis.
- The Response Generation phase is where our Multi-Agent Orchestrator shines, determining the most appropriate AI agent to handle the query and generating a response.
- Finally, the generated responses are queued for outbound delivery to customers.
- 📦 Order Management Agent: Handles everything related to orders, shipments, and returns.
- 🏷️ Product Information Agent: Answers questions about product specifications, compatibility, and availability.
- 💁 Customer Service Agent: For all those general inquiries and account stuff. It's basically a digital version of that super-helpful store clerk we all wish we had.
- 👤 Human Agent: Handles complex issues that require human intervention.
- 🤖👀 AI with Human Verification Agent: Generates AI responses for high-priority inquiries, which are then verified by a human.
1
mkdir ecommerce-support-ai && cd ecommerce-support-ai
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Initialize npm project
npm init -y
# Install TypeScript and Node.js types as development dependencies
npm install typescript @types/node --save-dev
# Install multi-agent orchestrator and AWS Bedrock agent runtime
npm install multi-agent-orchestrator
npm install @aws-sdk/client-bedrock-agent-runtime
# Install additional dependencies without specifying versions
npm install \
"@project-lakechain/bedrock-text-processors" \
"@project-lakechain/core" \
"@project-lakechain/email-text-processor" \
"@project-lakechain/opensearch-domain" \
"@project-lakechain/pandoc-text-converter" \
"@project-lakechain/pdf-text-converter" \
"@project-lakechain/recursive-character-text-splitter" \
"@project-lakechain/s3-event-trigger" \
"@project-lakechain/sqs-storage-connector"
src
directory and the agents.ts
file:1
mkdir src && touch src/agents.ts
agents.ts
in your project directory:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import {
MultiAgentOrchestrator,
BedrockLLMAgent,
BedrockLLMAgentOptions,
AmazonBedrockAgent,
AmazonBedrockAgentOptions,
AgentResponse,
AmazonKnowledgeBasesRetriever,
Agent,
ChainAgent,
ChainAgentOptions,
ConversationMessage,
ParticipantRole
} from 'multi-agent-orchestrator';
import { BedrockAgentRuntimeClient } from "@aws-sdk/client-bedrock-agent-runtime";
// Mock databases
const orderDb: Record<string, any> = {
"12345": { status: "Shipped", items: ["Widget A", "Gadget B"], total: 150.00 },
"67890": { status: "Processing", items: ["Gizmo C"], total: 75.50 },
};
const shipmentDb: Record<string, any> = {
"12345": { carrier: "FastShip", trackingNumber: "FS123456789", status: "In Transit" },
};
const productDb: Record<string, any> = {
"Widget A": { price: 50.00, stock: 100, description: "A fantastic widget" },
"Gadget B": { price: 100.00, stock: 50, description: "An amazing gadget" },
"Gizmo C": { price: 75.50, stock: 25, description: "A wonderful gizmo" },
};
// Mock functions for tools
const orderLookup = (orderId: string): any => orderDb[orderId] || null;
const shipmentTracker = (orderId: string): any => shipmentDb[orderId] || null;
const returnProcessor = (orderId: string): string => `Return initiated for order ${orderId}`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
const orderManagementToolConfig = [
{
toolSpec: {
name: "OrderLookup",
description: "Retrieve order details from the database",
inputSchema: {
json: {
type: "object",
properties: {
orderId: { type: "string", description: "The order ID to look up" }
},
required: ["orderId"]
}
}
}
},
{
toolSpec: {
name: "ShipmentTracker",
description: "Get real-time shipping information",
inputSchema: {
json: {
type: "object",
properties: {
orderId: { type: "string", description: "The order ID to track" }
},
required: ["orderId"]
}
}
}
},
{
toolSpec: {
name: "ReturnProcessor",
description: "Initiate and manage return requests",
inputSchema: {
json: {
type: "object",
properties: {
orderId: { type: "string", description: "The order ID for the return" }
},
required: ["orderId"]
}
}
}
}
];
export async function orderManagementToolHandler(response: ConversationMessage, conversation: ConversationMessage[]) {
const responseContentBlocks = response.content as any[];
let toolResults: any = [];
if (!responseContentBlocks) {
throw new Error("No content blocks in response");
}
for (const contentBlock of responseContentBlocks) {
if ("toolUse" in contentBlock) {
const toolUseBlock = contentBlock.toolUse;
const toolUseName = toolUseBlock.name;
let result;
switch (toolUseName) {
case "OrderLookup":
result = orderLookup(toolUseBlock.input.orderId);
break;
case "ShipmentTracker":
result = shipmentTracker(toolUseBlock.input.orderId);
break;
case "ReturnProcessor":
result = returnProcessor(toolUseBlock.input.orderId);
break;
}
if (result) {
toolResults.push({
"toolResult": {
"toolUseId": toolUseBlock.toolUseId,
"content": [{ json: { result } }],
}
});
}
}
}
const message: ConversationMessage = { role: ParticipantRole.USER, content: toolResults };
conversation.push(message);
}
const orderManagementAgent = new BedrockLLMAgent({
name: "Order Management Agent",
description: "Handles order-related inquiries including order status, shipment tracking, returns, and refunds. Uses order database and shipment tracking tools.",
modelId: "anthropic.claude-3-sonnet-20240229-v1:0",
toolConfig: {
useToolHandler: orderManagementToolHandler,
tool: orderManagementToolConfig,
toolMaxRecursions: 5
},
saveChat: false
} as BedrockLLMAgentOptions);
- OrderLookup: Retrieves order details from the database
- ShipmentTracker: Gets real-time shipping information
- ReturnProcessor: Initiates and manages return requests
- Tool Definition: We define the tools in the
orderManagementToolConfig
array. Each tool has a name, description, and an input schema that specifies what information is needed to use the tool. - Tool Implementation: The
orderManagementToolHandler
function contains the actual implementation of these tools. When the LLM determines it needs to use a tool, it calls this handler. The handler then:- Identifies which tool is being called
- Executes the corresponding function (e.g.,
orderLookup
,shipmentTracker
, orreturnProcessor
) - Formats the result and adds it to the conversation
- LLM Integration: The BedrockLLMAgent is configured with these tools using the
toolConfig
property. This allows the LLM to:- Understand what tools are available
- Decide when to use a tool based on the customer's request
- Call the appropriate tool and receive the results
1
2
3
4
5
6
7
const customerServiceAgent = new AmazonBedrockAgent({
name: "Customer Service Agent",
description: "Handles general customer inquiries, account-related questions, and non-technical support requests. Uses comprehensive customer service knowledge base.",
agentId: "your-agent-id",
agentAliasId: "your-agent-alias-id",
saveChat: false
} as AmazonBedrockAgentOptions);
- Frequently Asked Questions (FAQs)
- Company policies (returns, shipping, privacy, etc.)
- Account management procedures
- Troubleshooting guides for common issues
1
2
3
4
5
6
7
8
9
10
11
12
const productInfoRetriever = new AmazonKnowledgeBasesRetriever(
new BedrockAgentRuntimeClient({}),
{ knowledgeBaseId: "your-product-kb-id" }
);
const productInfoAgent = new BedrockLLMAgent({
name: "Product Information Agent",
description: "Provides detailed product information, answers questions about specifications, compatibility, and availability.",
modelId: "anthropic.claude-3-haiku-20240307-v1:0",
retriever: productInfoRetriever,
saveChat: false
} as BedrockLLMAgentOptions);
- Knowledge Integration: This setup implements a RAG. The AmazonKnowledgeBasesRetriever connects to a vector database containing contextual product data. When a query is received, relevant information is retrieved from this database and fed into the language model, allowing it to provide responses based on the most up-to-date and specific product information.
- Stateless Interactions: Setting
saveChat
tofalse
optimizes the agent for independent queries, as product information requests typically don't require context from previous interactions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class HumanAgent extends Agent {
async processRequest(inputText: string, userId: string, sessionId: string, chatHistory: ConversationMessage[]): Promise<ConversationMessage> {
console.log(`Human agent received request: ${inputText}`);
const humanResponse = await this.simulateHumanResponse(inputText);
return {
role: ParticipantRole.ASSISTANT,
content: [{ text: humanResponse }]
};
}
private async simulateHumanResponse(input: string): Promise<string> {
console.log(`Sending email to SQS queue: "${input}"`);
return `Your request has been received and will be processed by our customer service team. We'll get back to you as soon as possible.`;
}
}
const humanAgent = new HumanAgent({
name: "Human Agent",
description: "Handles complex inquiries, complaints, or sensitive issues requiring human expertise.",
saveChat: false
});
Agent
class. This agent is designed to handle specific domains or topics that are beyond the capabilities of our AI models. The key idea here is that we've defined a "domain" of queries that should be directly routed to human operators, rather than being processed by an LLM.- Handling Complex Issues: For topics that require nuanced understanding, emotional intelligence the Human Agent ensures these queries are directed to actual human operators.
- Sensitive Matters: Complaints, legal issues, or other sensitive topics that an LLM might not handle appropriately are routed through this agent.
- Compliance: For industries with strict regulations or where human oversight is mandatory, this agent ensures that certain types of inquiries are always handled by authorized personnel.
simulateHumanResponse
method would likely integrate with a ticketing system. This could involve placing the query in a queue for human review, sending an email to a relevant department, or triggering an alert in a customer service dashboard.1
2
3
4
5
6
7
8
9
10
11
12
13
const aiWithHumanVerificationAgent = new ChainAgent({
name: "AI with Human Verification Agent",
description: "Handles high-priority or sensitive customer inquiries by generating AI responses and having them verified by a human before sending.",
agents: [
customerServiceAgent,
new HumanAgent({
name: "Human Verifier",
description: "Verifies and potentially modifies AI-generated responses",
saveChat: false
})
],
saveChat: false
} as ChainAgentOptions);
- The LLM quickly generates a draft response, providing efficiency and consistency.
- A human expert reviews and refines the response, ensuring accuracy and adding nuance where necessary.
1
2
3
4
5
6
// Create and export the orchestrator
export const orchestrator = new MultiAgentOrchestrator();
orchestrator.addAgent(orderManagementAgent);
orchestrator.addAgent(productInfoAgent);
orchestrator.addAgent(humanAgent);
orchestrator.addAgent(aiWithHumanVerificationAgent);
MultiAgentOrchestrator
and add all the agents we've created to it. This process registers each agent with the orchestrator, allowing it to manage and coordinate their actions.local-test.ts
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { orchestrator } from './agents';
async function testOrchestrator() {
const testEmails = [
{ text: "What's the status of my order #12345?", userId: "user1", sessionId: "session1" },
{ text: "I need more information about Widget A", userId: "user2", sessionId: "session2" },
{ text: "How do I change my account password?", userId: "user3", sessionId: "session3" },
{ text: "I have a complex issue with my recent purchase", userId: "user4", sessionId: "session4" },
{ text: "Is my personal information secure on your website?", userId: "user5", sessionId: "session5" }
];
for (const email of testEmails) {
console.log(`Processing email: "${email.text}"`);
const response = await orchestrator.routeRequest(email.text, email.userId, email.sessionId);
console.log(`Response from ${response.metadata.agentName}:`);
console.log(response.output);
console.log('---');
}
}
testOrchestrator().catch(console.error);
ts-node local-test.ts
to see how the orchestrator handles different types of inquiries locally.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import { EmailTextProcessor } from "@project-lakechain/email-text-processor";
import { CacheStorage } from "@project-lakechain/core";
import { S3EventTrigger } from "@project-lakechain/s3-event-trigger";
import { SqsStorageConnector } from "@project-lakechain/sqs-storage-connector";
import path from "path";
import {
aws_lambda as lambda,
aws_sqs as sqs,
aws_s3 as s3,
aws_lambda_nodejs as nodejs,
aws_iam as iam,
} from "aws-cdk-lib";
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
export class TestCdkArticleEmailsStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const bucket = new s3.Bucket(this, "Bucket", {});
const cache = new CacheStorage(this, "Cache");
const processedEmailsQueue = new sqs.Queue(this, "ProcessedEmailsQueue");
const responseQueue = new sqs.Queue(this, "ResponseQueue");
// Create the S3 event trigger.
const trigger = new S3EventTrigger.Builder()
.withScope(this)
.withIdentifier("Trigger")
.withCacheStorage(cache)
.withBucket(bucket)
.build();
// Create the email text processor
const emailProcessor = new EmailTextProcessor.Builder()
.withScope(this)
.withIdentifier("EmailProcessor")
.withCacheStorage(cache)
.withSource(trigger)
.withOutputFormat("text")
.withIncludeAttachments(false)
.build();
// Create the SQS storage connector for processed emails
const processedEmailsConnector = new SqsStorageConnector.Builder()
.withScope(this)
.withIdentifier("ProcessedEmailsConnector")
.withCacheStorage(cache)
.withSource(emailProcessor)
.withDestinationQueue(processedEmailsQueue)
.build();
const orchestratorLambda = new nodejs.NodejsFunction(
this,
"OrchestratorLambda",
{
entry: path.join("lambda", "index.ts"),
handler: "handler",
runtime: lambda.Runtime.NODEJS_20_X,
environment: {
RESPONSE_QUEUE_URL: responseQueue.queueUrl,
},
memorySize: 2048,
timeout: cdk.Duration.minutes(10),
}
);
orchestratorLambda.addToRolePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["bedrock:InvokeModel"],
resources: ["*"],
})
);
// Grant Lambda permissions to read from processedEmailsQueue and write to responseQueue
processedEmailsQueue.grantConsumeMessages(orchestratorLambda);
responseQueue.grantSendMessages(orchestratorLambda);
processedEmailsConnector.grantReadProcessedDocuments(orchestratorLambda);
emailProcessor.grantReadProcessedDocuments(orchestratorLambda);
// Set up event source mapping for Lambda
new lambda.EventSourceMapping(this, "OrchestratorEventSourceMapping", {
target: orchestratorLambda,
batchSize: 10,
eventSourceArn: processedEmailsQueue.queueArn,
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import { SQSEvent } from "aws-lambda";
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
import { orchestrator } from "./agents";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { Readable } from "stream";
const sqs = new SQSClient({});
const s3 = new S3Client({});
export const handler = async (event: SQSEvent): Promise<void> => {
for (const record of event.Records) {
const eventData = JSON.parse(record.body);
console.log("Event Data: ", JSON.stringify(eventData, null, 2));
const documentUrl = eventData.data.document.url;
const subject = eventData.data.metadata.title;
// Extract the S3 bucket and key from the URL
const { bucketName, objectKey } = parseS3Url(documentUrl);
console.log(`Bucket: ${bucketName}, Key: ${objectKey}`);
try {
// Retrieve the document from S3
const objectContent = await getDocumentFromS3(bucketName, objectKey);
console.log("Document Content:\n", objectContent);
const response = await orchestrator.routeRequest(
objectContent,
"userId",
"sessionId"
);
// Print the subject and text before sending the message
console.log("Subject:", `Re: ${subject}`);
console.log("Text:", response.output);
const sendMessageCommand = new SendMessageCommand({
QueueUrl: process.env.RESPONSE_QUEUE_URL!,
MessageBody: JSON.stringify({
subject: `Re: ${subject}`,
text: response.output,
handlingAgent: response.metadata.agentName,
}),
});
await sqs.send(sendMessageCommand);
} catch (error) {
console.error("Error retrieving document from S3:", error);
}
}
};
// Helper function to parse S3 URL
const parseS3Url = (url: string): { bucketName: string; objectKey: string } => {
const match = url.match(/^s3:\/\/([^\/]+)\/(.+)$/);
if (!match) {
throw new Error(`Invalid S3 URL: ${url}`);
}
return {
bucketName: match[1],
objectKey: match[2],
};
};
// Helper function to retrieve the document from S3
const getDocumentFromS3 = async (
bucketName: string,
key: string
): Promise<string> => {
const command = new GetObjectCommand({
Bucket: bucketName,
Key: key,
});
const response = await s3.send(command);
const streamToString = (stream: Readable): Promise<string> =>
new Promise((resolve, reject) => {
const chunks: Uint8Array[] = [];
stream.on("data", (chunk) => chunks.push(chunk));
stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
stream.on("error", reject);
});
return streamToString(response.Body as Readable);
};
handlingAgent
field in the output shows which specialized agent was selected to handle the specific inquiry.- 📚 Documentation: Get comprehensive guidance and details on how to use the toolkit effectively.
- 🛠️ GitHub Repository: Access the source code, contribute, or browse issues and updates.
- 📚 Documentation: Comprehensive guidance
- 🛠️ GitHub Repository: Access the source code, contribute, or browse issues and updates.