import { defineAgent, defineTool, defineFunction } from "@zavu/functions"
import Zavudev from "@zavudev/sdk"
const zavu = new Zavudev({
apiKey: process.env.ZAVU_API_KEY!,
baseURL: process.env.ZAVU_API_BASE_URL,
})
defineAgent({
senderId: process.env.SENDER_ID!,
name: "Shop Helper",
provider: "zavu",
model: "openai/gpt-4o-mini",
channels: ["whatsapp"],
prompt: `Eres el asistente de Acme Shop en WhatsApp.
Tu trabajo:
- Ayudar a los clientes a encontrar productos (call recommend).
- Decirles el estado de sus pedidos (call lookup_order).
- Tomar el pedido si quieren comprar directo desde el chat (call create_order).
Tono:
- Amistoso, breve.
- SIEMPRE incluye el precio cuando recomiendes un producto.
- Si no encuentras lo que piden, ofrece alternativas, nunca digas "no tengo".
Reglas:
- Nunca inventes IDs, precios o stock — usa las tools.
- Si el cliente pregunta por algo no relacionado a la tienda, dile:
"Para eso te paso con un agente humano" y llama escalate.`,
})
// 1) Order lookup
defineTool({
name: "lookup_order",
description:
"Get the status of a customer's order. Use when they ask about delivery, " +
"tracking, where their package is.",
parameters: {
type: "object",
properties: {
orderId: { type: "string", description: "Order number like ACME-12345." },
},
required: ["orderId"],
},
handler: async ({ orderId }, ctx) => {
const order = await fetch(
`${process.env.SHOP_API_URL}/orders/${orderId}`,
{ headers: { authorization: `Bearer ${process.env.SHOP_API_KEY}` } }
).then((r) => (r.ok ? r.json() : null))
if (!order) return { error: "not_found", message: `No encuentro el pedido ${orderId}.` }
if (order.customerPhone !== ctx?.contactPhone) {
return { error: "not_yours", message: "Ese pedido está vinculado a otro número." }
}
return {
orderId: order.id,
status: order.status, // "processing" | "shipped" | "out_for_delivery" | "delivered"
eta: order.estimatedDeliveryAt,
tracking: order.trackingUrl,
items: order.items.map((i) => `${i.qty}× ${i.name}`),
summary:
order.status === "shipped"
? `Tu pedido fue despachado. Llega aprox ${formatDay(order.estimatedDeliveryAt)}. Tracking: ${order.trackingUrl}`
: order.status === "delivered"
? `Ya fue entregado el ${formatDay(order.deliveredAt)}. ¿Algún problema?`
: `Estado: ${order.status}. ETA: ${formatDay(order.estimatedDeliveryAt)}`,
}
},
})
// 2) Product recommendation
defineTool({
name: "recommend",
description:
"Search the catalog and return 3-5 products matching the customer's needs. " +
"Use when they describe what they want (e.g. 'a gift for my dad under $80, " +
"he likes camping').",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Customer's request, paraphrased." },
maxPrice: { type: "number", description: "Budget cap in USD." },
category: { type: "string", description: "Filter by category if obvious." },
},
required: ["query"],
},
handler: async ({ query, maxPrice, category }) => {
const res = await fetch(`${process.env.SHOP_API_URL}/search`, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${process.env.SHOP_API_KEY}`,
},
body: JSON.stringify({
query,
filters: {
...(maxPrice ? { priceLte: maxPrice } : {}),
...(category ? { category } : {}),
inStock: true,
},
limit: 5,
}),
}).then((r) => r.json())
return {
results: res.products.map((p) => ({
id: p.id,
name: p.name,
price: `$${p.price}`,
url: `${process.env.SHOP_API_URL}/p/${p.slug}`,
whyGoodFit: p.relevanceReason, // your search engine's explanation
})),
count: res.products.length,
}
},
})
// 3) Create an order from chat
defineTool({
name: "create_order",
description:
"Place an order on behalf of the customer. ONLY call after they explicitly " +
"confirmed the items and shipping address. Always confirm the total before " +
"calling.",
parameters: {
type: "object",
properties: {
items: {
type: "array",
items: {
type: "object",
properties: {
productId: { type: "string" },
quantity: { type: "number" },
},
required: ["productId", "quantity"],
},
},
shippingAddress: { type: "string", description: "Full address line." },
paymentMethod: {
type: "string",
enum: ["card_on_file", "cash_on_delivery"],
},
},
required: ["items", "shippingAddress", "paymentMethod"],
},
handler: async ({ items, shippingAddress, paymentMethod }, ctx) => {
const res = await fetch(`${process.env.SHOP_API_URL}/orders`, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${process.env.SHOP_API_KEY}`,
},
body: JSON.stringify({
items,
shippingAddress,
paymentMethod,
customerPhone: ctx?.contactPhone,
source: "whatsapp",
}),
}).then((r) => r.json())
if (res.error) {
return { error: res.error, message: res.message }
}
return {
orderId: res.id,
total: `$${res.total}`,
eta: res.estimatedDeliveryAt,
summary:
`Pedido ${res.id} creado por $${res.total}. ` +
`Envío a "${shippingAddress.slice(0, 40)}…". ` +
`Llega aprox ${formatDay(res.estimatedDeliveryAt)}.`,
}
},
})
// 4) Escalation
defineTool({
name: "escalate",
description:
"Forward this conversation to a human agent. Call when the customer needs " +
"help outside shopping (returns disputes, account issues, complaints).",
parameters: {
type: "object",
properties: { reason: { type: "string" } },
required: ["reason"],
},
handler: async ({ reason }, ctx) => {
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
text: `:wave: Escalation from ${ctx?.contactPhone}: ${reason}`,
}),
})
return { escalated: true, summary: "Te paso con un agente. Respuesta en ~10 min." }
},
})
// PROACTIVE: cart recovery trigger.
//
// When your shop backend sends a `cart.abandoned` event to your Zavu function
// (via your own webhook → custom event → trigger fanout), the agent fires off
// a nudge message.
//
// For this to work, you'd:
// 1. Have your shop POST to your Zavu function's HTTP URL (httpEnabled=true)
// with the abandoned-cart payload.
// 2. Format and send a reminder via the SDK.
export default defineFunction(async (event, ctx) => {
// Handle non-standard event from your shop: shop posts directly to fn URL.
// event.body is a stringified JSON body when called via the public HTTP path.
const body = typeof event.body === "string" ? JSON.parse(event.body) : event
if (body.type !== "cart.abandoned") return { ok: true }
const { customerPhone, items, cartUrl, total } = body
await zavu.messages.send({
to: customerPhone,
channel: "whatsapp",
"Zavu-Sender": process.env.SENDER_ID!,
text:
`¡Hola! Vi que dejaste algo en tu carrito 🛒\n\n` +
items.slice(0, 3).map((i: any) => `• ${i.name}`).join("\n") +
(items.length > 3 ? `\n• +${items.length - 3} más` : "") +
`\n\nTotal: $${total}\n${cartUrl}\n\n¿Querés que te ayude a finalizar?`,
})
return { ok: true, nudged: true }
})
function formatDay(iso: string) {
return new Date(iso).toLocaleDateString("es-ES", {
weekday: "long",
day: "numeric",
month: "short",
})
}