elevenlabs-webhook-nodejs 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +27 -0
- package/.dockerignore +6 -0
- package/.env.example +43 -0
- package/BEFORE-PRODUCTION.md +32 -0
- package/Dockerfile +23 -0
- package/SYSTEM_OVERVIEW.md +942 -0
- package/SYSTEM_STATUS.md +332 -0
- package/backup_script/main.py +146 -0
- package/baileys/.dockerignore +7 -0
- package/baileys/Dockerfile +13 -0
- package/baileys/README.md +412 -0
- package/baileys/index.js +499 -0
- package/baileys/package-lock.json +2532 -0
- package/baileys/package.json +25 -0
- package/baileys/server.js +96 -0
- package/baileys/src/config.js +55 -0
- package/baileys/src/middleware/api-key.js +16 -0
- package/baileys/src/routes/accounts.js +51 -0
- package/baileys/src/routes/chats.js +103 -0
- package/baileys/src/routes/webhooks.js +34 -0
- package/baileys/src/services/account-store.js +259 -0
- package/baileys/src/services/webhook-dispatcher.js +161 -0
- package/baileys/src/services/worker-manager.js +597 -0
- package/baileys/src/utils/jid.js +79 -0
- package/baileys/src/utils/logger.js +16 -0
- package/baileys/src/utils/use-mongodb-auth-state.js +122 -0
- package/baileys/worker.js +721 -0
- package/dist/app.d.ts +1 -0
- package/dist/app.js +84 -0
- package/dist/app.js.map +1 -0
- package/dist/config/agentPrompts.json +19 -0
- package/dist/config/database.d.ts +1 -0
- package/dist/config/database.js +26 -0
- package/dist/config/database.js.map +1 -0
- package/dist/config/env.d.ts +41 -0
- package/dist/config/env.js +81 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.js +73 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controllers/webhook.controller.d.ts +10 -0
- package/dist/controllers/webhook.controller.js +128 -0
- package/dist/controllers/webhook.controller.js.map +1 -0
- package/dist/errors/index.d.ts +4 -0
- package/dist/errors/index.js +13 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/hooks/webhookValidator.d.ts +2 -0
- package/dist/hooks/webhookValidator.js +47 -0
- package/dist/hooks/webhookValidator.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/001_session-architecture.d.ts +3 -0
- package/dist/migrations/001_session-architecture.js +119 -0
- package/dist/migrations/001_session-architecture.js.map +1 -0
- package/dist/migrations/002_agent-ref.d.ts +3 -0
- package/dist/migrations/002_agent-ref.js +55 -0
- package/dist/migrations/002_agent-ref.js.map +1 -0
- package/dist/migrations/003_shift-schedule.d.ts +3 -0
- package/dist/migrations/003_shift-schedule.js +48 -0
- package/dist/migrations/003_shift-schedule.js.map +1 -0
- package/dist/migrations/004_invert-shift-to-clinic-hours.d.ts +3 -0
- package/dist/migrations/004_invert-shift-to-clinic-hours.js +27 -0
- package/dist/migrations/004_invert-shift-to-clinic-hours.js.map +1 -0
- package/dist/migrations/005_composite-baileys-chat-ids.d.ts +3 -0
- package/dist/migrations/005_composite-baileys-chat-ids.js +47 -0
- package/dist/migrations/005_composite-baileys-chat-ids.js.map +1 -0
- package/dist/migrations/migration.model.d.ts +18 -0
- package/dist/migrations/migration.model.js +45 -0
- package/dist/migrations/migration.model.js.map +1 -0
- package/dist/migrations/migration.routes.d.ts +2 -0
- package/dist/migrations/migration.routes.js +37 -0
- package/dist/migrations/migration.routes.js.map +1 -0
- package/dist/migrations/migration.runner.d.ts +19 -0
- package/dist/migrations/migration.runner.js +74 -0
- package/dist/migrations/migration.runner.js.map +1 -0
- package/dist/models/ConversationClaim.d.ts +13 -0
- package/dist/models/ConversationClaim.js +44 -0
- package/dist/models/ConversationClaim.js.map +1 -0
- package/dist/models/ConversationEvaluation.d.ts +23 -0
- package/dist/models/ConversationEvaluation.js +92 -0
- package/dist/models/ConversationEvaluation.js.map +1 -0
- package/dist/models/Summary.d.ts +37 -0
- package/dist/models/Summary.js +78 -0
- package/dist/models/Summary.js.map +1 -0
- package/dist/models/Transcription.d.ts +34 -0
- package/dist/models/Transcription.js +71 -0
- package/dist/models/Transcription.js.map +1 -0
- package/dist/models/WhatsAppRecipient.d.ts +23 -0
- package/dist/models/WhatsAppRecipient.js +53 -0
- package/dist/models/WhatsAppRecipient.js.map +1 -0
- package/dist/modules/admin/admin.routes.d.ts +2 -0
- package/dist/modules/admin/admin.routes.js +1966 -0
- package/dist/modules/admin/admin.routes.js.map +1 -0
- package/dist/modules/admin/elevenlabs-test-chat.routes.d.ts +2 -0
- package/dist/modules/admin/elevenlabs-test-chat.routes.js +158 -0
- package/dist/modules/admin/elevenlabs-test-chat.routes.js.map +1 -0
- package/dist/modules/admin/whatsapp-test-chat.routes.d.ts +2 -0
- package/dist/modules/admin/whatsapp-test-chat.routes.js +204 -0
- package/dist/modules/admin/whatsapp-test-chat.routes.js.map +1 -0
- package/dist/modules/agents/agent.model.d.ts +38 -0
- package/dist/modules/agents/agent.model.js +92 -0
- package/dist/modules/agents/agent.model.js.map +1 -0
- package/dist/modules/agents/agent.routes.d.ts +2 -0
- package/dist/modules/agents/agent.routes.js +61 -0
- package/dist/modules/agents/agent.routes.js.map +1 -0
- package/dist/modules/appointment-validation/appointment-validation.model.d.ts +76 -0
- package/dist/modules/appointment-validation/appointment-validation.model.js +118 -0
- package/dist/modules/appointment-validation/appointment-validation.model.js.map +1 -0
- package/dist/modules/appointment-validation/appointment-validation.routes.d.ts +2 -0
- package/dist/modules/appointment-validation/appointment-validation.routes.js +202 -0
- package/dist/modules/appointment-validation/appointment-validation.routes.js.map +1 -0
- package/dist/modules/appointment-validation/appointment-validation.service.d.ts +53 -0
- package/dist/modules/appointment-validation/appointment-validation.service.js +827 -0
- package/dist/modules/appointment-validation/appointment-validation.service.js.map +1 -0
- package/dist/modules/auth/auth.model.d.ts +17 -0
- package/dist/modules/auth/auth.model.js +64 -0
- package/dist/modules/auth/auth.model.js.map +1 -0
- package/dist/modules/auth/auth.routes.d.ts +2 -0
- package/dist/modules/auth/auth.routes.js +202 -0
- package/dist/modules/auth/auth.routes.js.map +1 -0
- package/dist/modules/auth/auth.service.d.ts +28 -0
- package/dist/modules/auth/auth.service.js +183 -0
- package/dist/modules/auth/auth.service.js.map +1 -0
- package/dist/modules/auth/refresh-token.model.d.ts +13 -0
- package/dist/modules/auth/refresh-token.model.js +52 -0
- package/dist/modules/auth/refresh-token.model.js.map +1 -0
- package/dist/modules/billing/billing-alert.model.d.ts +16 -0
- package/dist/modules/billing/billing-alert.model.js +47 -0
- package/dist/modules/billing/billing-alert.model.js.map +1 -0
- package/dist/modules/billing/billing-period-snapshot.model.d.ts +35 -0
- package/dist/modules/billing/billing-period-snapshot.model.js +68 -0
- package/dist/modules/billing/billing-period-snapshot.model.js.map +1 -0
- package/dist/modules/billing/billing.model.d.ts +18 -0
- package/dist/modules/billing/billing.model.js +62 -0
- package/dist/modules/billing/billing.model.js.map +1 -0
- package/dist/modules/billing/billing.routes.d.ts +2 -0
- package/dist/modules/billing/billing.routes.js +63 -0
- package/dist/modules/billing/billing.routes.js.map +1 -0
- package/dist/modules/billing/billing.service.d.ts +69 -0
- package/dist/modules/billing/billing.service.js +498 -0
- package/dist/modules/billing/billing.service.js.map +1 -0
- package/dist/modules/billing/payment.model.d.ts +24 -0
- package/dist/modules/billing/payment.model.js +57 -0
- package/dist/modules/billing/payment.model.js.map +1 -0
- package/dist/modules/calls/call.model.d.ts +41 -0
- package/dist/modules/calls/call.model.js +97 -0
- package/dist/modules/calls/call.model.js.map +1 -0
- package/dist/modules/calls/call.routes.d.ts +2 -0
- package/dist/modules/calls/call.routes.js +103 -0
- package/dist/modules/calls/call.routes.js.map +1 -0
- package/dist/modules/campaigns/campaign.model.d.ts +45 -0
- package/dist/modules/campaigns/campaign.model.js +98 -0
- package/dist/modules/campaigns/campaign.model.js.map +1 -0
- package/dist/modules/campaigns/campaign.routes.d.ts +2 -0
- package/dist/modules/campaigns/campaign.routes.js +323 -0
- package/dist/modules/campaigns/campaign.routes.js.map +1 -0
- package/dist/modules/campaigns/campaign.service.d.ts +11 -0
- package/dist/modules/campaigns/campaign.service.js +86 -0
- package/dist/modules/campaigns/campaign.service.js.map +1 -0
- package/dist/modules/google-calendar/google-calendar.routes.d.ts +2 -0
- package/dist/modules/google-calendar/google-calendar.routes.js +32 -0
- package/dist/modules/google-calendar/google-calendar.routes.js.map +1 -0
- package/dist/modules/inbound-call/inbound-call-config.model.d.ts +20 -0
- package/dist/modules/inbound-call/inbound-call-config.model.js +68 -0
- package/dist/modules/inbound-call/inbound-call-config.model.js.map +1 -0
- package/dist/modules/inbound-call/inbound-call.routes.d.ts +2 -0
- package/dist/modules/inbound-call/inbound-call.routes.js +243 -0
- package/dist/modules/inbound-call/inbound-call.routes.js.map +1 -0
- package/dist/modules/leads/lead.model.d.ts +24 -0
- package/dist/modules/leads/lead.model.js +54 -0
- package/dist/modules/leads/lead.model.js.map +1 -0
- package/dist/modules/leads/lead.routes.d.ts +2 -0
- package/dist/modules/leads/lead.routes.js +201 -0
- package/dist/modules/leads/lead.routes.js.map +1 -0
- package/dist/modules/plans/plan.model.d.ts +25 -0
- package/dist/modules/plans/plan.model.js +59 -0
- package/dist/modules/plans/plan.model.js.map +1 -0
- package/dist/modules/surveys/survey.model.d.ts +30 -0
- package/dist/modules/surveys/survey.model.js +75 -0
- package/dist/modules/surveys/survey.model.js.map +1 -0
- package/dist/modules/surveys/survey.routes.d.ts +2 -0
- package/dist/modules/surveys/survey.routes.js +153 -0
- package/dist/modules/surveys/survey.routes.js.map +1 -0
- package/dist/modules/tenants/tenant.model.d.ts +86 -0
- package/dist/modules/tenants/tenant.model.js +127 -0
- package/dist/modules/tenants/tenant.model.js.map +1 -0
- package/dist/modules/tenants/tenant.routes.d.ts +2 -0
- package/dist/modules/tenants/tenant.routes.js +65 -0
- package/dist/modules/tenants/tenant.routes.js.map +1 -0
- package/dist/modules/users/user.routes.d.ts +2 -0
- package/dist/modules/users/user.routes.js +106 -0
- package/dist/modules/users/user.routes.js.map +1 -0
- package/dist/modules/webhooks/elevenlabs-tool.routes.d.ts +20 -0
- package/dist/modules/webhooks/elevenlabs-tool.routes.js +85 -0
- package/dist/modules/webhooks/elevenlabs-tool.routes.js.map +1 -0
- package/dist/modules/webhooks/elevenlabs-tool.service.d.ts +11 -0
- package/dist/modules/webhooks/elevenlabs-tool.service.js +360 -0
- package/dist/modules/webhooks/elevenlabs-tool.service.js.map +1 -0
- package/dist/modules/webhooks/elevenlabs.routes.d.ts +2 -0
- package/dist/modules/webhooks/elevenlabs.routes.js +34 -0
- package/dist/modules/webhooks/elevenlabs.routes.js.map +1 -0
- package/dist/modules/webhooks/elevenlabs.service.d.ts +6 -0
- package/dist/modules/webhooks/elevenlabs.service.js +512 -0
- package/dist/modules/webhooks/elevenlabs.service.js.map +1 -0
- package/dist/modules/webhooks/unipile.routes.d.ts +2 -0
- package/dist/modules/webhooks/unipile.routes.js +780 -0
- package/dist/modules/webhooks/unipile.routes.js.map +1 -0
- package/dist/modules/whatsapp/appointment.model.d.ts +27 -0
- package/dist/modules/whatsapp/appointment.model.js +58 -0
- package/dist/modules/whatsapp/appointment.model.js.map +1 -0
- package/dist/modules/whatsapp/operator-request.model.d.ts +29 -0
- package/dist/modules/whatsapp/operator-request.model.js +65 -0
- package/dist/modules/whatsapp/operator-request.model.js.map +1 -0
- package/dist/modules/whatsapp/stt-usage.model.d.ts +16 -0
- package/dist/modules/whatsapp/stt-usage.model.js +53 -0
- package/dist/modules/whatsapp/stt-usage.model.js.map +1 -0
- package/dist/modules/whatsapp/whatsapp-chat.model.d.ts +23 -0
- package/dist/modules/whatsapp/whatsapp-chat.model.js +55 -0
- package/dist/modules/whatsapp/whatsapp-chat.model.js.map +1 -0
- package/dist/modules/whatsapp/whatsapp-contact-profile.model.d.ts +23 -0
- package/dist/modules/whatsapp/whatsapp-contact-profile.model.js +54 -0
- package/dist/modules/whatsapp/whatsapp-contact-profile.model.js.map +1 -0
- package/dist/modules/whatsapp/whatsapp-message.model.d.ts +30 -0
- package/dist/modules/whatsapp/whatsapp-message.model.js +52 -0
- package/dist/modules/whatsapp/whatsapp-message.model.js.map +1 -0
- package/dist/modules/whatsapp/whatsapp-session.model.d.ts +33 -0
- package/dist/modules/whatsapp/whatsapp-session.model.js +65 -0
- package/dist/modules/whatsapp/whatsapp-session.model.js.map +1 -0
- package/dist/modules/whatsapp/whatsapp.routes.d.ts +2 -0
- package/dist/modules/whatsapp/whatsapp.routes.js +1237 -0
- package/dist/modules/whatsapp/whatsapp.routes.js.map +1 -0
- package/dist/plugins/cors.d.ts +3 -0
- package/dist/plugins/cors.js +11 -0
- package/dist/plugins/cors.js.map +1 -0
- package/dist/plugins/jwt.d.ts +3 -0
- package/dist/plugins/jwt.js +22 -0
- package/dist/plugins/jwt.js.map +1 -0
- package/dist/plugins/rawBody.d.ts +3 -0
- package/dist/plugins/rawBody.js +16 -0
- package/dist/plugins/rawBody.js.map +1 -0
- package/dist/plugins/rbac.d.ts +5 -0
- package/dist/plugins/rbac.js +29 -0
- package/dist/plugins/rbac.js.map +1 -0
- package/dist/routes/admin.routes.d.ts +2 -0
- package/dist/routes/admin.routes.js +169 -0
- package/dist/routes/admin.routes.js.map +1 -0
- package/dist/routes/health.routes.d.ts +2 -0
- package/dist/routes/health.routes.js +16 -0
- package/dist/routes/health.routes.js.map +1 -0
- package/dist/routes/webhook.routes.d.ts +2 -0
- package/dist/routes/webhook.routes.js +17 -0
- package/dist/routes/webhook.routes.js.map +1 -0
- package/dist/services/ai/base.ai.d.ts +19 -0
- package/dist/services/ai/base.ai.js +120 -0
- package/dist/services/ai/base.ai.js.map +1 -0
- package/dist/services/ai/gemini.service.d.ts +11 -0
- package/dist/services/ai/gemini.service.js +43 -0
- package/dist/services/ai/gemini.service.js.map +1 -0
- package/dist/services/ai/index.d.ts +2 -0
- package/dist/services/ai/index.js +26 -0
- package/dist/services/ai/index.js.map +1 -0
- package/dist/services/ai/openai.service.d.ts +11 -0
- package/dist/services/ai/openai.service.js +50 -0
- package/dist/services/ai/openai.service.js.map +1 -0
- package/dist/services/elevenlabs.service.d.ts +52 -0
- package/dist/services/elevenlabs.service.js +447 -0
- package/dist/services/elevenlabs.service.js.map +1 -0
- package/dist/services/google-calendar.service.d.ts +60 -0
- package/dist/services/google-calendar.service.js +494 -0
- package/dist/services/google-calendar.service.js.map +1 -0
- package/dist/services/inbound-call-schedule.service.d.ts +11 -0
- package/dist/services/inbound-call-schedule.service.js +162 -0
- package/dist/services/inbound-call-schedule.service.js.map +1 -0
- package/dist/services/netgsm.service.d.ts +41 -0
- package/dist/services/netgsm.service.js +89 -0
- package/dist/services/netgsm.service.js.map +1 -0
- package/dist/services/unipile.service.d.ts +41 -0
- package/dist/services/unipile.service.js +149 -0
- package/dist/services/unipile.service.js.map +1 -0
- package/dist/services/whatsapp-agent.service.d.ts +139 -0
- package/dist/services/whatsapp-agent.service.js +2055 -0
- package/dist/services/whatsapp-agent.service.js.map +1 -0
- package/dist/services/whatsapp.service.d.ts +26 -0
- package/dist/services/whatsapp.service.js +206 -0
- package/dist/services/whatsapp.service.js.map +1 -0
- package/dist/templates/index.d.ts +39 -0
- package/dist/templates/index.js +35 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/receptionist.d.ts +2 -0
- package/dist/templates/receptionist.js +39 -0
- package/dist/templates/receptionist.js.map +1 -0
- package/dist/templates/survey.d.ts +2 -0
- package/dist/templates/survey.js +41 -0
- package/dist/templates/survey.js.map +1 -0
- package/dist/types/index.d.ts +173 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.js +105 -0
- package/dist/utils/logger.js.map +1 -0
- package/docker-compose.nestjs.yml +89 -0
- package/docker-compose.yml +78 -0
- package/docs/AI_AGENT_ENHANCEMENT_PLAN.md +164 -0
- package/docs/API.md +1193 -0
- package/docs/API_ENDPOINTS.md +344 -0
- package/docs/ARCHITECTURE.md +305 -0
- package/docs/AUTH_API.md +252 -0
- package/docs/BILLING_SMS_ALERTS.md +94 -0
- package/docs/CHAT_ASSIGNMENT_SYSTEM.md +118 -0
- package/docs/CLIENT_TOOLS_AND_FEATURES.md +337 -0
- package/docs/ELEVENLABS_WEBHOOK_TOOLS.md +644 -0
- package/docs/FRONTEND_CHECKLIST.md +227 -0
- package/docs/IMPLEMENTATION_STATUS.md +470 -0
- package/docs/MIGRATION_GUIDE.md +96 -0
- package/docs/MISSINGS_REPORT.md +507 -0
- package/docs/NESTJS_MIGRATION_REFERENCE.md +5136 -0
- package/docs/PROJECT_DESCRIPTION.md +1038 -0
- package/docs/SCALING.md +148 -0
- package/docs/SESSION_SUMMARY_2026_03_17.md +135 -0
- package/docs/WHATSAPP_AGENT.md +1086 -0
- package/docs/architecture/00-SYSTEM-OVERVIEW.md +318 -0
- package/docs/architecture/01-DATABASE-SCHEMA.md +2564 -0
- package/docs/architecture/02-AUTHENTICATION.md +1040 -0
- package/docs/architecture/03-MULTI-CLINIC.md +742 -0
- package/docs/architecture/04-WHATSAPP-AGENT.md +608 -0
- package/docs/architecture/05-OPERATOR-WORKFLOW.md +444 -0
- package/docs/architecture/06-BAILEYS-MICROSERVICE.md +616 -0
- package/docs/architecture/07-APPOINTMENTS.md +849 -0
- package/docs/architecture/08-VOICE-CALLS.md +470 -0
- package/docs/architecture/09-OUTBOUND-CAMPAIGNS.md +542 -0
- package/docs/architecture/10-CLIENT-TOOLS.md +665 -0
- package/docs/architecture/11-BILLING.md +458 -0
- package/docs/architecture/12-SECURITY.md +216 -0
- package/docs/architecture/13-LOGGING-AUDIT.md +549 -0
- package/docs/architecture/14-META-BUSINESS-API.md +454 -0
- package/docs/architecture/15-AI-MODULE.md +479 -0
- package/docs/architecture/16-BACKGROUND-JOBS.md +469 -0
- package/docs/architecture/17-REALTIME-LIVECHAT.md +447 -0
- package/docs/architecture/18-FILE-STORAGE.md +410 -0
- package/docs/architecture/19-PATIENTS.md +1034 -0
- package/docs/architecture/20-TREATMENTS-AND-PLANS.md +774 -0
- package/docs/architecture/21-BEFORE-AFTER-PHOTOS.md +519 -0
- package/docs/database.md +456 -0
- package/docs/ornek-randevu-onay.csv +3 -0
- package/ecosystem.config.js +16 -0
- package/elevenlabs-convai-api-reference.md +1171 -0
- package/frontend/.dockerignore +2 -0
- package/frontend/Dockerfile +24 -0
- package/frontend/README.md +75 -0
- package/frontend/components.json +25 -0
- package/frontend/eslint.config.js +23 -0
- package/frontend/index.html +13 -0
- package/frontend/nginx.conf +37 -0
- package/frontend/package-lock.json +8709 -0
- package/frontend/package.json +71 -0
- package/frontend/public/favicon.svg +1 -0
- package/frontend/public/icons.svg +24 -0
- package/frontend/src/App.tsx +125 -0
- package/frontend/src/components/error-boundary.tsx +50 -0
- package/frontend/src/components/shared/activity-timeline.tsx +66 -0
- package/frontend/src/components/shared/appointment-calendar.css +80 -0
- package/frontend/src/components/shared/appointment-calendar.tsx +245 -0
- package/frontend/src/components/shared/chat-bubble.tsx +72 -0
- package/frontend/src/components/shared/combobox.tsx +119 -0
- package/frontend/src/components/shared/confirm-dialog.tsx +57 -0
- package/frontend/src/components/shared/data-table-pagination.tsx +97 -0
- package/frontend/src/components/shared/data-table-toolbar.tsx +39 -0
- package/frontend/src/components/shared/empty-state.tsx +27 -0
- package/frontend/src/components/shared/file-upload.tsx +140 -0
- package/frontend/src/components/shared/index.ts +20 -0
- package/frontend/src/components/shared/language-switcher.tsx +29 -0
- package/frontend/src/components/shared/notification-dropdown.tsx +115 -0
- package/frontend/src/components/shared/page-header.tsx +23 -0
- package/frontend/src/components/shared/stat-card.tsx +37 -0
- package/frontend/src/components/shared/status-badge.tsx +70 -0
- package/frontend/src/components/shared/status-dot.tsx +43 -0
- package/frontend/src/components/ui/alert-dialog.tsx +187 -0
- package/frontend/src/components/ui/alert.tsx +76 -0
- package/frontend/src/components/ui/avatar.tsx +109 -0
- package/frontend/src/components/ui/badge.tsx +52 -0
- package/frontend/src/components/ui/breadcrumb.tsx +125 -0
- package/frontend/src/components/ui/button.tsx +60 -0
- package/frontend/src/components/ui/calendar.tsx +219 -0
- package/frontend/src/components/ui/card.tsx +103 -0
- package/frontend/src/components/ui/chart.tsx +371 -0
- package/frontend/src/components/ui/checkbox.tsx +29 -0
- package/frontend/src/components/ui/collapsible.tsx +19 -0
- package/frontend/src/components/ui/command.tsx +194 -0
- package/frontend/src/components/ui/dialog.tsx +158 -0
- package/frontend/src/components/ui/dropdown-menu.tsx +268 -0
- package/frontend/src/components/ui/input-group.tsx +156 -0
- package/frontend/src/components/ui/input.tsx +20 -0
- package/frontend/src/components/ui/label.tsx +20 -0
- package/frontend/src/components/ui/pagination.tsx +130 -0
- package/frontend/src/components/ui/popover.tsx +90 -0
- package/frontend/src/components/ui/progress.tsx +83 -0
- package/frontend/src/components/ui/radio-group.tsx +36 -0
- package/frontend/src/components/ui/scroll-area.tsx +54 -0
- package/frontend/src/components/ui/select.tsx +199 -0
- package/frontend/src/components/ui/separator.tsx +23 -0
- package/frontend/src/components/ui/sheet.tsx +138 -0
- package/frontend/src/components/ui/sidebar.tsx +723 -0
- package/frontend/src/components/ui/skeleton.tsx +13 -0
- package/frontend/src/components/ui/sonner.tsx +47 -0
- package/frontend/src/components/ui/switch.tsx +30 -0
- package/frontend/src/components/ui/table.tsx +114 -0
- package/frontend/src/components/ui/tabs.tsx +82 -0
- package/frontend/src/components/ui/textarea.tsx +18 -0
- package/frontend/src/components/ui/toggle-group.tsx +89 -0
- package/frontend/src/components/ui/toggle.tsx +43 -0
- package/frontend/src/components/ui/tooltip.tsx +64 -0
- package/frontend/src/hooks/use-mobile.ts +19 -0
- package/frontend/src/i18n/index.ts +20 -0
- package/frontend/src/i18n/locales/en.json +786 -0
- package/frontend/src/i18n/locales/tr.json +786 -0
- package/frontend/src/index.css +134 -0
- package/frontend/src/layouts/admin-layout.tsx +111 -0
- package/frontend/src/layouts/app-header.tsx +130 -0
- package/frontend/src/layouts/app-layout.tsx +19 -0
- package/frontend/src/layouts/app-sidebar.tsx +212 -0
- package/frontend/src/layouts/auth-guard.tsx +31 -0
- package/frontend/src/layouts/mobile-sidebar.tsx +120 -0
- package/frontend/src/lib/api.ts +68 -0
- package/frontend/src/lib/hooks/use-appointments.ts +224 -0
- package/frontend/src/lib/hooks/use-availability-blocks.ts +83 -0
- package/frontend/src/lib/hooks/use-chats.ts +236 -0
- package/frontend/src/lib/hooks/use-clinic-detail.ts +171 -0
- package/frontend/src/lib/hooks/use-clinics.ts +37 -0
- package/frontend/src/lib/hooks/use-doctor-calendar.ts +80 -0
- package/frontend/src/lib/hooks/use-examinations.ts +89 -0
- package/frontend/src/lib/hooks/use-medical-history.ts +52 -0
- package/frontend/src/lib/hooks/use-patients.ts +296 -0
- package/frontend/src/lib/hooks/use-photo-sets.ts +118 -0
- package/frontend/src/lib/hooks/use-treatment-plans.ts +152 -0
- package/frontend/src/lib/hooks/use-treatments.ts +125 -0
- package/frontend/src/lib/hooks/use-users.ts +172 -0
- package/frontend/src/lib/utils.ts +6 -0
- package/frontend/src/main.tsx +17 -0
- package/frontend/src/pages/admin/agents/detail.tsx +774 -0
- package/frontend/src/pages/admin/agents/import.tsx +280 -0
- package/frontend/src/pages/admin/agents/index.tsx +245 -0
- package/frontend/src/pages/admin/ai-playground.tsx +543 -0
- package/frontend/src/pages/appointments/create-appointment-dialog.tsx +390 -0
- package/frontend/src/pages/appointments/index.tsx +860 -0
- package/frontend/src/pages/audit/index.tsx +24 -0
- package/frontend/src/pages/auth/login.tsx +194 -0
- package/frontend/src/pages/billing/index.tsx +24 -0
- package/frontend/src/pages/calendar/index.tsx +704 -0
- package/frontend/src/pages/calendar-connections/index.tsx +295 -0
- package/frontend/src/pages/calls/index.tsx +24 -0
- package/frontend/src/pages/campaigns/index.tsx +24 -0
- package/frontend/src/pages/chats/index.tsx +981 -0
- package/frontend/src/pages/clinics/index.tsx +224 -0
- package/frontend/src/pages/clinics/settings.tsx +412 -0
- package/frontend/src/pages/components-showcase.tsx +773 -0
- package/frontend/src/pages/connections/index.tsx +328 -0
- package/frontend/src/pages/dashboard.tsx +50 -0
- package/frontend/src/pages/leads/index.tsx +24 -0
- package/frontend/src/pages/my-schedule/index.tsx +496 -0
- package/frontend/src/pages/patients/create-patient-dialog.tsx +358 -0
- package/frontend/src/pages/patients/detail.tsx +1195 -0
- package/frontend/src/pages/patients/edit-patient-dialog.tsx +387 -0
- package/frontend/src/pages/patients/examinations-tab.tsx +460 -0
- package/frontend/src/pages/patients/index.tsx +381 -0
- package/frontend/src/pages/patients/medical-history-dialog.tsx +207 -0
- package/frontend/src/pages/patients/photo-sets-tab.tsx +616 -0
- package/frontend/src/pages/patients/timeline-tab.tsx +164 -0
- package/frontend/src/pages/patients/treatment-plans-tab.tsx +598 -0
- package/frontend/src/pages/phone-numbers/detail.tsx +427 -0
- package/frontend/src/pages/phone-numbers/index.tsx +455 -0
- package/frontend/src/pages/platform/index.tsx +454 -0
- package/frontend/src/pages/settings/index.tsx +126 -0
- package/frontend/src/pages/treatments/index.tsx +487 -0
- package/frontend/src/pages/users/doctor-profile.tsx +672 -0
- package/frontend/src/pages/users/edit.tsx +329 -0
- package/frontend/src/pages/users/index.tsx +407 -0
- package/frontend/src/pages/validation/index.tsx +24 -0
- package/frontend/src/stores/auth.store.ts +108 -0
- package/frontend/src/stores/ui.store.ts +41 -0
- package/frontend/tsconfig.app.json +32 -0
- package/frontend/tsconfig.json +13 -0
- package/frontend/tsconfig.node.json +26 -0
- package/frontend/vite.config.ts +29 -0
- package/nestjs/.dockerignore +5 -0
- package/nestjs/.env.docker +64 -0
- package/nestjs/.prettierrc +4 -0
- package/nestjs/Dockerfile +36 -0
- package/nestjs/README.md +98 -0
- package/nestjs/eslint.config.mjs +35 -0
- package/nestjs/nest-cli.json +8 -0
- package/nestjs/package-lock.json +13390 -0
- package/nestjs/package.json +114 -0
- package/nestjs/prisma/migrations/20260409161536_add_message_metadata_fields/migration.sql +1746 -0
- package/nestjs/prisma/migrations/20260410140436_add_agent_ai_fields/migration.sql +36 -0
- package/nestjs/prisma/migrations/20260410175519_add_agent_clinic_assignments/migration.sql +21 -0
- package/nestjs/prisma/migrations/20260412094344_make_agent_tenant_optional/migration.sql +2 -0
- package/nestjs/prisma/migrations/20260412110008_add_admin_chat_sessions/migration.sql +47 -0
- package/nestjs/prisma/migrations/migration_lock.toml +3 -0
- package/nestjs/prisma/schema.prisma +1843 -0
- package/nestjs/prisma/seed.ts +375 -0
- package/nestjs/prisma.config.ts +14 -0
- package/nestjs/src/admin/admin.controller.ts +27 -0
- package/nestjs/src/admin/admin.module.ts +16 -0
- package/nestjs/src/admin/admin.service.ts +91 -0
- package/nestjs/src/admin/ai-chat-session.service.ts +454 -0
- package/nestjs/src/admin/ai-test.controller.ts +191 -0
- package/nestjs/src/admin/superadmin.controller.ts +106 -0
- package/nestjs/src/agents/agents.controller.ts +262 -0
- package/nestjs/src/agents/agents.module.ts +13 -0
- package/nestjs/src/agents/agents.service.ts +733 -0
- package/nestjs/src/agents/dto/create-agent.dto.ts +99 -0
- package/nestjs/src/agents/dto/index.ts +2 -0
- package/nestjs/src/agents/dto/update-agent.dto.ts +148 -0
- package/nestjs/src/app.module.ts +115 -0
- package/nestjs/src/appointment-validation/appointment-validation.controller.ts +194 -0
- package/nestjs/src/appointment-validation/appointment-validation.module.ts +16 -0
- package/nestjs/src/appointment-validation/appointment-validation.service.ts +450 -0
- package/nestjs/src/appointment-validation/dto/create-batch.dto.ts +105 -0
- package/nestjs/src/appointment-validation/dto/index.ts +1 -0
- package/nestjs/src/appointment-validation/processors/validation-dispatch.processor.ts +26 -0
- package/nestjs/src/appointment-validation/processors/validation-sync.processor.ts +23 -0
- package/nestjs/src/appointments/appointments.controller.ts +268 -0
- package/nestjs/src/appointments/appointments.module.ts +13 -0
- package/nestjs/src/appointments/appointments.service.ts +773 -0
- package/nestjs/src/appointments/dto/create-appointment.dto.ts +72 -0
- package/nestjs/src/appointments/dto/index.ts +4 -0
- package/nestjs/src/appointments/dto/query-appointments.dto.ts +60 -0
- package/nestjs/src/appointments/dto/update-appointment.dto.ts +43 -0
- package/nestjs/src/appointments/dto/update-status.dto.ts +18 -0
- package/nestjs/src/appointments/processors/reminder.processor.ts +243 -0
- package/nestjs/src/audit/audit.controller.ts +84 -0
- package/nestjs/src/audit/audit.decorator.ts +20 -0
- package/nestjs/src/audit/audit.interceptor.ts +67 -0
- package/nestjs/src/audit/audit.interfaces.ts +15 -0
- package/nestjs/src/audit/audit.module.ts +12 -0
- package/nestjs/src/audit/audit.service.ts +177 -0
- package/nestjs/src/auth/auth.controller.ts +116 -0
- package/nestjs/src/auth/auth.module.ts +25 -0
- package/nestjs/src/auth/auth.service.ts +612 -0
- package/nestjs/src/auth/dto/change-password.dto.ts +13 -0
- package/nestjs/src/auth/dto/forgot-password.dto.ts +8 -0
- package/nestjs/src/auth/dto/index.ts +6 -0
- package/nestjs/src/auth/dto/login.dto.ts +13 -0
- package/nestjs/src/auth/dto/refresh.dto.ts +8 -0
- package/nestjs/src/auth/dto/register.dto.ts +28 -0
- package/nestjs/src/auth/dto/reset-password.dto.ts +13 -0
- package/nestjs/src/auth/permissions.config.ts +85 -0
- package/nestjs/src/availability-blocks/availability-blocks.controller.ts +83 -0
- package/nestjs/src/availability-blocks/availability-blocks.module.ts +10 -0
- package/nestjs/src/availability-blocks/availability-blocks.service.ts +202 -0
- package/nestjs/src/billing/billing.controller.ts +104 -0
- package/nestjs/src/billing/billing.module.ts +12 -0
- package/nestjs/src/billing/billing.service.ts +398 -0
- package/nestjs/src/billing/dto/index.ts +2 -0
- package/nestjs/src/billing/dto/query-billing.dto.ts +29 -0
- package/nestjs/src/billing/dto/record-payment.dto.ts +60 -0
- package/nestjs/src/billing/processors/alerts.processor.ts +216 -0
- package/nestjs/src/billing/processors/snapshot.processor.ts +181 -0
- package/nestjs/src/calls/calls.controller.ts +92 -0
- package/nestjs/src/calls/calls.module.ts +10 -0
- package/nestjs/src/calls/calls.service.ts +359 -0
- package/nestjs/src/calls/dto/index.ts +1 -0
- package/nestjs/src/calls/dto/query-calls.dto.ts +46 -0
- package/nestjs/src/clinics/clinics.controller.ts +226 -0
- package/nestjs/src/clinics/clinics.module.ts +11 -0
- package/nestjs/src/clinics/clinics.service.ts +203 -0
- package/nestjs/src/clinics/dto/create-clinic.dto.ts +40 -0
- package/nestjs/src/clinics/dto/create-meta-connection.dto.ts +44 -0
- package/nestjs/src/clinics/dto/index.ts +4 -0
- package/nestjs/src/clinics/dto/update-clinic.dto.ts +133 -0
- package/nestjs/src/clinics/dto/update-meta-connection.dto.ts +22 -0
- package/nestjs/src/clinics/meta-connections.service.ts +210 -0
- package/nestjs/src/common/decorators/accessible-clinics.decorator.ts +9 -0
- package/nestjs/src/common/decorators/current-user.decorator.ts +10 -0
- package/nestjs/src/common/decorators/features.decorator.ts +7 -0
- package/nestjs/src/common/decorators/index.ts +4 -0
- package/nestjs/src/common/decorators/permissions.decorator.ts +13 -0
- package/nestjs/src/common/gateways/events.gateway.ts +157 -0
- package/nestjs/src/common/guards/clinic-access.guard.ts +40 -0
- package/nestjs/src/common/guards/features.guard.ts +38 -0
- package/nestjs/src/common/guards/index.ts +5 -0
- package/nestjs/src/common/guards/jwt-auth.guard.ts +54 -0
- package/nestjs/src/common/guards/permissions.guard.ts +50 -0
- package/nestjs/src/common/guards/superadmin.guard.ts +35 -0
- package/nestjs/src/common/interceptors/request-context.interceptor.ts +32 -0
- package/nestjs/src/common/interceptors/superadmin-tenant.interceptor.ts +42 -0
- package/nestjs/src/common/interfaces/jwt-payload.interface.ts +11 -0
- package/nestjs/src/config/config.module.ts +13 -0
- package/nestjs/src/database/database.module.ts +9 -0
- package/nestjs/src/database/prisma.service.ts +20 -0
- package/nestjs/src/database/tenant-context.ts +27 -0
- package/nestjs/src/google-calendar/google-calendar.controller.ts +259 -0
- package/nestjs/src/google-calendar/google-calendar.module.ts +10 -0
- package/nestjs/src/google-calendar/google-calendar.service.ts +811 -0
- package/nestjs/src/integrations/ai/ai.interface.ts +74 -0
- package/nestjs/src/integrations/ai/ai.module.ts +11 -0
- package/nestjs/src/integrations/ai/ai.service.ts +148 -0
- package/nestjs/src/integrations/ai/providers/anthropic.provider.ts +146 -0
- package/nestjs/src/integrations/ai/providers/openai.provider.ts +158 -0
- package/nestjs/src/integrations/elevenlabs/elevenlabs.module.ts +8 -0
- package/nestjs/src/integrations/elevenlabs/elevenlabs.service.ts +226 -0
- package/nestjs/src/integrations/encryption/encryption.module.ts +9 -0
- package/nestjs/src/integrations/encryption/encryption.service.ts +31 -0
- package/nestjs/src/integrations/image/image.module.ts +9 -0
- package/nestjs/src/integrations/image/image.service.ts +61 -0
- package/nestjs/src/integrations/meta-business/meta-business.module.ts +10 -0
- package/nestjs/src/integrations/meta-business/meta-instagram.service.ts +94 -0
- package/nestjs/src/integrations/meta-business/meta-webhook.service.ts +52 -0
- package/nestjs/src/integrations/meta-business/meta-whatsapp.service.ts +254 -0
- package/nestjs/src/integrations/minio/minio.module.ts +9 -0
- package/nestjs/src/integrations/minio/minio.service.ts +88 -0
- package/nestjs/src/integrations/netgsm/netgsm.module.ts +8 -0
- package/nestjs/src/integrations/netgsm/netgsm.service.ts +17 -0
- package/nestjs/src/integrations/tektippay/tektippay.module.ts +8 -0
- package/nestjs/src/integrations/tektippay/tektippay.service.ts +23 -0
- package/nestjs/src/leads/dto/index.ts +2 -0
- package/nestjs/src/leads/dto/query-leads.dto.ts +50 -0
- package/nestjs/src/leads/dto/update-lead.dto.ts +26 -0
- package/nestjs/src/leads/leads.controller.ts +184 -0
- package/nestjs/src/leads/leads.module.ts +10 -0
- package/nestjs/src/leads/leads.service.ts +375 -0
- package/nestjs/src/logging/logging.controller.ts +82 -0
- package/nestjs/src/logging/logging.module.ts +11 -0
- package/nestjs/src/logging/logging.service.ts +180 -0
- package/nestjs/src/main.ts +86 -0
- package/nestjs/src/outbound-campaigns/dto/create-campaign.dto.ts +47 -0
- package/nestjs/src/outbound-campaigns/dto/index.ts +3 -0
- package/nestjs/src/outbound-campaigns/dto/update-campaign.dto.ts +31 -0
- package/nestjs/src/outbound-campaigns/dto/upload-entries.dto.ts +35 -0
- package/nestjs/src/outbound-campaigns/outbound-campaigns.controller.ts +307 -0
- package/nestjs/src/outbound-campaigns/outbound-campaigns.module.ts +13 -0
- package/nestjs/src/outbound-campaigns/outbound-campaigns.service.ts +471 -0
- package/nestjs/src/outbound-campaigns/processors/campaign-dispatch.processor.ts +366 -0
- package/nestjs/src/patients/documents.service.ts +231 -0
- package/nestjs/src/patients/dto/create-examination.dto.ts +34 -0
- package/nestjs/src/patients/dto/create-note.dto.ts +14 -0
- package/nestjs/src/patients/dto/create-patient.dto.ts +86 -0
- package/nestjs/src/patients/dto/create-photo-set.dto.ts +32 -0
- package/nestjs/src/patients/dto/index.ts +10 -0
- package/nestjs/src/patients/dto/update-examination.dto.ts +29 -0
- package/nestjs/src/patients/dto/update-medical-history.dto.ts +47 -0
- package/nestjs/src/patients/dto/update-patient.dto.ts +87 -0
- package/nestjs/src/patients/dto/update-photo-set.dto.ts +28 -0
- package/nestjs/src/patients/dto/upload-document.dto.ts +31 -0
- package/nestjs/src/patients/dto/upload-photo.dto.ts +27 -0
- package/nestjs/src/patients/examinations.service.ts +271 -0
- package/nestjs/src/patients/medical-history.service.ts +149 -0
- package/nestjs/src/patients/notes.service.ts +172 -0
- package/nestjs/src/patients/patients.controller.ts +485 -0
- package/nestjs/src/patients/patients.module.ts +22 -0
- package/nestjs/src/patients/patients.service.ts +412 -0
- package/nestjs/src/patients/photo-sets.service.ts +389 -0
- package/nestjs/src/phone-numbers/dto/create-phone-number.dto.ts +57 -0
- package/nestjs/src/phone-numbers/dto/index.ts +3 -0
- package/nestjs/src/phone-numbers/dto/update-phone-number.dto.ts +38 -0
- package/nestjs/src/phone-numbers/dto/update-schedule.dto.ts +39 -0
- package/nestjs/src/phone-numbers/phone-numbers.controller.ts +125 -0
- package/nestjs/src/phone-numbers/phone-numbers.module.ts +10 -0
- package/nestjs/src/phone-numbers/phone-numbers.service.ts +209 -0
- package/nestjs/src/plans/dto/create-plan.dto.ts +70 -0
- package/nestjs/src/plans/dto/index.ts +2 -0
- package/nestjs/src/plans/dto/update-plan.dto.ts +80 -0
- package/nestjs/src/plans/plans.controller.ts +50 -0
- package/nestjs/src/plans/plans.module.ts +10 -0
- package/nestjs/src/plans/plans.service.ts +115 -0
- package/nestjs/src/platform/dto/create-tenant.dto.ts +36 -0
- package/nestjs/src/platform/dto/index.ts +2 -0
- package/nestjs/src/platform/dto/update-tenant-platform.dto.ts +44 -0
- package/nestjs/src/platform/platform.controller.ts +79 -0
- package/nestjs/src/platform/platform.module.ts +10 -0
- package/nestjs/src/platform/platform.service.ts +301 -0
- package/nestjs/src/queue/queue.module.ts +56 -0
- package/nestjs/src/redis/redis.module.ts +20 -0
- package/nestjs/src/tenants/dto/index.ts +1 -0
- package/nestjs/src/tenants/dto/update-tenant.dto.ts +15 -0
- package/nestjs/src/tenants/tenants.controller.ts +45 -0
- package/nestjs/src/tenants/tenants.module.ts +10 -0
- package/nestjs/src/tenants/tenants.service.ts +41 -0
- package/nestjs/src/tools/adapters/elevenlabs-http.adapter.ts +51 -0
- package/nestjs/src/tools/adapters/elevenlabs-ws.adapter.ts +59 -0
- package/nestjs/src/tools/handlers/calendar.tools.ts +441 -0
- package/nestjs/src/tools/handlers/notification.tools.ts +34 -0
- package/nestjs/src/tools/handlers/operator.tools.ts +303 -0
- package/nestjs/src/tools/handlers/patient.tools.ts +242 -0
- package/nestjs/src/tools/handlers/payment.tools.ts +43 -0
- package/nestjs/src/tools/handlers/validation.tools.ts +152 -0
- package/nestjs/src/tools/tool-registry.service.ts +221 -0
- package/nestjs/src/tools/tool.decorator.ts +16 -0
- package/nestjs/src/tools/tool.interfaces.ts +26 -0
- package/nestjs/src/tools/tools.module.ts +50 -0
- package/nestjs/src/treatments/dto/create-plan-item.dto.ts +27 -0
- package/nestjs/src/treatments/dto/create-treatment-plan.dto.ts +69 -0
- package/nestjs/src/treatments/dto/create-treatment.dto.ts +59 -0
- package/nestjs/src/treatments/dto/index.ts +6 -0
- package/nestjs/src/treatments/dto/update-plan-item.dto.ts +23 -0
- package/nestjs/src/treatments/dto/update-treatment-plan.dto.ts +22 -0
- package/nestjs/src/treatments/dto/update-treatment.dto.ts +70 -0
- package/nestjs/src/treatments/treatment-plans.service.ts +362 -0
- package/nestjs/src/treatments/treatments.controller.ts +265 -0
- package/nestjs/src/treatments/treatments.module.ts +14 -0
- package/nestjs/src/treatments/treatments.service.ts +165 -0
- package/nestjs/src/users/doctor-profiles.service.ts +202 -0
- package/nestjs/src/users/dto/index.ts +4 -0
- package/nestjs/src/users/dto/invite-user.dto.ts +52 -0
- package/nestjs/src/users/dto/update-clinic-assignments.dto.ts +9 -0
- package/nestjs/src/users/dto/update-doctor-profile.dto.ts +49 -0
- package/nestjs/src/users/dto/update-user.dto.ts +41 -0
- package/nestjs/src/users/users.controller.ts +142 -0
- package/nestjs/src/users/users.module.ts +11 -0
- package/nestjs/src/users/users.service.ts +250 -0
- package/nestjs/src/webhooks/elevenlabs-tool.controller.ts +66 -0
- package/nestjs/src/webhooks/elevenlabs-webhook.controller.ts +60 -0
- package/nestjs/src/webhooks/meta-webhook.controller.ts +178 -0
- package/nestjs/src/webhooks/processors/elevenlabs-webhook.processor.ts +28 -0
- package/nestjs/src/webhooks/webhooks.module.ts +17 -0
- package/nestjs/src/whatsapp/chat-context.service.ts +281 -0
- package/nestjs/src/whatsapp/dto/add-blacklist.dto.ts +13 -0
- package/nestjs/src/whatsapp/dto/index.ts +2 -0
- package/nestjs/src/whatsapp/dto/send-message.dto.ts +14 -0
- package/nestjs/src/whatsapp/listeners/meta-message.listener.ts +579 -0
- package/nestjs/src/whatsapp/meta-auth.controller.ts +268 -0
- package/nestjs/src/whatsapp/meta-instagram-auth.controller.ts +244 -0
- package/nestjs/src/whatsapp/operator.service.ts +692 -0
- package/nestjs/src/whatsapp/processors/cost-sweep.processor.ts +130 -0
- package/nestjs/src/whatsapp/processors/grace.processor.ts +191 -0
- package/nestjs/src/whatsapp/processors/message-buffer.processor.ts +138 -0
- package/nestjs/src/whatsapp/processors/message-cleanup.processor.ts +148 -0
- package/nestjs/src/whatsapp/processors/meta-token-refresh.processor.ts +114 -0
- package/nestjs/src/whatsapp/processors/operator-expiry.processor.ts +105 -0
- package/nestjs/src/whatsapp/processors/profile-update.processor.ts +234 -0
- package/nestjs/src/whatsapp/processors/session-cleanup.processor.ts +178 -0
- package/nestjs/src/whatsapp/processors/session-labels.processor.ts +248 -0
- package/nestjs/src/whatsapp/whatsapp-agent.service.ts +2506 -0
- package/nestjs/src/whatsapp/whatsapp-recovery.service.ts +117 -0
- package/nestjs/src/whatsapp/whatsapp.controller.ts +398 -0
- package/nestjs/src/whatsapp/whatsapp.module.ts +51 -0
- package/nestjs/src/whatsapp/whatsapp.service.ts +592 -0
- package/nestjs/test/app.e2e-spec.ts +25 -0
- package/nestjs/test/jest-e2e.json +9 -0
- package/nestjs/tsconfig.build.json +4 -0
- package/nestjs/tsconfig.json +25 -0
- package/nginx.example.conf +18 -0
- package/package.json +47 -0
- package/scripts/addRecipient.ts +48 -0
- package/scripts/listRecipients.ts +31 -0
- package/scripts/migrate-agent-ref.ts +86 -0
- package/scripts/migrate-sessions.ts +183 -0
- package/scripts/promote.ts +27 -0
- package/scripts/retrigger.ts +84 -0
- package/scripts/seed.ts +435 -0
- package/scripts/testSend.ts +63 -0
- package/src/app.ts +85 -0
- package/src/config/agentPrompts.json +19 -0
- package/src/config/database.ts +21 -0
- package/src/config/env.ts +94 -0
- package/src/config/index.ts +86 -0
- package/src/controllers/webhook.controller.ts +150 -0
- package/src/errors/index.ts +9 -0
- package/src/hooks/webhookValidator.ts +55 -0
- package/src/index.ts +68 -0
- package/src/migrations/001_session-architecture.ts +138 -0
- package/src/migrations/002_agent-ref.ts +65 -0
- package/src/migrations/003_shift-schedule.ts +55 -0
- package/src/migrations/004_invert-shift-to-clinic-hours.ts +30 -0
- package/src/migrations/005_composite-baileys-chat-ids.ts +60 -0
- package/src/migrations/migration.model.ts +27 -0
- package/src/migrations/migration.routes.ts +40 -0
- package/src/migrations/migration.runner.ts +112 -0
- package/src/models/ConversationClaim.ts +17 -0
- package/src/models/ConversationEvaluation.ts +91 -0
- package/src/models/Summary.ts +77 -0
- package/src/models/Transcription.ts +68 -0
- package/src/models/WhatsAppRecipient.ts +37 -0
- package/src/modules/admin/admin.routes.ts +2385 -0
- package/src/modules/admin/elevenlabs-test-chat.routes.ts +193 -0
- package/src/modules/admin/whatsapp-test-chat.routes.ts +244 -0
- package/src/modules/agents/agent.model.ts +93 -0
- package/src/modules/agents/agent.routes.ts +65 -0
- package/src/modules/appointment-validation/appointment-validation.model.ts +163 -0
- package/src/modules/appointment-validation/appointment-validation.routes.ts +275 -0
- package/src/modules/appointment-validation/appointment-validation.service.ts +1028 -0
- package/src/modules/auth/auth.model.ts +42 -0
- package/src/modules/auth/auth.routes.ts +199 -0
- package/src/modules/auth/auth.service.ts +210 -0
- package/src/modules/auth/refresh-token.model.ts +26 -0
- package/src/modules/billing/billing-alert.model.ts +28 -0
- package/src/modules/billing/billing-period-snapshot.model.ts +68 -0
- package/src/modules/billing/billing.model.ts +42 -0
- package/src/modules/billing/billing.routes.ts +67 -0
- package/src/modules/billing/billing.service.ts +562 -0
- package/src/modules/billing/payment.model.ts +42 -0
- package/src/modules/calls/call.model.ts +102 -0
- package/src/modules/calls/call.routes.ts +118 -0
- package/src/modules/campaigns/campaign.model.ts +111 -0
- package/src/modules/campaigns/campaign.routes.ts +402 -0
- package/src/modules/campaigns/campaign.service.ts +99 -0
- package/src/modules/google-calendar/google-calendar.routes.ts +31 -0
- package/src/modules/inbound-call/inbound-call-config.model.ts +49 -0
- package/src/modules/inbound-call/inbound-call.routes.ts +289 -0
- package/src/modules/leads/lead.model.ts +40 -0
- package/src/modules/leads/lead.routes.ts +246 -0
- package/src/modules/logs/log.model.ts +27 -0
- package/src/modules/logs/log.routes.ts +102 -0
- package/src/modules/plans/plan.model.ts +45 -0
- package/src/modules/surveys/survey.model.ts +70 -0
- package/src/modules/surveys/survey.routes.ts +187 -0
- package/src/modules/tenants/tenant.model.ts +181 -0
- package/src/modules/tenants/tenant.routes.ts +78 -0
- package/src/modules/users/user.routes.ts +126 -0
- package/src/modules/webhooks/elevenlabs-tool.routes.ts +94 -0
- package/src/modules/webhooks/elevenlabs-tool.service.ts +491 -0
- package/src/modules/webhooks/elevenlabs.routes.ts +34 -0
- package/src/modules/webhooks/elevenlabs.service.ts +565 -0
- package/src/modules/webhooks/unipile.routes.ts +917 -0
- package/src/modules/whatsapp/appointment.model.ts +47 -0
- package/src/modules/whatsapp/operator-request.model.ts +58 -0
- package/src/modules/whatsapp/stt-usage.model.ts +30 -0
- package/src/modules/whatsapp/whatsapp-chat.model.ts +39 -0
- package/src/modules/whatsapp/whatsapp-contact-profile.model.ts +41 -0
- package/src/modules/whatsapp/whatsapp-message.model.ts +41 -0
- package/src/modules/whatsapp/whatsapp-session.model.ts +60 -0
- package/src/modules/whatsapp/whatsapp.routes.ts +1435 -0
- package/src/plugins/cors.ts +7 -0
- package/src/plugins/jwt.ts +18 -0
- package/src/plugins/rawBody.ts +12 -0
- package/src/plugins/rbac.ts +24 -0
- package/src/routes/admin.routes.ts +208 -0
- package/src/routes/health.routes.ts +12 -0
- package/src/routes/webhook.routes.ts +12 -0
- package/src/services/ai/base.ai.ts +132 -0
- package/src/services/ai/gemini.service.ts +41 -0
- package/src/services/ai/index.ts +24 -0
- package/src/services/ai/openai.service.ts +48 -0
- package/src/services/elevenlabs.service.ts +532 -0
- package/src/services/google-calendar.service.ts +656 -0
- package/src/services/inbound-call-schedule.service.ts +174 -0
- package/src/services/netgsm.service.ts +128 -0
- package/src/services/unipile.service.ts +200 -0
- package/src/services/whatsapp-agent.service.ts +2479 -0
- package/src/services/whatsapp.service.ts +245 -0
- package/src/templates/index.ts +71 -0
- package/src/templates/receptionist.ts +44 -0
- package/src/templates/survey.ts +44 -0
- package/src/types/index.ts +218 -0
- package/src/utils/logger.ts +83 -0
- package/tsconfig.json +19 -0
- package/web/.dockerignore +4 -0
- package/web/Dockerfile +18 -0
- package/web/README.md +73 -0
- package/web/components.json +23 -0
- package/web/eslint.config.js +23 -0
- package/web/index.html +14 -0
- package/web/nginx.conf +18 -0
- package/web/package-lock.json +10292 -0
- package/web/package.json +48 -0
- package/web/public/favicon.ico +0 -0
- package/web/public/vite.svg +1 -0
- package/web/src/App.tsx +191 -0
- package/web/src/assets/react.svg +1 -0
- package/web/src/components/Layout.tsx +261 -0
- package/web/src/components/LeadConversation.tsx +251 -0
- package/web/src/components/ProtectedRoute.tsx +20 -0
- package/web/src/components/conversation-review/CardAudioPlayer.tsx +200 -0
- package/web/src/components/conversation-review/CardEvaluation.tsx +351 -0
- package/web/src/components/conversation-review/CardMetadata.tsx +44 -0
- package/web/src/components/conversation-review/ConversationCard.tsx +95 -0
- package/web/src/components/conversation-review/ReviewContainer.tsx +120 -0
- package/web/src/components/conversation-review/ReviewFilters.tsx +88 -0
- package/web/src/components/empty-state.tsx +15 -0
- package/web/src/components/page-header.tsx +19 -0
- package/web/src/components/page-loader.tsx +32 -0
- package/web/src/components/pagination.tsx +38 -0
- package/web/src/components/stat-card.tsx +27 -0
- package/web/src/components/status-badge.tsx +125 -0
- package/web/src/components/ui/alert.tsx +66 -0
- package/web/src/components/ui/badge.tsx +48 -0
- package/web/src/components/ui/button.tsx +64 -0
- package/web/src/components/ui/card.tsx +92 -0
- package/web/src/components/ui/checkbox.tsx +32 -0
- package/web/src/components/ui/dialog.tsx +158 -0
- package/web/src/components/ui/dropdown-menu.tsx +255 -0
- package/web/src/components/ui/input.tsx +21 -0
- package/web/src/components/ui/label.tsx +22 -0
- package/web/src/components/ui/progress.tsx +29 -0
- package/web/src/components/ui/select.tsx +188 -0
- package/web/src/components/ui/separator.tsx +28 -0
- package/web/src/components/ui/sheet.tsx +123 -0
- package/web/src/components/ui/skeleton.tsx +13 -0
- package/web/src/components/ui/sonner.tsx +35 -0
- package/web/src/components/ui/table.tsx +116 -0
- package/web/src/components/ui/tabs.tsx +89 -0
- package/web/src/components/ui/textarea.tsx +18 -0
- package/web/src/components/ui/tooltip.tsx +57 -0
- package/web/src/components/whatsapp/ChatDetail.tsx +417 -0
- package/web/src/components/whatsapp/ChatHeader.tsx +78 -0
- package/web/src/components/whatsapp/ChatList.tsx +107 -0
- package/web/src/components/whatsapp/ChatListItem.tsx +60 -0
- package/web/src/components/whatsapp/MessageBubble.tsx +46 -0
- package/web/src/components/whatsapp/MessageInput.tsx +63 -0
- package/web/src/components/whatsapp/MessageStream.tsx +135 -0
- package/web/src/components/whatsapp/SessionDivider.tsx +65 -0
- package/web/src/components/whatsapp/SessionHistory.tsx +119 -0
- package/web/src/components/whatsapp/settings/AiSettingsTab.tsx +268 -0
- package/web/src/components/whatsapp/settings/CalendarTab.tsx +339 -0
- package/web/src/components/whatsapp/settings/ConnectionTab.tsx +236 -0
- package/web/src/components/whatsapp/settings/NotificationsTab.tsx +109 -0
- package/web/src/components/whatsapp/settings/OperatorTab.tsx +303 -0
- package/web/src/contexts/AuthContext.tsx +103 -0
- package/web/src/index.css +130 -0
- package/web/src/lib/api.ts +92 -0
- package/web/src/lib/utils.ts +6 -0
- package/web/src/main.tsx +10 -0
- package/web/src/pages/AppointmentDetail.tsx +206 -0
- package/web/src/pages/AppointmentValidation.tsx +157 -0
- package/web/src/pages/AppointmentValidationDetail.tsx +617 -0
- package/web/src/pages/AppointmentValidationNew.tsx +1005 -0
- package/web/src/pages/Appointments.tsx +283 -0
- package/web/src/pages/Billing.tsx +126 -0
- package/web/src/pages/CallDetail.tsx +293 -0
- package/web/src/pages/CallSettings.tsx +313 -0
- package/web/src/pages/Calls.tsx +188 -0
- package/web/src/pages/CampaignDetail.tsx +216 -0
- package/web/src/pages/CampaignNew.tsx +277 -0
- package/web/src/pages/CampaignResults.tsx +185 -0
- package/web/src/pages/Campaigns.tsx +171 -0
- package/web/src/pages/Dashboard.tsx +336 -0
- package/web/src/pages/InboundSchedule.tsx +246 -0
- package/web/src/pages/LeadDetail.tsx +183 -0
- package/web/src/pages/Leads.tsx +258 -0
- package/web/src/pages/Login.tsx +99 -0
- package/web/src/pages/Register.tsx +129 -0
- package/web/src/pages/Settings.tsx +133 -0
- package/web/src/pages/SurveyDetail.tsx +232 -0
- package/web/src/pages/SurveyPreview.tsx +179 -0
- package/web/src/pages/Surveys.tsx +207 -0
- package/web/src/pages/Users.tsx +199 -0
- package/web/src/pages/WhatsApp.tsx +147 -0
- package/web/src/pages/WhatsAppSettings.tsx +215 -0
- package/web/src/pages/admin/AdminBaileysMessageLog.tsx +331 -0
- package/web/src/pages/admin/AdminBaileysRawMessages.tsx +318 -0
- package/web/src/pages/admin/AdminConversationReview.tsx +116 -0
- package/web/src/pages/admin/AdminCredits.tsx +467 -0
- package/web/src/pages/admin/AdminElevenLabsAgentDetail.tsx +332 -0
- package/web/src/pages/admin/AdminElevenLabsAgents.tsx +164 -0
- package/web/src/pages/admin/AdminElevenLabsBatchCallDetail.tsx +214 -0
- package/web/src/pages/admin/AdminElevenLabsBatchCalls.tsx +293 -0
- package/web/src/pages/admin/AdminElevenLabsConversationDetail.tsx +230 -0
- package/web/src/pages/admin/AdminElevenLabsConversations.tsx +228 -0
- package/web/src/pages/admin/AdminElevenLabsInboundCalls.tsx +293 -0
- package/web/src/pages/admin/AdminElevenLabsPhoneNumbers.tsx +506 -0
- package/web/src/pages/admin/AdminElevenLabsTestChat.tsx +258 -0
- package/web/src/pages/admin/AdminElevenLabsWebhooks.tsx +289 -0
- package/web/src/pages/admin/AdminEvaluationDashboard.tsx +520 -0
- package/web/src/pages/admin/AdminLeads.tsx +339 -0
- package/web/src/pages/admin/AdminLogs.tsx +283 -0
- package/web/src/pages/admin/AdminMigrations.tsx +247 -0
- package/web/src/pages/admin/AdminPlans.tsx +313 -0
- package/web/src/pages/admin/AdminSystem.tsx +391 -0
- package/web/src/pages/admin/AdminTenantBillableItems.tsx +464 -0
- package/web/src/pages/admin/AdminTenantDetail.tsx +1317 -0
- package/web/src/pages/admin/AdminTenants.tsx +274 -0
- package/web/src/pages/admin/AdminWhatsApp.tsx +618 -0
- package/web/src/pages/admin/AdminWhatsAppTestChat.tsx +328 -0
- package/web/src/types/index.ts +242 -0
- package/web/tsconfig.app.json +32 -0
- package/web/tsconfig.json +13 -0
- package/web/tsconfig.node.json +26 -0
- package/web/vite.config.ts +23 -0
|
@@ -0,0 +1,1086 @@
|
|
|
1
|
+
# WhatsApp AI Agent System — Complete Technical Documentation
|
|
2
|
+
|
|
3
|
+
> Session-based WhatsApp AI agent that gives humans first chance to respond, then automatically hands off to ElevenLabs AI via real-time WebSocket.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [System Overview](#1-system-overview)
|
|
10
|
+
2. [Architecture](#2-architecture)
|
|
11
|
+
3. [External Dependencies](#3-external-dependencies)
|
|
12
|
+
4. [Database Models](#4-database-models)
|
|
13
|
+
5. [Tenant Configuration](#5-tenant-configuration)
|
|
14
|
+
6. [Session Lifecycle](#6-session-lifecycle)
|
|
15
|
+
7. [Webhook Handler — Unipile](#7-webhook-handler--unipile)
|
|
16
|
+
8. [Agent Service — WhatsAppAgentService](#8-agent-service--whatsappagentservice)
|
|
17
|
+
9. [API Routes](#9-api-routes)
|
|
18
|
+
10. [enrichChat — Backward Compatibility Layer](#10-enrichchat--backward-compatibility-layer)
|
|
19
|
+
11. [Frontend](#11-frontend)
|
|
20
|
+
12. [Blacklist System](#12-blacklist-system)
|
|
21
|
+
13. [Timers, Recovery & Safety Nets](#13-timers-recovery--safety-nets)
|
|
22
|
+
14. [ElevenLabs WebSocket Protocol](#14-elevenlabs-websocket-protocol)
|
|
23
|
+
15. [Migration](#15-migration)
|
|
24
|
+
16. [File Map](#16-file-map)
|
|
25
|
+
17. [Constants & Timeouts](#17-constants--timeouts)
|
|
26
|
+
18. [Decision Tables](#18-decision-tables)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 1. System Overview
|
|
31
|
+
|
|
32
|
+
The WhatsApp Agent is a fully backend-autonomous AI chat system. It requires **no cron jobs** and **no frontend dependency** to operate. The system:
|
|
33
|
+
|
|
34
|
+
1. Receives WhatsApp messages via **Unipile** webhooks
|
|
35
|
+
2. Creates a **session** with a configurable **grace period** (default: 180 seconds)
|
|
36
|
+
3. During the grace period, humans can respond first — if they do, the session resolves
|
|
37
|
+
4. If no human responds before the deadline, **ElevenLabs AI** takes over automatically
|
|
38
|
+
5. The AI communicates via a **text-only WebSocket** to ElevenLabs, and sends responses back through **Unipile**
|
|
39
|
+
6. Humans can **take over** from AI at any time, and later **release** back to AI
|
|
40
|
+
|
|
41
|
+
The entire system is **event-driven**: Unipile webhook triggers processing, in-memory `setTimeout` timers handle grace periods, and the WebSocket connection to ElevenLabs handles AI responses.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 2. Architecture
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
49
|
+
│ WhatsApp │ │ ElevenLabs AI │
|
|
50
|
+
│ (Contact) │ │ (Conversational│
|
|
51
|
+
│ │ │ Agent) │
|
|
52
|
+
└────────┬────────┘ └────────▲────────┘
|
|
53
|
+
│ │
|
|
54
|
+
│ Messages WebSocket (text-only)
|
|
55
|
+
│ getSignedUrl → ws://
|
|
56
|
+
▼ │
|
|
57
|
+
┌─────────────────┐ POST /webhooks/unipile ┌───────┴────────┐
|
|
58
|
+
│ │ ─────────────────────────────▶ │ │
|
|
59
|
+
│ Unipile │ │ Fastify API │
|
|
60
|
+
│ (WhatsApp │ ◀─────────────────────────── │ (Backend) │
|
|
61
|
+
│ Bridge) │ sendMessage(chatId, text) │ │
|
|
62
|
+
│ │ │ │
|
|
63
|
+
└─────────────────┘ └───────┬────────┘
|
|
64
|
+
│
|
|
65
|
+
┌──────┴──────┐
|
|
66
|
+
│ MongoDB │
|
|
67
|
+
│ - Chats │
|
|
68
|
+
│ - Sessions │
|
|
69
|
+
│ - Messages │
|
|
70
|
+
└─────────────┘
|
|
71
|
+
|
|
72
|
+
┌─────────────────┐ /api/whatsapp/*
|
|
73
|
+
│ React Frontend │ ──────────────────────────────▶ (same Fastify)
|
|
74
|
+
│ (Dashboard) │ ◀──────────────────────────────
|
|
75
|
+
└─────────────────┘ enrichChat() adds virtual fields
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Key insight**: The frontend is purely a management dashboard. The AI agent operates entirely on the backend. If the frontend is offline, the agent continues to work normally.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 3. External Dependencies
|
|
83
|
+
|
|
84
|
+
### 3.1 Unipile — WhatsApp Message Bridge
|
|
85
|
+
|
|
86
|
+
| Operation | Method | Endpoint | Purpose |
|
|
87
|
+
|-----------|--------|----------|---------|
|
|
88
|
+
| Connect WhatsApp | POST | `/api/v1/accounts` | Returns QR code for phone scan |
|
|
89
|
+
| Reconnect | POST | `/api/v1/accounts/:id/reconnect` | Re-establish dropped connection |
|
|
90
|
+
| Account Status | GET | `/api/v1/accounts/:id` | Check if WhatsApp is connected |
|
|
91
|
+
| Disconnect | DELETE | `/api/v1/accounts/:id` | Remove WhatsApp account |
|
|
92
|
+
| Send Message | POST | `/api/v1/chats/:chatId/messages` | Send text to contact |
|
|
93
|
+
| Register Webhook | POST | `/api/v1/webhooks` | Register URL for incoming messages |
|
|
94
|
+
|
|
95
|
+
**Auth**: `X-API-KEY` header with `UNIPILE_API_KEY`.
|
|
96
|
+
|
|
97
|
+
**Webhook payload** (incoming message):
|
|
98
|
+
```typescript
|
|
99
|
+
{
|
|
100
|
+
account_id: string; // Which Unipile account received it
|
|
101
|
+
account_type: 'WHATSAPP';
|
|
102
|
+
event: 'message_received';
|
|
103
|
+
chat_id: string; // Unipile's chat identifier
|
|
104
|
+
message_id: string; // Unique message ID
|
|
105
|
+
message: string; // Message text content
|
|
106
|
+
timestamp: string; // ISO timestamp
|
|
107
|
+
is_sender: boolean; // true = sent by us, false = received from contact
|
|
108
|
+
sender: {
|
|
109
|
+
attendee_name: string;
|
|
110
|
+
attendee_provider_id: string;
|
|
111
|
+
attendee_specifics?: { phone_number?: string };
|
|
112
|
+
};
|
|
113
|
+
attendees: [{ // attendees[0] = the contact
|
|
114
|
+
attendee_name: string;
|
|
115
|
+
attendee_provider_id: string;
|
|
116
|
+
attendee_specifics?: { phone_number?: string };
|
|
117
|
+
}];
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 3.2 ElevenLabs — Conversational AI (Text-Only WebSocket)
|
|
122
|
+
|
|
123
|
+
**Connection flow**:
|
|
124
|
+
1. Call `elevenlabsService.getSignedUrl({ agentId })` → returns a time-limited signed URL (15min)
|
|
125
|
+
2. Append `?textOnly=true` to the URL
|
|
126
|
+
3. Open a `ws://` WebSocket connection
|
|
127
|
+
4. Wait for `conversation_initiation_metadata` message (contains `conversation_id`)
|
|
128
|
+
5. Send `contextual_update` with conversation history
|
|
129
|
+
6. Send `user_message` with the contact's text
|
|
130
|
+
7. Receive `agent_response` with AI reply text
|
|
131
|
+
|
|
132
|
+
**WebSocket messages we send**:
|
|
133
|
+
```json
|
|
134
|
+
{ "type": "contextual_update", "text": "Previous conversation history:\nPatient: ...\nOperator: ..." }
|
|
135
|
+
{ "type": "user_message", "text": "Contact's message text" }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**WebSocket messages we receive**:
|
|
139
|
+
```json
|
|
140
|
+
{ "type": "conversation_initiation_metadata", "conversation_initiation_metadata_event": { "conversation_id": "..." } }
|
|
141
|
+
{ "type": "agent_response", "agent_response_event": { "agent_response": "AI reply text" } }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 4. Database Models
|
|
147
|
+
|
|
148
|
+
### 4.1 WhatsAppChat
|
|
149
|
+
|
|
150
|
+
**Collection**: `whatsappchats`
|
|
151
|
+
|
|
152
|
+
Represents a 1:1 WhatsApp conversation between the business and a contact. One chat per contact per tenant. Never deleted, can be closed/reopened.
|
|
153
|
+
|
|
154
|
+
| Field | Type | Default | Description |
|
|
155
|
+
|-------|------|---------|-------------|
|
|
156
|
+
| `tenant_id` | ObjectId → Tenant | required | Tenant ownership |
|
|
157
|
+
| `unipile_chat_id` | String | required | Unipile's unique chat ID |
|
|
158
|
+
| `unipile_account_id` | String | required | Which Unipile account this chat belongs to |
|
|
159
|
+
| `contact_name` | String | `'Bilinmiyor'` | Contact's display name from WhatsApp |
|
|
160
|
+
| `contact_phone` | String | `''` | Contact's phone number (e.g. `+905551234567`) |
|
|
161
|
+
| `is_closed` | Boolean | `false` | Whether the chat has been manually closed |
|
|
162
|
+
| `active_session_id` | ObjectId → WhatsAppSession | `null` | Currently active session (null = no session) |
|
|
163
|
+
| `last_message_at` | Date | `Date.now` | Timestamp of the most recent message |
|
|
164
|
+
| `last_message_preview` | String | `''` | First 100 characters of the last message |
|
|
165
|
+
| `message_count` | Number | `0` | Total messages across all sessions |
|
|
166
|
+
| `createdAt` | Date | auto | |
|
|
167
|
+
| `updatedAt` | Date | auto | |
|
|
168
|
+
|
|
169
|
+
**Indexes**:
|
|
170
|
+
- `{ tenant_id: 1, last_message_at: -1 }` — List chats sorted by recency
|
|
171
|
+
- `{ unipile_chat_id: 1 }` unique — Fast lookup from webhook
|
|
172
|
+
|
|
173
|
+
### 4.2 WhatsAppSession
|
|
174
|
+
|
|
175
|
+
**Collection**: `whatsappsessions`
|
|
176
|
+
|
|
177
|
+
Represents one conversation "turn" within a chat. A chat can have many sessions over its lifetime. Each session tracks the grace period, AI connection, and human takeover state.
|
|
178
|
+
|
|
179
|
+
| Field | Type | Default | Description |
|
|
180
|
+
|-------|------|---------|-------------|
|
|
181
|
+
| `chat_id` | ObjectId → WhatsAppChat | required | Parent chat |
|
|
182
|
+
| `tenant_id` | ObjectId → Tenant | required | Tenant ownership |
|
|
183
|
+
| `status` | `'waiting'` \| `'active'` \| `'resolved'` | `'waiting'` | Current lifecycle state |
|
|
184
|
+
| `resolved_by` | `'human'` \| `'ai_timeout'` \| `'manual'` \| `'timeout'` \| `null` | `null` | How the session ended |
|
|
185
|
+
| `started_at` | Date | `Date.now` | When this session was created |
|
|
186
|
+
| `resolved_at` | Date \| null | `null` | When this session ended |
|
|
187
|
+
| `grace_deadline` | Date \| null | `null` | When AI will take over if human doesn't respond |
|
|
188
|
+
| `taken_over_by` | ObjectId → User \| null | `null` | User who took over (via dashboard UI) |
|
|
189
|
+
| `taken_over_at` | Date \| null | `null` | When takeover happened (by UI or WhatsApp reply) |
|
|
190
|
+
| `elevenlabs_conversation_id` | String \| null | `null` | Active ElevenLabs conversation ID |
|
|
191
|
+
| `elevenlabs_conversation_created_at` | Date \| null | `null` | When EL conversation was established |
|
|
192
|
+
| `elevenlabs_last_interaction_at` | Date \| null | `null` | Last message sent to/from ElevenLabs |
|
|
193
|
+
| `message_count` | Number | `0` | Messages within this session |
|
|
194
|
+
| `createdAt` | Date | auto | |
|
|
195
|
+
| `updatedAt` | Date | auto | |
|
|
196
|
+
|
|
197
|
+
**Indexes**:
|
|
198
|
+
- `{ chat_id: 1, status: 1 }` — Find active session for a chat
|
|
199
|
+
- `{ status: 1, grace_deadline: 1 }` — Stuck session sweeper query
|
|
200
|
+
- `{ tenant_id: 1, status: 1 }` — Stats queries (count waiting/active sessions)
|
|
201
|
+
|
|
202
|
+
**Status meanings**:
|
|
203
|
+
| Status | Meaning | Who is handling? |
|
|
204
|
+
|--------|---------|-----------------|
|
|
205
|
+
| `waiting` | Grace period running, messages are being buffered | Nobody yet — waiting for human or timer |
|
|
206
|
+
| `active` + no takeover | AI has taken over, ElevenLabs WebSocket active | AI |
|
|
207
|
+
| `active` + `taken_over_by` set | Human claimed via dashboard "Devral" button | Human (via dashboard) |
|
|
208
|
+
| `active` + `taken_over_at` set (no `taken_over_by`) | Human replied directly in WhatsApp app | Human (via WhatsApp) |
|
|
209
|
+
| `resolved` | Session ended | Nobody — chat is idle |
|
|
210
|
+
|
|
211
|
+
**resolved_by meanings**:
|
|
212
|
+
| Value | Trigger |
|
|
213
|
+
|-------|---------|
|
|
214
|
+
| `'human'` | A self-sent message arrived while session was `waiting` |
|
|
215
|
+
| `'ai_timeout'` | Grace period expired but no agent configured (unused currently) |
|
|
216
|
+
| `'manual'` | User clicked "Close" or "Takeover" then "Close" in dashboard |
|
|
217
|
+
| `'timeout'` | Session had no activity for 30 minutes and was auto-resolved by the inactivity sweeper |
|
|
218
|
+
|
|
219
|
+
### 4.3 WhatsAppMessage
|
|
220
|
+
|
|
221
|
+
**Collection**: `whatsappmessages`
|
|
222
|
+
|
|
223
|
+
Every message exchanged in a WhatsApp chat. Messages from all three parties (contact, AI, human operator) are stored here.
|
|
224
|
+
|
|
225
|
+
| Field | Type | Default | Description |
|
|
226
|
+
|-------|------|---------|-------------|
|
|
227
|
+
| `chat_id` | ObjectId → WhatsAppChat | required | Parent chat |
|
|
228
|
+
| `session_id` | ObjectId → WhatsAppSession \| null | `null` | Which session this belongs to (null = blacklisted/legacy) |
|
|
229
|
+
| `tenant_id` | ObjectId → Tenant | required | Tenant ownership |
|
|
230
|
+
| `unipile_message_id` | String \| null | `null` | Unipile's message ID (for dedup of self-sent messages) |
|
|
231
|
+
| `sender` | `'contact'` \| `'ai'` \| `'human'` | required | Who sent this message |
|
|
232
|
+
| `sender_name` | String | `''` | Display name (contact name, `'AI'`, user ID, or `'WhatsApp'`) |
|
|
233
|
+
| `text` | String | required | Message content |
|
|
234
|
+
| `sent_via_unipile` | Boolean | `false` | `true` if we sent this through Unipile API |
|
|
235
|
+
| `createdAt` | Date | auto | |
|
|
236
|
+
|
|
237
|
+
**Indexes**:
|
|
238
|
+
- `{ chat_id: 1, createdAt: 1 }` — Load chat messages chronologically
|
|
239
|
+
- `{ session_id: 1, createdAt: 1 }` — Session-scoped queries (context building, replay)
|
|
240
|
+
|
|
241
|
+
**sender field explanation**:
|
|
242
|
+
| `sender` value | Origin | `sender_name` | `sent_via_unipile` |
|
|
243
|
+
|---------------|--------|---------------|---------------------|
|
|
244
|
+
| `'contact'` | Incoming from WhatsApp user | Contact's name | `false` |
|
|
245
|
+
| `'human'` + sender_name=`'AI'` | AI response sent through Unipile | `'AI'` | `true` |
|
|
246
|
+
| `'human'` + sender_name=userId | Manual message from dashboard | User's ID | `true` |
|
|
247
|
+
| `'human'` + sender_name=`'WhatsApp'` | Self-sent from WhatsApp app directly | `'WhatsApp'` | `false` |
|
|
248
|
+
|
|
249
|
+
> Note: AI messages use `sender: 'human'` (not `'ai'`) because from WhatsApp's perspective they appear as messages from the business number. The `sender_name: 'AI'` distinguishes them.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 5. Tenant Configuration
|
|
254
|
+
|
|
255
|
+
WhatsApp agent settings are stored in `Tenant.settings.whatsapp_agent`:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
{
|
|
259
|
+
unipile_account_id: string | null; // Linked Unipile account
|
|
260
|
+
unipile_account_status: string | null; // 'ok', 'pending', 'error', etc.
|
|
261
|
+
agent_id: ObjectId → Agent | null; // Reference to Agent model (ElevenLabs ID resolved through Agent record)
|
|
262
|
+
connected_at: Date | null; // When WhatsApp was connected
|
|
263
|
+
connected_phone: string | null; // The connected WhatsApp phone number
|
|
264
|
+
grace_period_seconds: number; // Default: 180 (3 minutes)
|
|
265
|
+
blacklisted_numbers: string[]; // Numbers excluded from AI
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Agent resolution**: The tenant no longer stores a raw ElevenLabs agent ID string. Instead, `agent_id` is an ObjectId reference to the `Agent` model. When an ElevenLabs agent ID is needed (e.g. for WebSocket connection), it is resolved via `resolveAgentId(agentId)` which fetches the Agent document, checks `is_active`, and returns the ElevenLabs ID. If the agent is inactive or not found, AI handoff is skipped.
|
|
270
|
+
|
|
271
|
+
**Feature flag**: The tenant must have `'whatsapp_agent'` in `enabled_features[]` for all `/whatsapp/*` routes to work. Checked via `requireFeature('whatsapp_agent')` middleware.
|
|
272
|
+
|
|
273
|
+
**Grace period range**: Clamped to 30–1800 seconds (30s min, 30min max) in the config endpoint.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## 6. Session Lifecycle
|
|
278
|
+
|
|
279
|
+
### 6.1 Complete State Machine
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
Contact sends message
|
|
283
|
+
│
|
|
284
|
+
▼
|
|
285
|
+
┌───────────────────────┐
|
|
286
|
+
│ Create Session │
|
|
287
|
+
│ status = 'waiting' │
|
|
288
|
+
│ grace_deadline = │
|
|
289
|
+
│ now + 180s │
|
|
290
|
+
└───────────┬───────────┘
|
|
291
|
+
│
|
|
292
|
+
┌───────────────────┼───────────────────┐
|
|
293
|
+
│ │ │
|
|
294
|
+
Human replies Grace period Dashboard user
|
|
295
|
+
in WhatsApp expires clicks "Devral"
|
|
296
|
+
│ │ │
|
|
297
|
+
▼ ▼ ▼
|
|
298
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
|
|
299
|
+
│ RESOLVED │ │ ACTIVE │ │ ACTIVE │
|
|
300
|
+
│ resolved_by │ │ (AI mode) │ │ (Human mode) │
|
|
301
|
+
│ = 'human' │ │ no takeover │ │ taken_over_by │
|
|
302
|
+
└──────────────┘ │ fields set │ │ = userId │
|
|
303
|
+
└──────┬───────┘ └──────┬───────────┘
|
|
304
|
+
│ │
|
|
305
|
+
Contact sends Dashboard user
|
|
306
|
+
more messages clicks "AI'ya Birak"
|
|
307
|
+
│ │
|
|
308
|
+
▼ ▼
|
|
309
|
+
AI responds via ┌──────────────────┐
|
|
310
|
+
EL WebSocket → │ Release dialog: │
|
|
311
|
+
Unipile sends │ 1. "Hemen Yanit" │
|
|
312
|
+
to WhatsApp │ 2. "Mesaj Bekle" │
|
|
313
|
+
│ └──────┬───────────┘
|
|
314
|
+
│ │
|
|
315
|
+
┌──────┴──────┐ ┌──────┴──────┐
|
|
316
|
+
│ Human │ │ immediate │
|
|
317
|
+
│ replies in │ │ = true │
|
|
318
|
+
│ WhatsApp │ │ │
|
|
319
|
+
│ (is_sender) │ │ Replay │
|
|
320
|
+
└──────┬──────┘ │ pending │
|
|
321
|
+
│ │ messages │
|
|
322
|
+
▼ └─────────────┘
|
|
323
|
+
┌──────────────┐
|
|
324
|
+
│ taken_over_at│
|
|
325
|
+
│ = now │
|
|
326
|
+
│ EL WS closed │
|
|
327
|
+
└──────────────┘
|
|
328
|
+
│
|
|
329
|
+
(stays active until
|
|
330
|
+
"Close" or new contact
|
|
331
|
+
message creates new
|
|
332
|
+
session after resolve)
|
|
333
|
+
|
|
334
|
+
─── Any state ───
|
|
335
|
+
│
|
|
336
|
+
"Close" button
|
|
337
|
+
│
|
|
338
|
+
▼
|
|
339
|
+
┌──────────────┐
|
|
340
|
+
│ RESOLVED │
|
|
341
|
+
│ resolved_by │
|
|
342
|
+
│ = 'manual' │
|
|
343
|
+
│ chat.is_ │
|
|
344
|
+
│ closed=true │
|
|
345
|
+
└──────────────┘
|
|
346
|
+
│
|
|
347
|
+
"Yeniden Ac" button
|
|
348
|
+
│
|
|
349
|
+
▼
|
|
350
|
+
chat.is_closed = false
|
|
351
|
+
(no new session until
|
|
352
|
+
contact sends message)
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### 6.2 Session Boundaries
|
|
356
|
+
|
|
357
|
+
A new session is created when:
|
|
358
|
+
1. **First-ever message** from a new contact (new chat + new session)
|
|
359
|
+
2. **Contact message arrives** and chat has **no active session** (previous session was resolved)
|
|
360
|
+
3. **Contact message arrives** and chat was **closed** (`is_closed = true` → flipped to `false`, new session created)
|
|
361
|
+
|
|
362
|
+
A session ends (resolves) when:
|
|
363
|
+
1. **Human responds** during grace period (via WhatsApp or dashboard send) → `resolved_by = 'human'`
|
|
364
|
+
2. **User clicks "Close"** in dashboard → `resolved_by = 'manual'`
|
|
365
|
+
3. **Grace period expires** but no ElevenLabs agent is configured → `resolved_by = 'manual'`
|
|
366
|
+
4. **No activity for 30 minutes** on an active session → `resolved_by = 'timeout'` (auto-resolved by inactivity sweeper)
|
|
367
|
+
|
|
368
|
+
A session does **NOT** end when:
|
|
369
|
+
- AI takes over (transitions to `active`)
|
|
370
|
+
- Human takes over via dashboard (stays `active`, `taken_over_by` set)
|
|
371
|
+
- Human replies directly in WhatsApp during AI mode (stays `active`, `taken_over_at` set)
|
|
372
|
+
- User releases back to AI (stays `active`, takeover fields cleared)
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## 7. Webhook Handler — Unipile
|
|
377
|
+
|
|
378
|
+
**File**: `src/modules/webhooks/unipile.routes.ts`
|
|
379
|
+
|
|
380
|
+
**Endpoint**: `POST /webhooks/unipile`
|
|
381
|
+
|
|
382
|
+
**Auth**: Optional shared secret via `x-unipile-secret` header.
|
|
383
|
+
|
|
384
|
+
**Response**: Always returns `200 { status: 'received' }` immediately. Processing happens asynchronously.
|
|
385
|
+
|
|
386
|
+
### 7.1 Pre-Processing Filters
|
|
387
|
+
|
|
388
|
+
Messages are discarded (not stored) if:
|
|
389
|
+
1. `event !== 'message_received'`
|
|
390
|
+
2. `account_type !== 'WHATSAPP'`
|
|
391
|
+
3. `message` is empty or whitespace
|
|
392
|
+
4. No tenant found for the `account_id` with `whatsapp_agent` feature enabled
|
|
393
|
+
5. Contact phone is not a valid phone number (filters out group chats)
|
|
394
|
+
|
|
395
|
+
Valid phone regex: `/^\+?\d{10,15}$/` (after stripping spaces, hyphens, parens)
|
|
396
|
+
|
|
397
|
+
### 7.2 New Chat Flow
|
|
398
|
+
|
|
399
|
+
When `unipile_chat_id` doesn't match any existing `WhatsAppChat`:
|
|
400
|
+
|
|
401
|
+
1. Create `WhatsAppChat` document
|
|
402
|
+
2. If contact is **blacklisted**: store message with `session_id: null`, return (no session)
|
|
403
|
+
3. Create `WhatsAppSession` (status=`waiting`, `grace_deadline`=now + grace period)
|
|
404
|
+
4. Set `chat.active_session_id` = new session ID
|
|
405
|
+
5. Store `WhatsAppMessage` with `session_id` = new session
|
|
406
|
+
6. Call `whatsappAgentService.startSession()` to begin grace period timer
|
|
407
|
+
|
|
408
|
+
### 7.3 Self-Sent Message Flow (`is_sender = true`)
|
|
409
|
+
|
|
410
|
+
When the webhook reports a message sent by the business (not received from contact):
|
|
411
|
+
|
|
412
|
+
1. Look up active session via `chat.active_session_id`
|
|
413
|
+
2. If session is `waiting` → **resolve as human** (cancel timer, `resolved_by = 'human'`). The session ID is preserved before resolution.
|
|
414
|
+
3. Check if message already exists in DB (dedup by `unipile_message_id`) — if sent via our API, it's already stored
|
|
415
|
+
4. Store the message with `session_id` of the resolved session (if just resolved) or the current active session (or `null` if no session)
|
|
416
|
+
5. If session is `active` with no takeover (AI was responding) → set `taken_over_at = now`, close ElevenLabs WebSocket
|
|
417
|
+
|
|
418
|
+
This handles the case where a human operator replies directly in WhatsApp (not through the dashboard), automatically switching from AI to human mode.
|
|
419
|
+
|
|
420
|
+
### 7.4 Incoming Contact Message Flow (`is_sender = false`)
|
|
421
|
+
|
|
422
|
+
When a contact sends a message:
|
|
423
|
+
|
|
424
|
+
1. **Blacklisted**: Store with `session_id: null`, no session/AI activity. If chat was closed, reopen it.
|
|
425
|
+
2. **Session is `waiting`**: Store message with `session_id`. Timer is still running. Multiple messages accumulate.
|
|
426
|
+
3. **Session is `active`**: Forward to `whatsappAgentService.handleIncomingMessage()` which stores the message and triggers AI (unless human-taken).
|
|
427
|
+
4. **No active session**: Create new session → store message → start grace period timer (same as new chat flow, but for existing chat)
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## 8. Agent Service — WhatsAppAgentService
|
|
432
|
+
|
|
433
|
+
**File**: `src/services/whatsapp-agent.service.ts`
|
|
434
|
+
|
|
435
|
+
**Singleton**: `const whatsappAgentService = new WhatsAppAgentService()` exported as default.
|
|
436
|
+
|
|
437
|
+
**State**: Three in-memory `Map` structures:
|
|
438
|
+
- `connections: Map<unipileChatId, ActiveConnection>` — Active ElevenLabs WebSocket connections
|
|
439
|
+
- `sessionTimers: Map<sessionId, NodeJS.Timeout>` — Grace period timers
|
|
440
|
+
- `messageBufferTimers: Map<sessionId, NodeJS.Timeout>` — 20-second message buffer debounce timers
|
|
441
|
+
|
|
442
|
+
### 8.1 ActiveConnection Interface
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
interface ActiveConnection {
|
|
446
|
+
ws: WebSocket; // ElevenLabs WebSocket
|
|
447
|
+
chatId: string; // MongoDB chat._id
|
|
448
|
+
sessionId: string; // MongoDB session._id
|
|
449
|
+
unipileChatId: string; // Unipile chat identifier
|
|
450
|
+
agentId: string; // ElevenLabs agent ID
|
|
451
|
+
lastActivity: number; // Date.now() of last activity
|
|
452
|
+
conversationId: string | null; // ElevenLabs conversation ID
|
|
453
|
+
ready: boolean; // true after init metadata received
|
|
454
|
+
suppressFirstResponse: boolean; // Suppresses agent greeting
|
|
455
|
+
reconnectAttempts: number; // Number of reconnect attempts so far (0 initially)
|
|
456
|
+
intentionallyClosed: boolean; // true when WS was closed on purpose (takeover, close, etc.)
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### 8.2 Public Methods
|
|
461
|
+
|
|
462
|
+
#### `initialize()`
|
|
463
|
+
Called on server startup. Sets up:
|
|
464
|
+
- Idle connection cleanup interval (every 60s)
|
|
465
|
+
- Stuck session sweeper interval (every 5min)
|
|
466
|
+
- Inactive session sweeper interval (every 5min) — auto-resolves sessions idle for 30min
|
|
467
|
+
- Recovers `waiting` sessions from DB (restarts timers or fires expired ones)
|
|
468
|
+
|
|
469
|
+
#### `startSession({ session, gracePeriodSeconds, agentId, tenantId })`
|
|
470
|
+
Creates an in-memory `setTimeout` for the grace period. When it fires, calls `onGracePeriodExpired()`.
|
|
471
|
+
|
|
472
|
+
#### `cancelSession(sessionId: string)`
|
|
473
|
+
Clears the in-memory timer for the given session.
|
|
474
|
+
|
|
475
|
+
#### `resolveSessionAsHuman(session: IWhatsAppSession)`
|
|
476
|
+
Cancels timer, sets session status to `resolved` with `resolved_by = 'human'`, clears `chat.active_session_id`.
|
|
477
|
+
|
|
478
|
+
#### `replayPendingMessages({ tenantId, agentId, session })`
|
|
479
|
+
Finds all `contact` messages in the session (by `session_id`), combines their text with newlines, and sends to AI as a single `user_message`. Used by:
|
|
480
|
+
- Grace period expiry (replay all buffered messages)
|
|
481
|
+
- "Hemen Yanit Ver" release option (replay messages accumulated during human takeover)
|
|
482
|
+
|
|
483
|
+
Returns `{ pendingCount: number }`.
|
|
484
|
+
|
|
485
|
+
#### `handleIncomingMessage({ tenantId, agentId, chat, session, messageText, senderName, unipileMessageId })`
|
|
486
|
+
Stores the incoming message, updates chat/session metadata, then:
|
|
487
|
+
- If session has `taken_over_by` or no `agentId` → skip AI response
|
|
488
|
+
- Otherwise → starts/resets a **20-second message buffer timer**. Messages are NOT sent to ElevenLabs immediately. Each new contact message resets the 20s timer. When the buffer expires, all unanswered messages since the last AI response are combined and sent as a single `user_message`. This prevents the AI from responding to each rapid-fire message separately.
|
|
489
|
+
|
|
490
|
+
#### `closeConnection(unipileChatId: string)`
|
|
491
|
+
Closes the ElevenLabs WebSocket for a chat and removes it from the connections map.
|
|
492
|
+
|
|
493
|
+
#### `getActiveConnectionCount(): number`
|
|
494
|
+
Returns the number of active ElevenLabs WebSocket connections.
|
|
495
|
+
|
|
496
|
+
#### `shutdown()`
|
|
497
|
+
Cleans up all timers, intervals, and WebSocket connections. Called on graceful server shutdown.
|
|
498
|
+
|
|
499
|
+
### 8.3 Private Methods
|
|
500
|
+
|
|
501
|
+
#### `onGracePeriodExpired(sessionId, agentId, tenantId)`
|
|
502
|
+
1. Fetch session from DB
|
|
503
|
+
2. If session is no longer `waiting` (e.g. human already responded) → return
|
|
504
|
+
3. If no `agentId` configured → resolve session as `manual`
|
|
505
|
+
4. Otherwise → set session status to `active`, call `replayPendingMessages()`
|
|
506
|
+
|
|
507
|
+
#### `recoverSessions()`
|
|
508
|
+
On startup, finds all sessions with `status: 'waiting'` in DB:
|
|
509
|
+
- If `grace_deadline` has already passed → immediately fire `onGracePeriodExpired`
|
|
510
|
+
- If `grace_deadline` is in the future → create a new `setTimeout` with remaining time
|
|
511
|
+
|
|
512
|
+
This handles server restarts without losing any sessions.
|
|
513
|
+
|
|
514
|
+
#### `sweepStuckSessions()`
|
|
515
|
+
Every 5 minutes, finds sessions where:
|
|
516
|
+
- `status: 'waiting'`
|
|
517
|
+
- `grace_deadline < now`
|
|
518
|
+
- No in-memory timer exists
|
|
519
|
+
|
|
520
|
+
For each stuck session, fires `onGracePeriodExpired`. This is a safety net for edge cases like timer GC or memory leaks.
|
|
521
|
+
|
|
522
|
+
#### `sweepInactiveSessions()`
|
|
523
|
+
Every 5 minutes, finds `active` sessions where no message activity has occurred for 30 minutes (`SESSION_INACTIVITY_TIMEOUT_MS`). For each inactive session:
|
|
524
|
+
1. Resolves the session with `resolved_by: 'timeout'`
|
|
525
|
+
2. Closes any active ElevenLabs WebSocket connection
|
|
526
|
+
3. Clears `chat.active_session_id`
|
|
527
|
+
4. Cancels any pending message buffer timer
|
|
528
|
+
|
|
529
|
+
This prevents abandoned sessions from lingering indefinitely.
|
|
530
|
+
|
|
531
|
+
#### `attemptReconnect(connection: ActiveConnection)`
|
|
532
|
+
Called when an ElevenLabs WebSocket closes unexpectedly (close code !== 1000) and `intentionallyClosed` is `false`. Uses exponential backoff:
|
|
533
|
+
- Attempt 1: 1.5s delay
|
|
534
|
+
- Attempt 2: 3s delay
|
|
535
|
+
- Attempt 3: 6s delay
|
|
536
|
+
- Max attempts: 3 (`MAX_RECONNECT_ATTEMPTS`)
|
|
537
|
+
|
|
538
|
+
On each attempt, creates a new WebSocket connection and replaces the old one in the connections map. If all attempts fail, the connection is removed.
|
|
539
|
+
|
|
540
|
+
#### `onMessageBufferExpired(sessionId: string)`
|
|
541
|
+
Fired when the 20-second message buffer timer expires. Collects all unanswered `contact` messages since the last AI response in the session, combines their text with newlines, and sends them as a single `user_message` to the ElevenLabs WebSocket. Removes the timer from `messageBufferTimers`.
|
|
542
|
+
|
|
543
|
+
#### `cancelMessageBuffer(sessionId: string)`
|
|
544
|
+
Cancels a pending message buffer timer for the given session, if one exists. Called during session resolution, takeover, and close operations.
|
|
545
|
+
|
|
546
|
+
#### `triggerAiResponse({ tenantId, agentId, chat, session, messageText })`
|
|
547
|
+
1. Check if an alive WebSocket connection exists for this chat
|
|
548
|
+
2. If not → create new connection via `createConnection()`
|
|
549
|
+
3. Update `elevenlabs_last_interaction_at` on session
|
|
550
|
+
4. If new connection → send chat context first
|
|
551
|
+
5. Send `user_message` with the message text
|
|
552
|
+
|
|
553
|
+
#### `handleIncomingMessage(...)` [internal AI trigger]
|
|
554
|
+
Same as `triggerAiResponse` but also checks `shouldStartNewConversation()` for conversation timeout.
|
|
555
|
+
|
|
556
|
+
#### `shouldStartNewConversation(session): boolean`
|
|
557
|
+
Returns `true` if:
|
|
558
|
+
- No `elevenlabs_conversation_id` on session
|
|
559
|
+
- No `elevenlabs_last_interaction_at`
|
|
560
|
+
- Last interaction was > 30 minutes ago
|
|
561
|
+
|
|
562
|
+
#### `createConnection(agentId, chat, session): Promise<ActiveConnection>`
|
|
563
|
+
1. Get signed URL from ElevenLabs
|
|
564
|
+
2. Open WebSocket with `?textOnly=true`
|
|
565
|
+
3. Wait up to 15 seconds for `conversation_initiation_metadata`
|
|
566
|
+
4. Store `conversation_id` on session in DB
|
|
567
|
+
5. Set up persistent `agent_response` handler:
|
|
568
|
+
- First response is **suppressed** (it's the agent greeting)
|
|
569
|
+
- Subsequent responses → `sendResponse()` → Unipile → WhatsApp
|
|
570
|
+
6. Return the `ActiveConnection` object
|
|
571
|
+
|
|
572
|
+
#### `sendResponse(chat, session, text)`
|
|
573
|
+
1. Send message via Unipile API
|
|
574
|
+
2. Store as `WhatsAppMessage` with `sender: 'human'`, `sender_name: 'AI'`, `sent_via_unipile: true`
|
|
575
|
+
3. Update chat and session atomically using `$inc` for `message_count` and `$set` for `last_message_at`/`last_message_preview` (prevents stale document overwrites from WebSocket closure captures)
|
|
576
|
+
|
|
577
|
+
#### `buildChatContext(sessionId): string | null`
|
|
578
|
+
Fetches last 20 messages in the session, formats as:
|
|
579
|
+
```
|
|
580
|
+
Previous conversation history:
|
|
581
|
+
Patient: ...
|
|
582
|
+
Operator: ...
|
|
583
|
+
Patient: ...
|
|
584
|
+
```
|
|
585
|
+
Sent to ElevenLabs via `contextual_update` message type so the AI has conversation context.
|
|
586
|
+
|
|
587
|
+
#### `cleanupIdle()`
|
|
588
|
+
Every 60 seconds, closes WebSocket connections where `lastActivity` is > 10 minutes old.
|
|
589
|
+
|
|
590
|
+
### 8.4 Standalone Helpers
|
|
591
|
+
|
|
592
|
+
#### `resolveAgentId(agentId: string): Promise<string | null>`
|
|
593
|
+
Takes an Agent model ObjectId, fetches the Agent document from MongoDB, checks `is_active`, and returns the ElevenLabs agent ID string. Returns `null` if the agent is not found or is inactive. Used throughout the service wherever an ElevenLabs agent ID is needed (grace period expiry, AI trigger, replay).
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## 9. API Routes
|
|
598
|
+
|
|
599
|
+
**File**: `src/modules/whatsapp/whatsapp.routes.ts`
|
|
600
|
+
|
|
601
|
+
**Prefix**: `/whatsapp`
|
|
602
|
+
|
|
603
|
+
**Auth**: All routes require JWT authentication + `whatsapp_agent` feature flag.
|
|
604
|
+
|
|
605
|
+
### 9.1 Connection Management
|
|
606
|
+
|
|
607
|
+
| Method | Path | Role | Description |
|
|
608
|
+
|--------|------|------|-------------|
|
|
609
|
+
| POST | `/connect` | admin | Creates Unipile WhatsApp account (returns QR code). Tries reconnect first. Migrates chats if account ID changes. |
|
|
610
|
+
| GET | `/connection-status` | any | Checks Unipile account status. Returns `{ connected, status, phone, agent_id, grace_period_seconds }`. |
|
|
611
|
+
| POST | `/disconnect` | admin | Disconnects Unipile account, clears tenant settings. |
|
|
612
|
+
| PUT | `/config` | admin | Updates `agent_id` (ObjectId reference to Agent model) and/or `grace_period_seconds`. Grace period clamped to 30–1800s. |
|
|
613
|
+
| POST | `/register-webhook` | admin | Registers a webhook URL with Unipile. |
|
|
614
|
+
| GET | `/agents` | any | Lists the tenant's agents for agent selection dropdown. Returns `[{ _id, name, is_active }]`. |
|
|
615
|
+
|
|
616
|
+
### 9.2 Blacklist Management
|
|
617
|
+
|
|
618
|
+
| Method | Path | Role | Description |
|
|
619
|
+
|--------|------|------|-------------|
|
|
620
|
+
| GET | `/blacklist` | admin, manager | Get blacklisted phone numbers. |
|
|
621
|
+
| POST | `/blacklist` | admin, manager | Add phone number. Body: `{ phone }`. |
|
|
622
|
+
| DELETE | `/blacklist/:phone` | admin, manager | Remove phone number from blacklist. |
|
|
623
|
+
|
|
624
|
+
### 9.3 Statistics
|
|
625
|
+
|
|
626
|
+
| Method | Path | Role | Description |
|
|
627
|
+
|--------|------|------|-------------|
|
|
628
|
+
| GET | `/stats` | any | Returns `{ totalChats, aiChats, humanChats, closedChats, waitingChats, totalMessages }`. |
|
|
629
|
+
|
|
630
|
+
Stats derivation:
|
|
631
|
+
- `totalChats` = all chats for tenant
|
|
632
|
+
- `closedChats` = chats with `is_closed: true`
|
|
633
|
+
- `waitingChats` = sessions with `status: 'waiting'`
|
|
634
|
+
- `humanChats` = sessions with active takeover + waiting sessions
|
|
635
|
+
- `aiChats` = active sessions without takeover
|
|
636
|
+
|
|
637
|
+
### 9.4 Chat Management
|
|
638
|
+
|
|
639
|
+
| Method | Path | Role | Description |
|
|
640
|
+
|--------|------|------|-------------|
|
|
641
|
+
| GET | `/chats` | any | Paginated list. Query: `page`, `limit`, `mode` (ai/human/closed). Uses aggregation pipeline for mode filtering. All chats enriched via `enrichChat()`. |
|
|
642
|
+
| GET | `/chats/:chatId` | any | Single chat detail, enriched. |
|
|
643
|
+
| GET | `/chats/:chatId/messages` | any | Paginated messages. Query: `page`, `limit`. Max 200 per page. |
|
|
644
|
+
| GET | `/chats/:chatId/sessions` | any | Paginated session history for a chat. Query: `page`, `limit`. Returns sessions sorted by `started_at` descending. |
|
|
645
|
+
|
|
646
|
+
### 9.5 Chat Actions
|
|
647
|
+
|
|
648
|
+
| Method | Path | Role | Body | Description |
|
|
649
|
+
|--------|------|------|------|-------------|
|
|
650
|
+
| POST | `/chats/:chatId/takeover` | admin, manager | — | Human takes over. Cancels grace timer if waiting, closes EL WebSocket, sets `taken_over_by` to current user. |
|
|
651
|
+
| POST | `/chats/:chatId/release` | admin, manager | `{ immediate?: boolean }` | Releases to AI. Clears takeover fields, resets EL conversation fields. If `immediate: true`, replays pending messages immediately. |
|
|
652
|
+
| POST | `/chats/:chatId/close` | admin, manager | — | Closes chat. Resolves active session (manual), closes EL WebSocket, sets `is_closed: true`. |
|
|
653
|
+
| POST | `/chats/:chatId/reopen` | admin, manager | — | Reopens chat. Sets `is_closed: false`. No new session until contact sends a message. |
|
|
654
|
+
| POST | `/chats/:chatId/send` | admin, manager | `{ text }` | Sends manual message via Unipile. Only works in human mode. If session is `waiting`, resolves it as human (cancels grace timer). Stores message with `session_id`. |
|
|
655
|
+
|
|
656
|
+
---
|
|
657
|
+
|
|
658
|
+
## 10. enrichChat — Backward Compatibility Layer
|
|
659
|
+
|
|
660
|
+
**File**: `src/modules/whatsapp/whatsapp.routes.ts` (top-level function)
|
|
661
|
+
|
|
662
|
+
The database stores `is_closed` and `active_session_id` on the chat, and all session state on `WhatsAppSession`. But the frontend expects flat fields like `mode`, `session_status`, `taken_over_by`.
|
|
663
|
+
|
|
664
|
+
`enrichChat()` looks up the active session and derives virtual fields:
|
|
665
|
+
|
|
666
|
+
| DB State | Derived `mode` | Derived `session_status` |
|
|
667
|
+
|----------|----------------|--------------------------|
|
|
668
|
+
| No active session + `is_closed: false` | `'ai'` | `'idle'` |
|
|
669
|
+
| No active session + `is_closed: true` | `'closed'` | `'idle'` |
|
|
670
|
+
| Session status `'waiting'` | `'human'` | `'waiting'` |
|
|
671
|
+
| Session status `'active'` + taken over | `'human'` | `'idle'` |
|
|
672
|
+
| Session status `'active'` + no takeover | `'ai'` | `'idle'` |
|
|
673
|
+
|
|
674
|
+
Also populates: `taken_over_by` (populated with `name`, `email`), `taken_over_at`, `session_started_at`, `session_grace_deadline`, `session_resolved_by`.
|
|
675
|
+
|
|
676
|
+
Removes internal fields: `active_session_id`, `is_closed`. Also removes aggregation pipeline temporary fields (`_session`, `_activeSession`, `_mode`) when present from mode-filtered queries.
|
|
677
|
+
|
|
678
|
+
For the `/chats` list endpoint with mode filtering, a MongoDB aggregation pipeline with `$lookup` and `$switch` replicates this logic at the database level.
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
## 11. Frontend
|
|
683
|
+
|
|
684
|
+
### 11.1 Layout Overview
|
|
685
|
+
|
|
686
|
+
The WhatsApp frontend uses a **two-panel WhatsApp Web-style layout**. Both `/whatsapp` and `/whatsapp/:chatId` render the same root component (`WhatsApp.tsx`). The old `WhatsAppChat.tsx` page has been deleted.
|
|
687
|
+
|
|
688
|
+
```
|
|
689
|
+
┌──────────────────────────────────────────────────────────┐
|
|
690
|
+
│ Left Panel (ChatList) │ Right Panel (Detail) │
|
|
691
|
+
│ │ │
|
|
692
|
+
│ ┌─ StatsBar ─────────────────┐ │ ┌─ ChatHeader ──────┐ │
|
|
693
|
+
│ │ Total | AI | Human | Wait │ │ │ Name, phone, mode │ │
|
|
694
|
+
│ └────────────────────────────┘ │ │ Actions │ │
|
|
695
|
+
│ ┌─ ModeFilter ───────────────┐ │ └────────────────────┘ │
|
|
696
|
+
│ │ All / AI / Human / Closed │ │ ┌─ Tabs ─────────────┐ │
|
|
697
|
+
│ └────────────────────────────┘ │ │ Mesajlar | Oturum │ │
|
|
698
|
+
│ ┌─ SearchBar ────────────────┐ │ │ Gecmisi │ │
|
|
699
|
+
│ │ 🔍 Search contacts... │ │ └────────────────────┘ │
|
|
700
|
+
│ └────────────────────────────┘ │ ┌─ MessageList ──────┐ │
|
|
701
|
+
│ ┌─ ChatListItem ─────────────┐ │ │ Session dividers │ │
|
|
702
|
+
│ │ Contact name, preview, │ │ │ Message bubbles │ │
|
|
703
|
+
│ │ time, mode badge │ │ │ (color-coded) │ │
|
|
704
|
+
│ ├────────────────────────────┤ │ └────────────────────┘ │
|
|
705
|
+
│ │ ChatListItem... │ │ ┌─ MessageInput ─────┐ │
|
|
706
|
+
│ │ ChatListItem... │ │ │ Text input + send │ │
|
|
707
|
+
│ └────────────────────────────┘ │ └────────────────────┘ │
|
|
708
|
+
│ │ │
|
|
709
|
+
│ ⚙️ Settings button → Dialog │ │
|
|
710
|
+
└──────────────────────────────────────────────────────────┘
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
**Left panel**: Chat list with stats bar, mode filter (All/AI/Human/Closed), search bar, and scrollable chat list items. Settings (connection, agent selection, grace period, blacklist) are accessed via a settings dialog.
|
|
714
|
+
|
|
715
|
+
**Right panel**: Selected chat detail with a header showing contact info and action buttons, tabbed content (Mesajlar for messages, Oturum Gecmisi for session history), message stream with session dividers, and a message input bar.
|
|
716
|
+
|
|
717
|
+
### 11.2 Components (`web/src/components/whatsapp/`)
|
|
718
|
+
|
|
719
|
+
10 new components power the two-panel layout:
|
|
720
|
+
|
|
721
|
+
| Component | Purpose |
|
|
722
|
+
|-----------|---------|
|
|
723
|
+
| `ChatList.tsx` | Left panel container: stats bar, filters, search, scrollable chat items |
|
|
724
|
+
| `ChatListItem.tsx` | Single chat row: contact name, preview, timestamp, mode badge |
|
|
725
|
+
| `ChatHeader.tsx` | Right panel header: contact info, mode badge, action buttons (Takeover/Release/Close/Reopen) |
|
|
726
|
+
| `MessageList.tsx` | Scrollable message stream with session dividers between sessions |
|
|
727
|
+
| `MessageBubble.tsx` | Single message bubble, color-coded by sender (gray=contact, green=AI, blue=human) |
|
|
728
|
+
| `MessageInput.tsx` | Text input bar with send button (only active in human mode) |
|
|
729
|
+
| `SessionDivider.tsx` | Visual separator between sessions in the message stream |
|
|
730
|
+
| `SessionHistory.tsx` | "Oturum Gecmisi" tab: paginated session list with status, duration, resolved_by |
|
|
731
|
+
| `StatsBar.tsx` | Compact stats row: Total, AI Active, Human Active, Waiting, Closed |
|
|
732
|
+
| `SettingsDialog.tsx` | Dialog for connection management, agent selection dropdown, grace period, blacklist |
|
|
733
|
+
|
|
734
|
+
### 11.3 Routing
|
|
735
|
+
|
|
736
|
+
Both routes render the same component:
|
|
737
|
+
- `/whatsapp` — shows left panel only (or left + empty right panel on wide screens)
|
|
738
|
+
- `/whatsapp/:chatId` — shows left panel + right panel with selected chat
|
|
739
|
+
|
|
740
|
+
### 11.4 Frontend Types (`web/src/types/index.ts`)
|
|
741
|
+
|
|
742
|
+
```typescript
|
|
743
|
+
export type ChatMode = 'ai' | 'human' | 'closed';
|
|
744
|
+
export type MessageSender = 'contact' | 'ai' | 'human';
|
|
745
|
+
export type SessionStatus = 'idle' | 'waiting' | 'resolved';
|
|
746
|
+
|
|
747
|
+
export interface WhatsAppChat {
|
|
748
|
+
_id: string;
|
|
749
|
+
tenant_id: string;
|
|
750
|
+
unipile_chat_id: string;
|
|
751
|
+
contact_name: string;
|
|
752
|
+
contact_phone: string;
|
|
753
|
+
mode: ChatMode; // Derived by enrichChat()
|
|
754
|
+
taken_over_by: { _id: string; name: string; email: string } | null;
|
|
755
|
+
taken_over_at: string | null;
|
|
756
|
+
last_message_at: string;
|
|
757
|
+
last_message_preview: string;
|
|
758
|
+
message_count: number;
|
|
759
|
+
session_status: SessionStatus; // Derived by enrichChat()
|
|
760
|
+
session_started_at: string | null;
|
|
761
|
+
session_grace_deadline: string | null;
|
|
762
|
+
session_resolved_by: string | null;
|
|
763
|
+
createdAt: string;
|
|
764
|
+
updatedAt: string;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
export interface WhatsAppMessage {
|
|
768
|
+
_id: string;
|
|
769
|
+
chat_id: string;
|
|
770
|
+
session_id: string | null; // Which session this message belongs to
|
|
771
|
+
sender: MessageSender;
|
|
772
|
+
sender_name: string;
|
|
773
|
+
text: string;
|
|
774
|
+
sent_via_unipile: boolean;
|
|
775
|
+
createdAt: string;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
export interface WhatsAppSession {
|
|
779
|
+
_id: string;
|
|
780
|
+
chat_id: string;
|
|
781
|
+
tenant_id: string;
|
|
782
|
+
status: 'waiting' | 'active' | 'resolved';
|
|
783
|
+
resolved_by: 'human' | 'ai_timeout' | 'manual' | 'timeout' | null;
|
|
784
|
+
started_at: string;
|
|
785
|
+
resolved_at: string | null;
|
|
786
|
+
grace_deadline: string | null;
|
|
787
|
+
taken_over_by: string | null;
|
|
788
|
+
taken_over_at: string | null;
|
|
789
|
+
message_count: number;
|
|
790
|
+
createdAt: string;
|
|
791
|
+
updatedAt: string;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export interface WhatsAppAgentSettings {
|
|
795
|
+
agent_id: string | null; // ObjectId reference to Agent model
|
|
796
|
+
grace_period_seconds: number;
|
|
797
|
+
// ... other fields
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export interface WhatsAppStats {
|
|
801
|
+
totalChats: number;
|
|
802
|
+
aiChats: number;
|
|
803
|
+
humanChats: number;
|
|
804
|
+
closedChats: number;
|
|
805
|
+
waitingChats: number;
|
|
806
|
+
totalMessages: number;
|
|
807
|
+
}
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
### 11.5 Status Badges (`web/src/components/status-badge.tsx`)
|
|
811
|
+
|
|
812
|
+
- **ChatModeBadge**: `ai` → green "AI", `human` → amber "Insan", `closed` → gray "Kapali"
|
|
813
|
+
- **SessionStatusBadge**: Only renders for `'waiting'` → blue "Bekleniyor". Returns `null` for all other values.
|
|
814
|
+
|
|
815
|
+
---
|
|
816
|
+
|
|
817
|
+
## 12. Blacklist System
|
|
818
|
+
|
|
819
|
+
Blacklisted numbers are stored in `Tenant.settings.whatsapp_agent.blacklisted_numbers[]`.
|
|
820
|
+
|
|
821
|
+
**Behavior for blacklisted contacts**:
|
|
822
|
+
1. Messages are **always stored** in DB (for audit/visibility)
|
|
823
|
+
2. Messages have `session_id: null` — no session is ever created
|
|
824
|
+
3. No grace period timer, no AI response
|
|
825
|
+
4. Chat appears in the dashboard, but in a passive state
|
|
826
|
+
5. If a blacklisted chat was closed, incoming messages reopen it (`is_closed = false`)
|
|
827
|
+
6. Blacklist is checked by exact phone number match
|
|
828
|
+
|
|
829
|
+
**Management**:
|
|
830
|
+
- Can be managed via API (`GET/POST/DELETE /whatsapp/blacklist`)
|
|
831
|
+
- Can be managed from the chat list UI (ban icon per row)
|
|
832
|
+
- No limit on number of blacklisted numbers
|
|
833
|
+
|
|
834
|
+
---
|
|
835
|
+
|
|
836
|
+
## 13. Timers, Recovery & Safety Nets
|
|
837
|
+
|
|
838
|
+
### 13.1 In-Memory Timers
|
|
839
|
+
|
|
840
|
+
| Timer | Interval | Purpose |
|
|
841
|
+
|-------|----------|---------|
|
|
842
|
+
| Grace period timer | Per session (e.g. 180s) | Fires `onGracePeriodExpired()` when human doesn't respond |
|
|
843
|
+
| Message buffer timer | 20 seconds (per session, resets on each new message) | Debounces rapid-fire contact messages before sending to AI |
|
|
844
|
+
| Idle cleanup | Every 60 seconds | Closes EL WebSocket connections idle > 10 minutes |
|
|
845
|
+
| Stuck session sweep | Every 5 minutes | Finds sessions stuck in `waiting` past deadline with no timer |
|
|
846
|
+
| Inactive session sweep | Every 5 minutes | Auto-resolves `active` sessions with no activity for 30 minutes (`resolved_by: 'timeout'`) |
|
|
847
|
+
| Reconnect backoff | 1.5s / 3s / 6s (exponential) | Retries EL WebSocket connection on unexpected close (max 3 attempts) |
|
|
848
|
+
| Cleanup interval | Every 60 seconds | Same as idle cleanup |
|
|
849
|
+
|
|
850
|
+
### 13.2 Startup Recovery
|
|
851
|
+
|
|
852
|
+
When the server starts (`initialize()`):
|
|
853
|
+
1. Queries all `WhatsAppSession` documents with `status: 'waiting'`
|
|
854
|
+
2. For each:
|
|
855
|
+
- If `grace_deadline` has already passed → immediately fire `onGracePeriodExpired`
|
|
856
|
+
- If `grace_deadline` is in the future → create a new `setTimeout` with remaining milliseconds
|
|
857
|
+
3. Logs count of recovered sessions
|
|
858
|
+
|
|
859
|
+
This ensures no sessions are lost across server restarts.
|
|
860
|
+
|
|
861
|
+
### 13.3 Stuck Session Sweeper
|
|
862
|
+
|
|
863
|
+
Every 5 minutes, runs a MongoDB query:
|
|
864
|
+
```javascript
|
|
865
|
+
WhatsAppSession.find({
|
|
866
|
+
status: 'waiting',
|
|
867
|
+
grace_deadline: { $lt: new Date() }
|
|
868
|
+
})
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
For each result, checks if an in-memory timer exists. If not, fires `onGracePeriodExpired`. This catches edge cases where:
|
|
872
|
+
- Timer was garbage collected
|
|
873
|
+
- Server memory pressure caused timer loss
|
|
874
|
+
- Race condition between timer creation and webhook processing
|
|
875
|
+
|
|
876
|
+
### 13.4 WebSocket Lifecycle
|
|
877
|
+
|
|
878
|
+
- **15-second init timeout**: If ElevenLabs WebSocket doesn't send `conversation_initiation_metadata` within 15s, the connection is rejected
|
|
879
|
+
- **10-minute idle timeout**: Connections with no activity for 10 minutes are automatically closed
|
|
880
|
+
- **30-minute conversation timeout**: If `elevenlabs_last_interaction_at` is > 30 minutes old, a new conversation is started (new WebSocket connection)
|
|
881
|
+
- **First response suppression**: The ElevenLabs agent sends a greeting on connection. This is suppressed (not forwarded to WhatsApp) because it would be contextually inappropriate.
|
|
882
|
+
- **Automatic reconnection**: When the ElevenLabs WebSocket closes unexpectedly (close code !== 1000) and `intentionallyClosed` is `false`, the system automatically attempts reconnection with exponential backoff (1.5s, 3s, 6s — max 3 attempts). All intentional close paths (takeover, session close, manual disconnect) set `intentionallyClosed = true` on the connection before closing to prevent unwanted reconnection.
|
|
883
|
+
|
|
884
|
+
---
|
|
885
|
+
|
|
886
|
+
## 14. ElevenLabs WebSocket Protocol
|
|
887
|
+
|
|
888
|
+
### 14.1 Connection Setup
|
|
889
|
+
|
|
890
|
+
```
|
|
891
|
+
1. GET signed URL via elevenlabsService.getSignedUrl({ agentId })
|
|
892
|
+
2. Append ?textOnly=true to URL
|
|
893
|
+
3. Open WebSocket
|
|
894
|
+
4. Wait for: { "type": "conversation_initiation_metadata" }
|
|
895
|
+
5. Extract conversation_id
|
|
896
|
+
6. Mark connection as ready
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
### 14.2 Sending Messages
|
|
900
|
+
|
|
901
|
+
**Context (sent once per new connection)**:
|
|
902
|
+
```json
|
|
903
|
+
{
|
|
904
|
+
"type": "contextual_update",
|
|
905
|
+
"text": "Previous conversation history:\nPatient: Merhaba, randevu almak istiyorum\nOperator: Tabii, hangi doktor icin?"
|
|
906
|
+
}
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
**User message**:
|
|
910
|
+
```json
|
|
911
|
+
{
|
|
912
|
+
"type": "user_message",
|
|
913
|
+
"text": "Yarin saat 10'da musait misiniz?"
|
|
914
|
+
}
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
### 14.3 Receiving Responses
|
|
918
|
+
|
|
919
|
+
```json
|
|
920
|
+
{
|
|
921
|
+
"type": "agent_response",
|
|
922
|
+
"agent_response_event": {
|
|
923
|
+
"agent_response": "Evet, yarin saat 10:00 icin randevunuzu olusturuyorum."
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
**Response handling**:
|
|
929
|
+
1. First `agent_response` → suppressed (`suppressFirstResponse` flag)
|
|
930
|
+
2. Subsequent responses → `sendResponse()` → Unipile → WhatsApp
|
|
931
|
+
|
|
932
|
+
### 14.4 Connection Reuse & Reconnection
|
|
933
|
+
|
|
934
|
+
- One WebSocket per `unipile_chat_id` (not per session)
|
|
935
|
+
- If a connection already exists and is alive + not timed out → reuse it
|
|
936
|
+
- If connection is dead or conversation timed out → create new one
|
|
937
|
+
- On unexpected close (code !== 1000, `intentionallyClosed` = false) → `attemptReconnect()` with exponential backoff
|
|
938
|
+
- On intentional close → removed from connections map, no reconnection attempted
|
|
939
|
+
|
|
940
|
+
---
|
|
941
|
+
|
|
942
|
+
## 15. Migration
|
|
943
|
+
|
|
944
|
+
### 15.1 Session Migration — `scripts/migrate-sessions.ts`
|
|
945
|
+
|
|
946
|
+
Migrates from the old chat-centric architecture (session fields embedded on `WhatsAppChat`) to the session-centric architecture (`WhatsAppSession` documents).
|
|
947
|
+
|
|
948
|
+
**Usage**:
|
|
949
|
+
```bash
|
|
950
|
+
npx tsx scripts/migrate-sessions.ts --dry-run # Preview changes
|
|
951
|
+
npx tsx scripts/migrate-sessions.ts # Execute migration
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
**What it does**:
|
|
955
|
+
1. Reads all chats from `whatsappchats` collection (raw MongoDB, not Mongoose)
|
|
956
|
+
2. For each chat with old session fields:
|
|
957
|
+
- Creates a `WhatsAppSession` document with mapped fields
|
|
958
|
+
- Backfills `session_id` on `WhatsAppMessage` documents created since session start
|
|
959
|
+
- Updates `message_count` on the new session
|
|
960
|
+
3. Updates each chat:
|
|
961
|
+
- Sets `is_closed` (true if old `mode` was `'closed'`)
|
|
962
|
+
- Sets `active_session_id` (if session is not resolved)
|
|
963
|
+
- `$unset` removes 10 old fields: `mode`, `session_status`, `session_started_at`, `session_grace_deadline`, `session_resolved_by`, `taken_over_by`, `taken_over_at`, `elevenlabs_conversation_id`, `elevenlabs_conversation_created_at`, `elevenlabs_last_interaction_at`
|
|
964
|
+
|
|
965
|
+
**Idempotent**: Skips chats that already have `active_session_id` set.
|
|
966
|
+
|
|
967
|
+
### 15.2 Agent Reference Migration — `scripts/migrate-agent-ref.ts`
|
|
968
|
+
|
|
969
|
+
Migrates the old `elevenlabs_agent_id` string in tenant WhatsApp settings to the new `agent_id` ObjectId reference to the Agent model.
|
|
970
|
+
|
|
971
|
+
**Usage**:
|
|
972
|
+
```bash
|
|
973
|
+
npx tsx scripts/migrate-agent-ref.ts --dry-run # Preview changes
|
|
974
|
+
npx tsx scripts/migrate-agent-ref.ts # Execute migration
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
**What it does**:
|
|
978
|
+
1. Finds all tenants that have `settings.whatsapp_agent.elevenlabs_agent_id` set
|
|
979
|
+
2. For each tenant, looks up the Agent document by matching the ElevenLabs agent ID
|
|
980
|
+
3. Sets `settings.whatsapp_agent.agent_id` to the Agent's ObjectId
|
|
981
|
+
4. Removes the old `settings.whatsapp_agent.elevenlabs_agent_id` field
|
|
982
|
+
|
|
983
|
+
**Idempotent**: Skips tenants that already have `agent_id` set or where the old field is absent.
|
|
984
|
+
|
|
985
|
+
---
|
|
986
|
+
|
|
987
|
+
## 16. File Map
|
|
988
|
+
|
|
989
|
+
```
|
|
990
|
+
src/
|
|
991
|
+
├── modules/
|
|
992
|
+
│ ├── whatsapp/
|
|
993
|
+
│ │ ├── whatsapp-chat.model.ts # WhatsAppChat Mongoose model
|
|
994
|
+
│ │ ├── whatsapp-session.model.ts # WhatsAppSession Mongoose model
|
|
995
|
+
│ │ ├── whatsapp-message.model.ts # WhatsAppMessage Mongoose model
|
|
996
|
+
│ │ └── whatsapp.routes.ts # All /whatsapp/* API routes + enrichChat()
|
|
997
|
+
│ └── webhooks/
|
|
998
|
+
│ └── unipile.routes.ts # POST /webhooks/unipile handler
|
|
999
|
+
├── services/
|
|
1000
|
+
│ ├── whatsapp-agent.service.ts # Session timers, EL WebSocket, AI orchestration
|
|
1001
|
+
│ ├── unipile.service.ts # Unipile REST API client
|
|
1002
|
+
│ └── elevenlabs.service.ts # ElevenLabs SDK wrapper (getSignedUrl)
|
|
1003
|
+
│
|
|
1004
|
+
web/src/
|
|
1005
|
+
├── pages/
|
|
1006
|
+
│ └── WhatsApp.tsx # Two-panel layout (both /whatsapp and /whatsapp/:chatId)
|
|
1007
|
+
├── components/
|
|
1008
|
+
│ ├── whatsapp/
|
|
1009
|
+
│ │ ├── ChatList.tsx # Left panel: stats, filters, search, chat items
|
|
1010
|
+
│ │ ├── ChatListItem.tsx # Single chat row in the list
|
|
1011
|
+
│ │ ├── ChatHeader.tsx # Right panel header: contact info, actions
|
|
1012
|
+
│ │ ├── MessageList.tsx # Scrollable message stream with session dividers
|
|
1013
|
+
│ │ ├── MessageBubble.tsx # Single message bubble (color-coded)
|
|
1014
|
+
│ │ ├── MessageInput.tsx # Text input bar + send button
|
|
1015
|
+
│ │ ├── SessionDivider.tsx # Visual separator between sessions
|
|
1016
|
+
│ │ ├── SessionHistory.tsx # "Oturum Gecmisi" tab content
|
|
1017
|
+
│ │ ├── StatsBar.tsx # Compact stats row
|
|
1018
|
+
│ │ └── SettingsDialog.tsx # Connection, agent, grace period, blacklist settings
|
|
1019
|
+
│ └── status-badge.tsx # ChatModeBadge, SessionStatusBadge
|
|
1020
|
+
└── types/
|
|
1021
|
+
└── index.ts # WhatsAppChat, WhatsAppMessage, WhatsAppSession, WhatsAppStats types
|
|
1022
|
+
|
|
1023
|
+
scripts/
|
|
1024
|
+
├── migrate-sessions.ts # Old → new session architecture migration
|
|
1025
|
+
└── migrate-agent-ref.ts # elevenlabs_agent_id → agent_id (ObjectId) migration
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
---
|
|
1029
|
+
|
|
1030
|
+
## 17. Constants & Timeouts
|
|
1031
|
+
|
|
1032
|
+
| Constant | Value | Location | Purpose |
|
|
1033
|
+
|----------|-------|----------|---------|
|
|
1034
|
+
| Default grace period | 180 seconds (3 min) | Tenant settings | Time before AI takes over |
|
|
1035
|
+
| Min grace period | 30 seconds | whatsapp.routes.ts config endpoint | Lower bound |
|
|
1036
|
+
| Max grace period | 1800 seconds (30 min) | whatsapp.routes.ts config endpoint | Upper bound |
|
|
1037
|
+
| MESSAGE_BUFFER_MS | 20 seconds | whatsapp-agent.service.ts | Debounce window for rapid-fire contact messages before sending to AI |
|
|
1038
|
+
| SESSION_INACTIVITY_TIMEOUT_MS | 30 minutes | whatsapp-agent.service.ts | Auto-resolve active sessions with no activity |
|
|
1039
|
+
| MAX_RECONNECT_ATTEMPTS | 3 | whatsapp-agent.service.ts | Max EL WebSocket reconnection attempts on unexpected close |
|
|
1040
|
+
| RECONNECT_BASE_DELAY_MS | 1500 ms (1.5 seconds) | whatsapp-agent.service.ts | Base delay for exponential backoff (1.5s, 3s, 6s) |
|
|
1041
|
+
| Conversation timeout | 30 minutes | whatsapp-agent.service.ts | If EL conversation idle > 30min, start new one |
|
|
1042
|
+
| Idle connection timeout | 10 minutes | whatsapp-agent.service.ts | Close EL WebSocket after 10min idle |
|
|
1043
|
+
| WS init timeout | 15 seconds | whatsapp-agent.service.ts | Reject EL WebSocket if no init in 15s |
|
|
1044
|
+
| Idle cleanup interval | 60 seconds | whatsapp-agent.service.ts | How often to check for idle connections |
|
|
1045
|
+
| Stuck session sweep interval | 5 minutes | whatsapp-agent.service.ts | How often to check for stuck sessions |
|
|
1046
|
+
| Inactive session sweep interval | 5 minutes | whatsapp-agent.service.ts | How often to check for inactive sessions to auto-resolve |
|
|
1047
|
+
| Chat context messages | 20 messages | whatsapp-agent.service.ts | Max messages sent to EL as context |
|
|
1048
|
+
| Message preview length | 100 characters | webhook + routes | Truncated for `last_message_preview` |
|
|
1049
|
+
| Frontend poll interval | 3 seconds | WhatsApp.tsx | How often to refresh messages |
|
|
1050
|
+
| Frontend list poll interval | 5 seconds | WhatsApp.tsx | How often to refresh chat list |
|
|
1051
|
+
| Max messages per page | 200 | whatsapp.routes.ts | Limit on messages endpoint |
|
|
1052
|
+
| Max chats per page | 100 | whatsapp.routes.ts | Limit on chats endpoint |
|
|
1053
|
+
|
|
1054
|
+
---
|
|
1055
|
+
|
|
1056
|
+
## 18. Decision Tables
|
|
1057
|
+
|
|
1058
|
+
### 18.1 Incoming Contact Message → What Happens?
|
|
1059
|
+
|
|
1060
|
+
| Blacklisted? | Session Exists? | Session Status | Action |
|
|
1061
|
+
|-------------|-----------------|----------------|--------|
|
|
1062
|
+
| Yes | — | — | Store message (no session_id), no AI |
|
|
1063
|
+
| No | No | — | Create session (waiting), start timer |
|
|
1064
|
+
| No | Yes | `waiting` | Store message in session, timer keeps running |
|
|
1065
|
+
| No | Yes | `active` + no takeover | Forward to AI via ElevenLabs WebSocket |
|
|
1066
|
+
| No | Yes | `active` + taken over | Store message in session, skip AI |
|
|
1067
|
+
| No | Yes | `resolved` | Clear stale ref, create new session |
|
|
1068
|
+
|
|
1069
|
+
### 18.2 Self-Sent Message → What Happens?
|
|
1070
|
+
|
|
1071
|
+
| Session Exists? | Session Status | Action |
|
|
1072
|
+
|-----------------|----------------|--------|
|
|
1073
|
+
| Yes | `waiting` | Resolve session as human, cancel timer |
|
|
1074
|
+
| Yes | `active` + AI mode | Set `taken_over_at`, close EL WebSocket |
|
|
1075
|
+
| Yes | `active` + already human | Store message only |
|
|
1076
|
+
| No | — | Store message (no session_id) |
|
|
1077
|
+
|
|
1078
|
+
### 18.3 Dashboard Action → What Happens?
|
|
1079
|
+
|
|
1080
|
+
| Action | Precondition | Session Changes | Side Effects |
|
|
1081
|
+
|--------|-------------|-----------------|-------------|
|
|
1082
|
+
| **Takeover** | Active session exists | If waiting: cancel timer, → active. Set `taken_over_by/at`. | Close EL WebSocket |
|
|
1083
|
+
| **Release** | Human-taken session | Clear takeover fields, reset EL fields. | If immediate: replay pending messages |
|
|
1084
|
+
| **Close** | Any | Resolve session (manual). Set `chat.is_closed = true`. | Close EL WebSocket, cancel timer |
|
|
1085
|
+
| **Reopen** | Chat is closed | — | Set `chat.is_closed = false` |
|
|
1086
|
+
| **Send** | Human mode | If waiting: resolve as human, cancel grace timer | Send via Unipile, store in DB |
|