import { defineAgent, defineTool } from "@zavu/functions"
defineAgent({
senderId: process.env.SENDER_ID!,
name: "Aria",
provider: "zavu",
model: "anthropic/claude-3-5-haiku-20241022", // good at instruction-following
channels: ["whatsapp"],
prompt: `Eres Aria, asistente de soporte de Acme Corp.
Tu misión:
1. Resolver preguntas frecuentes usando search_kb antes de cualquier otra cosa.
2. Si la pregunta es específica del cliente (cuenta, factura, pedido), crea
un ticket con create_ticket.
3. Si el cliente está enojado o pide "hablar con alguien", llama
escalate_to_human y promete que alguien responderá en 15 minutos.
Reglas:
- Respuestas cortas (WhatsApp).
- Cita la fuente del KB cuando uses información de ahí.
- NO inventes información. Si search_kb no devuelve nada relevante, di
"no tengo esa información" y crea un ticket.`,
})
defineTool({
name: "search_kb",
description:
"Search the knowledge base for FAQs, policies, how-tos. Call FIRST for any informational question.",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Customer's question, paraphrased if needed." },
topK: { type: "number", description: "Max results (default 3, max 10)." },
},
required: ["query"],
},
handler: async ({ query, topK = 3 }) => {
// Replace with your real vector search. Here we use the Convex action exposed
// via the SDK (Zavu's built-in agent knowledge base).
const results = await searchKnowledgeBase(query, topK)
return {
results: results.map((r) => ({
title: r.title,
excerpt: r.excerpt,
source: r.url,
})),
count: results.length,
}
},
})
defineTool({
name: "create_ticket",
description:
"Open a support ticket when you can't resolve the issue with KB lookup. " +
"Use for account-specific questions (billing, orders, refunds, account changes).",
parameters: {
type: "object",
properties: {
subject: { type: "string", description: "Short title, 5-10 words." },
details: { type: "string", description: "Full context of the issue, what the customer said." },
priority: {
type: "string",
enum: ["low", "normal", "high", "urgent"],
description: "low = informational, urgent = revenue/account at risk",
},
},
required: ["subject", "details", "priority"],
},
handler: async ({ subject, details, priority }, ctx) => {
const ticket = await fetch(`${process.env.HELPDESK_URL}/tickets`, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${process.env.HELPDESK_API_KEY}`,
},
body: JSON.stringify({
subject,
details,
priority,
contactPhone: ctx?.contactPhone,
source: "whatsapp",
}),
}).then((r) => r.json())
return {
ticketId: ticket.id,
eta: priority === "urgent" ? "15 minutos" : "1 día hábil",
summary: `Ticket #${ticket.id} creado. Te respondemos en ${
priority === "urgent" ? "15 min" : "1 día hábil"
}.`,
}
},
})
defineTool({
name: "check_ticket",
description:
"Look up the status of an existing ticket by ID.",
parameters: {
type: "object",
properties: { ticketId: { type: "string" } },
required: ["ticketId"],
},
handler: async ({ ticketId }, ctx) => {
const res = await fetch(`${process.env.HELPDESK_URL}/tickets/${ticketId}`, {
headers: { authorization: `Bearer ${process.env.HELPDESK_API_KEY}` },
})
if (!res.ok) {
return { error: "not_found", message: `No encuentro el ticket #${ticketId}.` }
}
const ticket = await res.json()
// Privacy guard: only return if the contact owns this ticket.
if (ticket.contactPhone !== ctx?.contactPhone) {
return { error: "not_yours", message: "Ese ticket no está asociado a tu número." }
}
return {
ticketId,
status: ticket.status,
lastUpdate: ticket.updatedAt,
summary: ticket.publicSummary,
}
},
})
defineTool({
name: "escalate_to_human",
description:
"Page the on-call team via Slack. Call when the customer is frustrated, " +
"explicitly asks for a human, or the issue is time-sensitive and outside your scope.",
parameters: {
type: "object",
properties: {
reason: { type: "string", description: "Why escalating (frustration, complexity, urgency)." },
summary: { type: "string", description: "What the customer needs, in 1-2 sentences." },
},
required: ["reason", "summary"],
},
handler: async ({ reason, summary }, ctx) => {
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
text:
`:rotating_light: *Customer needs human support*\n` +
`*From:* ${ctx?.contactPhone}\n` +
`*Reason:* ${reason}\n` +
`*Summary:* ${summary}\n` +
`<https://dashboard.zavu.dev/inbox?contact=${ctx?.contactPhone}|Open conversation>`,
}),
})
return {
escalated: true,
eta: "15 minutos",
summary: "Un agente humano responderá en aproximadamente 15 minutos.",
}
},
})