Restaurant23 — Phone Assistant (Recepcionista Telefónico AI)
Módulo premium de Restaurant23 que convierte cualquier número de teléfono del negocio en un recepcionista virtual capaz de responder, reservar, tomar pedidos para envío y gestionar pedidos de catering usando voz natural en español (y multi‑idioma).
No es una app aparte: es una vertical de alto valor dentro de Restaurant23, que reutiliza al 100% la infraestructura de Cadences (Twilio + ElevenLabs + D1 + Vectorize
- Workers AI) y se vende como add‑on por suscripción o por minuto.
1. Veredicto y posicionamiento
Sí lo construimos, pero como módulo de Restaurant23 (no como app separada). Razones:
- Infra existe: el pipeline
Twilio → Durable Object → ElevenLabs Conversational AIestá en producción (verfunctions/api/webhooks/twilio-call.jsysrc/durable-objects/ElevenLabsMediaStream.js). - Restaurant23 ya tiene el contexto del negocio (carta, menús del día, horarios, sala, reservas, FAQs, blog) en D1 a través de Cadences. El asistente solo tiene que consultarlo.
- El público objetivo se solapa: el restaurante que paga Restaurant23 para tener web + reservas online es exactamente el que más sufre las llamadas mientras sirve.
- Para tiendas de restauración online (catering, delivery, food‑shops) la llamada no es secundaria: es canal de venta. El asistente puede tomar el pedido completo, cobrar (link de pago por SMS/WhatsApp), confirmar dirección y agendar entrega.
Naming sugerido
- Comercial: Restaurant23 Phone Concierge o Recepcionista 23.
- Técnico interno:
phoneAssistant(campo enstorefrontConfig).
2. Casos de uso (dos perspectivas que conviven)
A. Restaurante físico clásico
| Caso | Acción del asistente |
|---|---|
| "¿Tenéis mesa para 4 esta noche a las 21:00?" | Consulta availability, ofrece slot, crea reserva, manda SMS/WhatsApp con código |
| "Quiero cancelar mi reserva" | Pide código o teléfono, busca, cancela, dispara workflow reservationCancelled |
| "¿Tenéis menú sin gluten?" | RAG sobre carta + alérgenos, responde con platos concretos |
| "¿A qué hora cerráis hoy?" | Lee schedules del día, responde |
| "Quiero hablar con el dueño" | Transfer a humano (Twilio <Dial>) o deja mensaje en voice_calls.summary + workflow |
B. Tienda de restauración online / catering / delivery
| Caso | Acción del asistente |
|---|---|
| "Quiero encargar 20 bandejas variadas para el sábado" | Recopila: fecha, hora, dirección, nº personas, alérgenos, presupuesto. Crea registro en catering_requests con estado pending_quote. Workflow notifica al equipo. |
| "Quiero pedir 2 paellas y 1 tortilla para llevar a las 14:00" | Lee carta + menus, arma carrito en online_orders, calcula total, manda link de pago Stripe por SMS/WhatsApp, confirma cuando se cobra. |
| "¿Cuánto cuesta el envío al CP 28010?" | Consulta delivery_zones (nuevo proyecto), responde precio y tiempo estimado. |
| "¿Qué hay de menú del día hoy?" | Lee menus filtrado por fecha actual, lo recita. |
| "Cambia mi pedido, quita la tortilla y añade croquetas" | Busca pedido por teléfono+código, modifica, recalcula, reenvía link si cambió el total. |
| "¿Cuándo llega mi pedido?" | Busca online_orders por teléfono, devuelve estado/ETA del repartidor. |
La diferencia clave: en (A) el asistente agenda; en (B) el asistente vende y cobra. El mismo motor sirve ambas, cambia el
phoneAssistant.modey losactionshabilitados.
3. Arquitectura
3.1 Pipeline de llamada (ya existe)
Cliente llama al nº Twilio del restaurante
│
▼
POST functions/api/webhooks/twilio-call.js
├─ Resuelve organización por toNumber (mapping a storefrontId)
├─ Carga storefrontConfig.phoneAssistant
├─ lookupClientByPhone() → si existe, dynamic_variables.client_*
├─ Construye contextPrompt (sección 3.3)
└─ Devuelve TwiML <Stream wss://.../api/elevenlabs/media-stream>
│
▼
Durable Object ElevenLabsMediaStream
├─ getElevenLabsCredentials(env, organizationId) ← per-tenant API key
├─ getSignedUrl() contra ElevenLabs
├─ conversation_initiation_client_data:
│ · conversation_config_override.agent.prompt = contextPrompt
│ · conversation_config_override.agent.first_message = greeting
│ · dynamic_variables = { caller_*, business_*, last_order_id, ... }
└─ Bridge bidireccional audio Twilio ↔ ElevenLabs
│
▼
ElevenLabs Agent (configurado con tools/webhooks que apuntan a):
· POST /api/storefront/[id]/availability (consulta slots)
· POST /api/storefront/[id]/reservations (crea reserva)
· POST /api/storefront/[id]/orders (crea pedido online)
· POST /api/storefront/[id]/catering-requests (alta solicitud catering)
· POST /api/storefront/[id]/knowledge/search (RAG carta/FAQ/alérgenos)
· POST /api/storefront/[id]/payment-link (genera Stripe link, manda SMS)
· POST /api/storefront/[id]/transfer (transfer humano si escala)
│
▼
Al finalizar:
POST functions/api/webhooks/elevenlabs.js (call.ended)
├─ Guarda transcript + analysis + cost en voice_calls
├─ Genera resumen estructurado (intent, entities, outcome)
└─ Dispara workflow voice_call_ended → notificaciones, CRM, métricas
3.2 Configuración por tenant
Extender src/config/storefront.config.ts:
export interface StorefrontConfig {
// ...existing fields
phoneAssistant?: {
enabled: boolean;
mode: 'reservations' | 'orders' | 'hybrid'; // A, B, o ambas
twilioNumber: string; // E.164
elevenLabsAgentId: string; // override per-tenant si aplica
voice: {
voiceId: string; // Antoni, Sarah, custom...
language: 'es-ES' | 'es-MX' | 'en-US' | 'ca-ES' | ...;
speakingRate?: number;
};
persona: {
name: string; // "Lucía, recepcionista de La Tagliatella"
tone: 'formal' | 'cercano' | 'profesional';
greeting?: string; // override del first_message
};
actions: {
reservations: boolean;
onlineOrders: boolean;
cateringQuotes: boolean;
deliveryStatus: boolean;
menuQuestions: boolean;
transferToHuman: { enabled: boolean; number?: string; hours?: string };
};
knowledge: {
includeCarta: boolean;
includeMenusDelDia: boolean;
includeAllergens: boolean;
includeFaqs: boolean;
includeBlog: boolean;
extraPromptSnippets?: string[]; // "Somos halal", "Sin alcohol los viernes"...
};
delivery?: {
enabled: boolean;
zonesProject: string; // projectId de delivery_zones
minOrder: number;
paymentMethod: 'stripe_link' | 'cod' | 'on_pickup';
};
limits: {
maxCallSeconds: number; // hard cap
monthlyMinutesIncluded: number; // billing
escalateAfterFailedTurns: number; // p.ej. 3 fallos → transfer
};
compliance: {
announceRecording: boolean; // GDPR
recordingDisclosure: string; // texto legal
dataRetentionDays: number;
};
};
}
3.3 Context builder (prompt dinámico)
Pseudo‑código del bloque que twilio-call.js añadiría antes de generar TwiML:
const cfg = org.storefrontConfig.phoneAssistant;
const ctx = await buildPhoneAssistantContext(env, org.id, {
includeCarta: cfg.knowledge.includeCarta,
includeMenuToday: cfg.knowledge.includeMenusDelDia,
includeSchedulesToday: true,
includeOpenReservationsForCaller: caller.phone,
includeLastOrdersForCaller: caller.phone,
});
const contextPrompt = renderPrompt(`
Eres ${cfg.persona.name}. Tono: ${cfg.persona.tone}.
Negocio: ${org.name}, ${org.address}.
HOY ${ctx.dateHuman}. Horario: ${ctx.scheduleToday}.
CAPACIDADES:
${cfg.actions.reservations ? '- Puedes crear/cancelar/modificar reservas.' : ''}
${cfg.actions.onlineOrders ? '- Puedes tomar pedidos para llevar/envío y mandar link de pago por SMS.' : ''}
${cfg.actions.cateringQuotes ? '- Puedes recoger solicitudes de catering (no cierres precio, lo hace el equipo).' : ''}
MENÚ DEL DÍA: ${ctx.menuToday || '(no hay menú del día publicado)'}
PLATOS DESTACADOS: ${ctx.topDishes}
ALÉRGENOS RELEVANTES: ${ctx.allergens}
CLIENTE QUE LLAMA:
${caller.known ? `- Es ${caller.name}, última visita ${caller.lastVisit}, pedidos previos: ${caller.lastOrders}` : '- Cliente nuevo o no identificado.'}
REGLAS:
- Si te piden algo fuera de capacidades, ofrece transferir a humano (${cfg.actions.transferToHuman.number}).
- Confirma siempre nombre, teléfono y dirección antes de cerrar pedido de envío.
- Si dudas del CP/zona, llama a la tool delivery_zones.
- ${cfg.compliance.recordingDisclosure}
`);
3.4 Base de conocimiento (RAG)
Reutilizar el patrón de Memora hybrid search (storefronts/memora/src/lib/retrieval/):
dense (@cf/baai/bge-m3 1024d en Vectorize) + sparse (FTS5/BM25 en D1) + structured
(SQL filters por categoría/alérgeno/precio) fusionados con RRF.
Nuevo índice Vectorize: restaurant-knowledge (chunks de carta, FAQs, política
catering, condiciones envío, blog). Indexación en background al editar carta/blog
desde el admin de Cadences (hook en data_rows updates).
Endpoint nuevo:
POST /api/storefront/[id]/knowledge/search→ body{ query, filters?, topK }→ respuesta estructurada para que el agente la cite ({ chunks: [{ text, source }] }).
3.5 Acciones / Tools del agente
Cada tool es un endpoint público de Pages Function autenticado con un token corto
firmado que el twilio-call.js mete en dynamic_variables.tool_token (TTL = duración
máxima de llamada). Así ElevenLabs puede llamar sin filtrar credenciales reales.
| Tool | Endpoint | Idempotencia |
|---|---|---|
check_availability |
GET /api/storefront/[id]/availability |
Read-only |
create_reservation |
POST /api/storefront/[id]/reservations |
Idempotency-Key = callSid+turn |
cancel_reservation |
PUT /api/storefront/[id]/reservations |
sí |
start_order |
POST /api/storefront/[id]/orders |
callSid |
add_order_item |
PATCH /api/storefront/[id]/orders/{id} |
item-key |
quote_delivery |
GET /api/storefront/[id]/delivery/quote?cp=... |
read-only |
request_payment_link |
POST /api/storefront/[id]/payment-link |
order id |
create_catering_request |
POST /api/storefront/[id]/catering-requests |
callSid |
escalate_to_human |
POST /api/storefront/[id]/transfer |
— |
search_knowledge |
POST /api/storefront/[id]/knowledge/search |
read-only |
4. Modelo de datos (D1)
4.1 Normalizar voice_calls
⚠️ Inconsistencia detectada:
functions/api/storefront/voice-call.jsusacallTypeycadencesPromptque no existen enmigrations/0002_add_whatsapp_voice_tables.sql. Antes de seguir, crear migración que añada esos campos o renombre.
Migración nueva sugerida 0090_phone_assistant.sql:
ALTER TABLE voice_calls ADD COLUMN callType TEXT; -- 'reservation' | 'order' | 'catering' | 'info' | 'mixed'
ALTER TABLE voice_calls ADD COLUMN intent TEXT; -- intent detectado
ALTER TABLE voice_calls ADD COLUMN outcome TEXT; -- 'completed' | 'transferred' | 'abandoned' | 'failed'
ALTER TABLE voice_calls ADD COLUMN linkedOrderId TEXT;
ALTER TABLE voice_calls ADD COLUMN linkedReservationId TEXT;
ALTER TABLE voice_calls ADD COLUMN linkedCateringId TEXT;
ALTER TABLE voice_calls ADD COLUMN cadencesPrompt TEXT; -- snapshot del prompt usado
ALTER TABLE voice_calls ADD COLUMN storefrontId TEXT;
CREATE INDEX idx_voice_calls_storefront ON voice_calls(storefrontId, startedAt);
4.2 Nuevos proyectos en storefrontConfig.projectIds
{
"projectIds": {
// ...existentes
"knowledge": "proj_xxx", // chunks RAG (carta, FAQ, política)
"online_orders": "proj_xxx", // pedidos para llevar/envío
"catering_requests": "proj_xxx", // solicitudes de catering pendientes de presupuestar
"delivery_zones": "proj_xxx" // CPs, precios y tiempos
}
}
Schemas mínimos:
- online_orders:
id, code, customerId, channel('phone'|'web'|'whatsapp'), items JSON, subtotal, deliveryFee, total, status('cart'|'pending_payment'|'paid'|'preparing'| 'out_for_delivery'|'delivered'|'cancelled'), deliveryType('pickup'|'delivery'), address, postalCode, scheduledFor, paymentLinkUrl, paidAt, callSid?, notes. - catering_requests:
id, code, customerId, eventDate, headcount, addressOrVenue, budgetRange, dietaryRestrictions, preferredDishes, contactPhone, status('pending_quote' |'quoted'|'accepted'|'rejected'|'expired'), assignedTo, callSid?, notes. - delivery_zones:
id, postalCode, zoneName, fee, etaMinutes, minOrder, active.
5. Integración con Restaurant23 existente
- Setup: añadir paso "Asistente telefónico" en
setup.js(alta de número Twilio o port‑in, selección de voz, modoreservations/orders/hybrid, horarios de atención humana). - Carta y FAQs ya editables desde el admin existente alimentan el RAG sin trabajo
extra (hook de reindexación en
data_rowswrites paraprojectKey in ('carta','menus','faqs','knowledge')). - Reservas y pedidos del asistente caen en las mismas tablas que web/WhatsApp → un solo dashboard, una sola caja.
- Sección nueva en admin:
Llamadas→ lista devoice_callsfiltrada por storefront, con audio player, transcript, intent, outcome, links a la reserva/pedido creados, coste y minutos consumidos. - Widget en home admin: "Hoy hemos respondido 34 llamadas (28 resueltas por IA, 6 escaladas), 12 reservas y 9 pedidos generados".
6. Modelo de negocio
| Plan | Precio sugerido | Incluye |
|---|---|---|
phone-lite |
+29 €/mes | Solo reservas y FAQ. 100 min incluidos, 0,15 €/min extra |
phone-pro |
+79 €/mes | Reservas + pedidos online + pago. 400 min, 0,12 €/min extra |
phone-catering |
+149 €/mes | Pro + catering + zonas envío + multi‑idioma. 800 min, 0,10 €/min extra |
phone-enterprise |
a medida | Multi‑local, transferencia avanzada, voz custom clonada, SLA |
Coste real estimado (referencia): Twilio voice ~0,013 €/min + ElevenLabs Conv AI ~0,08 €/min + Workers AI/Vectorize despreciable + número Twilio 1 €/mes. Margen sano incluso en el plan lite.
7. Riesgos y mitigaciones
| Riesgo | Mitigación |
|---|---|
| GDPR / grabación de llamadas | Disclaimer obligatorio configurable, retention configurable, derecho al olvido por teléfono, export por cliente |
Schema mismatch voice_calls |
Migración 0090 antes de ampliar |
| Credenciales ElevenLabs por tenant | getElevenLabsCredentials(env, organizationId) ya existe; validar que el flujo inbound lo usa correctamente (ahora va por DO, revisar que la API key correcta llega) |
| Usuarios mayores / acentos fuertes | Voz es-ES natural, speakingRate 0.95, fallback rápido a humano tras N turnos fallidos |
| Alucinaciones sobre alérgenos (riesgo legal) | Tool obligatoria search_knowledge antes de afirmar algo sobre alérgenos; si la confianza < umbral, responder "lo confirmo con cocina y te llamo" + crea tarea |
| Pedidos fraudulentos / pruebas | Confirmación SMS de pedido + cobro por link antes de empezar a preparar; rate‑limit por nº origen |
| Caída de ElevenLabs | TwiML fallback a <Say> con mensaje "ahora mismo no puedo atenderte, déjame un mensaje" + grabación a voice_calls |
| Coste descontrolado | limits.maxCallSeconds en DO + alarma a monthlyMinutesIncluded |
| Multi‑local (cadenas) | storefrontId por local, número por local, agregación en cuenta padre |
8. Roadmap MVP (4 fases)
Fase 1 — Piloto interno (1 restaurante real, modo reservations)
- Migración 0090 (normalizar
voice_calls). phoneAssistantenStorefrontConfig+ UI mínima de configuración.- Context builder + prompt dinámico con carta/horarios/menú del día.
- Tools:
check_availability,create_reservation,cancel_reservation,search_knowledge,escalate_to_human. - Listado de llamadas en admin con audio + transcript.
- KPIs: % resueltas, duración media, coste/llamada, NPS post‑llamada por SMS.
Fase 2 — Pedidos para llevar y envío (modo orders)
- Proyectos
online_ordersydelivery_zones. - Tools
start_order,add_order_item,quote_delivery,request_payment_link(Stripe Payment Links + SMS Twilio). - Estado del pedido vía
deliveryStatustool. - Integración con TPV (opcional, segunda iteración).
Fase 3 — Catering y solicitudes complejas
- Proyecto
catering_requests. - Tool
create_catering_requestcon captura estructurada (fecha, headcount, alérgenos, presupuesto, ubicación). - Workflow notifica al equipo, plantilla de presupuesto, follow‑up automático.
Fase 4 — Multi‑local, voz clonada, multi‑idioma, outbound
- Voz clonada de un humano del local (consentimiento ElevenLabs).
ca-ES,eu-ES,gl-ES,en-US,fr-FR,de-DE.- Outbound: confirmación de reserva al día siguiente, recuperación de pedidos abandonados, encuestas post‑servicio.
- Cadenas / franquicias: panel agregado, routing inteligente al local más cercano por CP del llamante.
9. Métricas a trackear desde el día 1
calls_total,calls_resolved_by_ai,calls_transferred,calls_abandoned.reservations_created_via_phone,orders_created_via_phone,catering_requests_created_via_phone.revenue_attributed_to_phone(suma deonline_orders.totalconchannel='phone').avg_call_seconds,avg_cost_per_call,cost_per_resolution.containment_rate= resueltas IA / total.csat_phone(SMS post‑llamada con 1 pregunta).- Top intents fallidos → cola de mejora del prompt/RAG.
10. Decisiones abiertas
- ¿Pago: Stripe Payment Link enviado por SMS, o llamada cobrada con DTMF (PCI‑DSS)? → Recomendado link por SMS (sin alcance PCI propio).
- ¿Reusar el mismo
elevenLabsAgentIdglobal con prompt override por tenant, o un agente por tenant? → Override por tenant en Fase 1 (más barato, más rápido); agente dedicado solo si el cliente pagaenterprise. - ¿Indexación RAG: incremental por hook o batch nocturno? → Hook para
carta,menus,faqs; batch parablog. - ¿Quién es el "dueño" del número Twilio: nosotros (revenue share) o el cliente
(BYO‑number)? → Por defecto nosotros para acelerar onboarding; BYO en
enterprise.
11. Referencias en el código
- Webhook entrada Twilio:
functions/api/webhooks/twilio-call.js - Bridge WebSocket:
src/durable-objects/ElevenLabsMediaStream.js - Endpoint media-stream:
functions/api/elevenlabs/media-stream.js - Webhook eventos ElevenLabs:
functions/api/webhooks/elevenlabs.js - Reservas storefront:
functions/api/storefront/[id]/reservations.js - Disponibilidad:
functions/api/storefront/[id]/availability.js - Config storefront:
src/config/storefront.config.ts - Cliente API:
src/lib/cadences.ts - Patrón RAG reusable:
storefronts/memora/src/lib/retrieval/ - Migración voice_calls original:
migrations/0002_add_whatsapp_voice_tables.sql - Migración incoming_calls:
migrations/0017_add_incoming_calls_table.sql - Specs Restaurant23:
RESTAURANT23_SPECS.md - Estado producción:
TODO_PRODUCCION.md
12. TL;DR
- Sí, lo metemos como módulo premium de Restaurant23.
- Sirve dos perspectivas con el mismo motor: restaurante físico (reserva) y tienda online de restauración / catering (vende, cobra, reparte).
- Infra ya existe al 90%; lo que falta es producto: configuración por tenant, tools de agente, RAG sobre carta, normalización de schema, dashboard de llamadas.
- Roadmap en 4 fases empezando por piloto reservas → pedidos → catering → multi‑local/outbound.
- Modelo de negocio claro con margen sano desde el plan lite.