Assistente IA via WhatsApp
O PopinaFlow integra o assistente de IA ao canal WhatsApp, permitindo que admins e staff gerenciem o restaurante pelo WhatsApp do restaurante. Clientes também recebem um bot de pedidos conversacional por IA, substituindo o antigo bot de regras.
Arquitetura
Mensagem recebida → POST /webhooks/whatsapp/:tenantId
↓
Validação HMAC-SHA256 (por-tenant secret)
↓
findVerifiedStaffByPhone(tenantId, phone)
├── Encontrado (admin/staff verificado) → WhatsAppAdminAiService
└── Não encontrado (cliente) → WhatsAppCustomerAiService
↓ ↓
AssistantService.chatSync() AssistantService.chatSync()
(prompt de gestão, todas as tools) (prompt de pedidos, tools limitadas)
↓ ↓
WhatsAppService.sendTextMessageFromInstance()O método AssistantService.chatSync() é a versão não-streaming do chat() existente — mesma lógica de contexto, ferramentas e histórico, mas retorna string completa em vez de SSE chunks.
Verificação de Telefone (OTP)
Antes de usar o assistente, admin/staff precisam vincular o número pessoal de WhatsApp à conta.
Endpoints
POST /t/:slug/auth/profile/phone/send-otp
Guard: JwtAuthGuard + TenantGuard
Body: { phone: string }
→ Gera OTP 6 dígitos, armazena no Redis com TTL 600s
→ Envia mensagem WhatsApp via instância do tenant
→ Rate-limit: 1 envio/min por usuário (Redis TTL 60s)
POST /t/:slug/auth/profile/phone/confirm-otp
Guard: JwtAuthGuard + TenantGuard
Body: { code: string }
→ Valida OTP (máx 3 tentativas)
→ Marca User.phoneVerified = true
→ Remove chave RedisRedis keys
| Chave | TTL | Conteúdo |
|---|---|---|
phone-otp:{userId} | 600s | { code, phone, attempts } |
phone-otp-rl:{userId} | 60s | "1" (rate-limit flag) |
Schema
// user.schema.ts
@Prop({ default: false })
phoneVerified: boolean;Serviço Admin/Staff — WhatsAppAdminAiService
Arquivo: backend/src/assistant/whatsapp-admin-assistant.service.ts
Session ID
whatsapp-admin-{tenantId}-{userId}Sessão persistente — sem expiração. Comandos especiais:
| Comando | Ação |
|---|---|
/limpar /clear | deleteSession(tenantId, sessionId) → nova conversa |
/ajuda /help | Resposta fixa com lista de capacidades |
/status | Chama buildContext(tenantId), extrai e formata os dados do dia |
Contexto carregado
- Últimas 30 mensagens da sessão (
getSession(tenantId, sessionId)) - System prompt:
buildContext(tenantId)(mesmo do browser — pedidos, estoque, reservas, mesas) - Role:
isAdmin = user.role in ['admin', 'superadmin'] - Ferramentas: todas as 10 ferramentas para admin; subset para staff (mesmo comportamento do browser)
Serviço Cliente — WhatsAppCustomerAiService
Arquivo: backend/src/assistant/whatsapp-customer-assistant.service.ts
Session ID
whatsapp-customer-{tenantId}-{phone}Sessão expira naturalmente (sem TTL ativo — historicamente o bot usava 2h via MongoDB TTL no WhatsAppConversation).
Contexto carregado
- Últimas 20 mensagens da sessão
- System prompt customizado (cliente): cardápio completo, nome do restaurante, regras de uso
user = null(anônimo)isAdmin = false
Ferramentas disponíveis (cliente)
Somente create_order e confirm_reservation. O AssistantService.chatSync() chama getToolsForRole(false) — qualquer ferramenta destrutiva ou de gestão é bloqueada automaticamente.
AssistantService.chatSync()
Arquivo: backend/src/assistant/assistant.service.ts
async chatSync(
tenantId: string,
userId: string | null,
isAdmin: boolean,
messages: ChatMessage[],
sessionId: string,
systemPromptOverride?: string,
): Promise<{ text: string; toolEvents: Record<string, any>[] }>Diferenças em relação a chat():
chat() (SSE) | chatSync() (WhatsApp) |
|---|---|
Phase 2: stream: true → SSE chunks | Phase 2: stream: false → string completa |
Retorna AsyncGenerator<string> | Retorna Promise<{ text, toolEvents }> |
userId obrigatório | userId pode ser null (cliente anônimo) |
systemPromptOverride não existe | systemPromptOverride permite prompt de cliente |
O histórico é salvo via AssistantHistoryService.saveMessage() — mesmo schema ConversationMessage.
Utilitários — whatsapp-ai.utils.ts
Arquivo: backend/src/assistant/whatsapp-ai.utils.ts
markdownToWhatsApp(text: string): string
// **bold** → *bold*, *italic* → _italic_, # Header → *Header*, - → •
splitForWhatsApp(text: string, maxChars = 4000): string[]
// Divide em chunks ≤ 4000 chars nos \n\n mais próximos
sleep(ms: number): Promise<void>
// Delay entre chunks sequenciais (600ms padrão)Roteamento do Webhook
Arquivo: backend/src/whatsapp-ordering/whatsapp-ordering.controller.ts
O antigo WhatsAppOrderingService (bot de regras) foi removido do fluxo principal. O controller agora:
- Valida HMAC-SHA256 usando
tenant.whatsappWebhookSecret - Normaliza o telefone (E.164: adiciona
55se ausente) - Busca
UserTenantondetenant = tenantIderole in [admin, staff], filtra poruser.phone = phone AND user.phoneVerified = true - Se encontrado →
WhatsAppAdminAiService.handle()(fire-and-forget) - Se não encontrado →
WhatsAppCustomerAiService.handle()(fire-and-forget) - Retorna 200 imediatamente
Módulos modificados
| Módulo | Mudança |
|---|---|
AssistantModule | Exporta AssistantService e AssistantHistoryService |
WhatsAppOrderingModule | Importa AssistantModule + UsersModule; registra MenuItem schema; providers: WhatsAppAdminAiService, WhatsAppCustomerAiService |
UsersModule | Importa RedisModule (para OTP) |
AuthModule | Injeta WhatsAppService e TenantsService no TenantAuthController |
Referência de arquivos
backend/src/
assistant/
assistant.service.ts → chatSync() adicionado
assistant.module.ts → exports expandidos
whatsapp-admin-assistant.service.ts → handler admin/staff (NOVO)
whatsapp-customer-assistant.service.ts → handler cliente (NOVO)
whatsapp-ai.utils.ts → formatação e split (NOVO)
auth/
tenant-auth.controller.ts → endpoints OTP
users/
schemas/user.schema.ts → campo phoneVerified
users.service.ts → generatePhoneOtp, confirmPhoneOtp, findVerifiedStaffByPhone
users.module.ts → importa RedisModule
dto/verify-phone.dto.ts → SendOtpDto, ConfirmOtpDto (NOVO)
notifications/
whatsapp.service.ts → sendTextMessageFromInstance()
whatsapp-ordering/
whatsapp-ordering.controller.ts → roteamento admin vs cliente
whatsapp-ordering.module.ts → módulos e providers atualizados
frontend-react/src/
types/index.ts → User.phoneVerified
views/admin/AdminProfileView.tsx → WhatsAppVerificationCard