Overview
AI-powered customer support that integrates with Zendesk for ticketing and Lua Data API for knowledge base search. What it does:- Search knowledge base with semantic search
- Create support tickets in Zendesk
- Check ticket status
- Answer common questions
- Escalate to human agents
Complete Implementation
src/index.ts
Copy
import { LuaAgent, LuaSkill, LuaWebhook, LuaJob } from "lua-cli";
import {
SearchKnowledgeBaseTool,
CreateTicketTool,
GetTicketStatusTool,
UpdateTicketTool
} from "./tools/SupportTools";
// Support skill
const supportSkill = new LuaSkill({
name: "customer-support",
description: "AI-powered customer support with ticketing and knowledge base",
context: `
This skill provides customer support.
- search_knowledge_base: Use first to find answers in documentation
- create_ticket: Create Zendesk ticket if knowledge base can't help
- get_ticket_status: Check status of existing tickets
- update_ticket: Add information to existing tickets
Always search knowledge base before creating tickets.
Be empathetic and professional.
Escalate complex issues to human agents.
`,
tools: [
new SearchKnowledgeBaseTool(),
new CreateTicketTool(),
new GetTicketStatusTool(),
new UpdateTicketTool()
]
});
// Zendesk webhook for ticket updates
const zendeskWebhook = new LuaWebhook({
name: 'zendesk-webhook',
description: 'Handle Zendesk ticket update events',
secret: env('ZENDESK_WEBHOOK_SECRET'),
execute: async (event) => {
if (event.type === 'ticket.solved') {
// Get user ID from ticket metadata
const userId = event.data.custom_fields?.lua_user_id;
if (userId) {
const user = await User.get(userId);
await user.send([{
type: 'text',
text: `✅ Your support ticket #${event.data.id} has been resolved! We hope we were able to help.`
}]);
}
}
return { received: true };
}
});
// Daily follow-up job for open tickets
const ticketFollowUpJob = new LuaJob({
name: 'ticket-followup',
description: 'Send follow-up messages for open tickets',
schedule: {
type: 'cron',
pattern: '0 10 * * *' // Daily at 10 AM
},
execute: async (job) => {
const tickets = await Data.search('support_tickets', 'status:open', 50);
const user = await job.user();
if (tickets.count > 0) {
await user.send([{
type: 'text',
text: `You have ${tickets.count} open support ticket(s). Need any updates?`
}]);
}
}
});
// Configure agent (v3.0.0)
export const agent = new LuaAgent({
name: "support-agent",
persona: `You are Alex, a customer support specialist.
Your role:
- Help customers find answers to their questions
- Search the knowledge base for solutions
- Create support tickets when needed
- Track and update existing tickets
- Provide empathetic and professional support
Communication style:
- Patient and empathetic
- Clear and professional
- Solution-oriented
- Reassuring and supportive
Workflow:
1. First, search the knowledge base for answers
2. If no solution found, offer to create a ticket
3. For existing tickets, help track status
4. For urgent issues, escalate to priority support
Best practices:
- Always search knowledge base first
- Gather all details before creating tickets
- Set realistic expectations for response times
- Thank customers for their patience
- Confirm resolution before closing
When to escalate:
- Billing disputes over $500
- Account security issues
- Legal or compliance matters
- VIP customer requests`,
skills: [supportSkill],
webhooks: [zendeskWebhook],
jobs: [ticketFollowUpJob]
});
v3.0.0 Features: This demo uses
LuaAgent with webhooks for Zendesk events and scheduled jobs for ticket follow-ups.src/tools/SupportTools.ts
Copy
import { LuaTool, Data, env } from "lua-cli";
import { z } from "zod";
// 1. Search Knowledge Base (Lua Data with Vector Search)
export class SearchKnowledgeBaseTool implements LuaTool {
name = "search_knowledge_base";
description = "Search help articles and documentation";
inputSchema = z.object({
query: z.string().describe("User's question or search query")
});
async execute(input: z.infer<typeof this.inputSchema>) {
// Use Lua's vector search for semantic matching
const results = await Data.search(
'help_articles',
input.query,
5,
0.7
);
if (results.count === 0) {
return {
articles: [],
message: "No articles found. Would you like to create a support ticket?"
};
}
return {
articles: results.data.map(entry => ({
id: entry.id,
title: entry.data.title,
content: entry.data.content.substring(0, 300) + '...',
category: entry.data.category,
relevance: `${Math.round(entry.score * 100)}% match`,
url: entry.data.url
})),
count: results.count,
message: `Found ${results.count} helpful articles`
};
}
}
// 2. Create Zendesk Ticket (External API)
export class CreateTicketTool implements LuaTool {
name = "create_ticket";
description = "Create a support ticket in Zendesk";
inputSchema = z.object({
subject: z.string().describe("Ticket subject/summary"),
description: z.string().describe("Detailed description of the issue"),
priority: z.enum(['low', 'normal', 'high', 'urgent']).default('normal'),
customerEmail: z.string().email().describe("Customer's email address"),
customerName: z.string().describe("Customer's name")
});
async execute(input: z.infer<typeof this.inputSchema>) {
const zendeskKey = env('ZENDESK_API_KEY');
const zendeskSubdomain = env('ZENDESK_SUBDOMAIN');
if (!zendeskKey || !zendeskSubdomain) {
throw new Error('Zendesk API credentials not configured');
}
// Create ticket in Zendesk
const response = await fetch(
`https://${zendeskSubdomain}.zendesk.com/api/v2/tickets.json`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${Buffer.from(`${input.customerEmail}/token:${zendeskKey}`).toString('base64')}`
},
body: JSON.stringify({
ticket: {
subject: input.subject,
description: input.description,
priority: input.priority,
requester: {
name: input.customerName,
email: input.customerEmail
},
tags: ['ai_created', 'chat']
}
})
}
);
if (!response.ok) {
throw new Error(`Failed to create ticket: ${response.statusText}`);
}
const data = await response.json();
// Also log ticket in Lua Data for our records
await Data.create('support_tickets', {
zendeskId: data.ticket.id,
subject: input.subject,
customerEmail: input.customerEmail,
priority: input.priority,
createdAt: new Date().toISOString()
}, `${input.subject} ${input.description}`);
return {
success: true,
ticketId: data.ticket.id,
ticketUrl: data.ticket.url,
message: `Ticket #${data.ticket.id} created. We'll respond within 24 hours.`
};
}
}
// 3. Get Ticket Status (External API)
export class GetTicketStatusTool implements LuaTool {
name = "get_ticket_status";
description = "Check the status of a support ticket";
inputSchema = z.object({
ticketId: z.string().describe("Ticket ID")
});
async execute(input: z.infer<typeof this.inputSchema>) {
const zendeskKey = env('ZENDESK_API_KEY');
const zendeskSubdomain = env('ZENDESK_SUBDOMAIN');
// Get ticket from Zendesk
const response = await fetch(
`https://${zendeskSubdomain}.zendesk.com/api/v2/tickets/${input.ticketId}.json`,
{
headers: {
'Authorization': `Basic ${Buffer.from(`${env('ZENDESK_EMAIL')}/token:${zendeskKey}`).toString('base64')}`
}
}
);
if (!response.ok) {
throw new Error(`Ticket not found: ${input.ticketId}`);
}
const data = await response.json();
const ticket = data.ticket;
return {
ticketId: ticket.id,
subject: ticket.subject,
status: ticket.status,
priority: ticket.priority,
createdAt: new Date(ticket.created_at).toLocaleDateString(),
updatedAt: new Date(ticket.updated_at).toLocaleDateString(),
assignee: ticket.assignee_id ? 'Assigned to support agent' : 'Not yet assigned',
message: this.getStatusMessage(ticket.status)
};
}
private getStatusMessage(status: string): string {
const messages = {
new: "Your ticket is in queue and will be reviewed shortly",
open: "Our team is currently working on your ticket",
pending: "Waiting for your response",
solved: "This ticket has been resolved",
closed: "This ticket is closed"
};
return messages[status] || "Status unknown";
}
}
// 4. Update Ticket (External API)
export class UpdateTicketTool implements LuaTool {
name = "update_ticket";
description = "Add a comment or update to an existing ticket";
inputSchema = z.object({
ticketId: z.string(),
comment: z.string().describe("Additional information or update")
});
async execute(input: z.infer<typeof this.inputSchema>) {
const zendeskKey = env('ZENDESK_API_KEY');
const zendeskSubdomain = env('ZENDESK_SUBDOMAIN');
// Add comment to ticket
const response = await fetch(
`https://${zendeskSubdomain}.zendesk.com/api/v2/tickets/${input.ticketId}.json`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Basic ${Buffer.from(`${env('ZENDESK_EMAIL')}/token:${zendeskKey}`).toString('base64')}`
},
body: JSON.stringify({
ticket: {
comment: {
body: input.comment,
public: true,
author_id: 'end-user'
}
}
})
}
);
if (!response.ok) {
throw new Error('Failed to update ticket');
}
return {
success: true,
ticketId: input.ticketId,
message: "Your comment has been added to the ticket"
};
}
}
Environment Setup
Copy
# .env
ZENDESK_API_KEY=your_zendesk_api_key
ZENDESK_SUBDOMAIN=your_company
ZENDESK_EMAIL=[email protected]
Seed Knowledge Base
Copy
// scripts/seed-knowledge-base.ts
import { Data } from "lua-cli";
const articles = [
{
title: "How to reset your password",
content: "To reset your password: 1) Click 'Forgot Password' 2) Enter your email 3) Check your inbox for reset link 4) Create new password",
category: "Account",
url: "/help/reset-password"
},
{
title: "Shipping and delivery times",
content: "Standard shipping takes 5-7 business days. Express shipping takes 2-3 business days. Free shipping on orders over $50.",
category: "Shipping",
url: "/help/shipping"
},
{
title: "Return policy",
content: "Returns accepted within 30 days of purchase. Items must be unused with original packaging. Refunds processed within 5-10 business days.",
category: "Returns",
url: "/help/returns"
}
];
async function seedKnowledgeBase() {
for (const article of articles) {
const searchText = `${article.title} ${article.content} ${article.category}`;
await Data.create('help_articles', article, searchText);
}
console.log('✅ Knowledge base seeded');
}
seedKnowledgeBase();
Key Features
External API
Integrates with Zendesk ticketing
Vector Search
AI-powered knowledge base search
Hybrid Approach
Best of both worlds
Smart Escalation
Search first, create ticket only if needed
Next Demo
Hotel Booking Agent
See reservation management with Lua Data API

