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,2055 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const mongoose_1 = __importDefault(require("mongoose"));
|
|
7
|
+
const ws_1 = __importDefault(require("ws"));
|
|
8
|
+
const elevenlabs_service_1 = __importDefault(require("./elevenlabs.service"));
|
|
9
|
+
const unipile_service_1 = __importDefault(require("./unipile.service"));
|
|
10
|
+
const whatsapp_chat_model_1 = __importDefault(require("../modules/whatsapp/whatsapp-chat.model"));
|
|
11
|
+
const whatsapp_session_model_1 = __importDefault(require("../modules/whatsapp/whatsapp-session.model"));
|
|
12
|
+
const whatsapp_message_model_1 = __importDefault(require("../modules/whatsapp/whatsapp-message.model"));
|
|
13
|
+
const appointment_model_1 = __importDefault(require("../modules/whatsapp/appointment.model"));
|
|
14
|
+
const tenant_model_1 = __importDefault(require("../modules/tenants/tenant.model"));
|
|
15
|
+
const agent_model_1 = __importDefault(require("../modules/agents/agent.model"));
|
|
16
|
+
const whatsapp_contact_profile_model_1 = __importDefault(require("../modules/whatsapp/whatsapp-contact-profile.model"));
|
|
17
|
+
const operator_request_model_1 = __importDefault(require("../modules/whatsapp/operator-request.model"));
|
|
18
|
+
const google_calendar_service_1 = __importDefault(require("./google-calendar.service"));
|
|
19
|
+
const netgsm_service_1 = __importDefault(require("./netgsm.service"));
|
|
20
|
+
const lead_model_1 = __importDefault(require("../modules/leads/lead.model"));
|
|
21
|
+
const ai_1 = require("./ai");
|
|
22
|
+
const openai_1 = __importDefault(require("openai"));
|
|
23
|
+
const config_1 = __importDefault(require("../config"));
|
|
24
|
+
const logger_1 = __importDefault(require("../utils/logger"));
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the active ElevenLabs agent ID for a tenant's WhatsApp agent.
|
|
27
|
+
* Returns the elevenlabs_agent_id string if the agent exists and is active, or '' otherwise.
|
|
28
|
+
*/
|
|
29
|
+
async function resolveAgentId(tenant) {
|
|
30
|
+
const agentObjId = tenant.settings?.whatsapp_agent?.agent_id;
|
|
31
|
+
if (!agentObjId)
|
|
32
|
+
return '';
|
|
33
|
+
const agent = await agent_model_1.default.findById(agentObjId).lean();
|
|
34
|
+
if (!agent || !agent.is_active)
|
|
35
|
+
return '';
|
|
36
|
+
return agent.elevenlabs_agent_id;
|
|
37
|
+
}
|
|
38
|
+
// Keepalive ping interval to prevent EL from dropping idle connections
|
|
39
|
+
const WS_PING_INTERVAL_MS = 30_000;
|
|
40
|
+
// Auto-resolve active sessions with no activity after this duration
|
|
41
|
+
const SESSION_INACTIVITY_TIMEOUT_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
42
|
+
// Wait this long after the last contact message before sending to AI (debounce)
|
|
43
|
+
const MESSAGE_BUFFER_MIN_MS = 10_000; // 10 seconds
|
|
44
|
+
const MESSAGE_BUFFER_MAX_MS = 50_000; // 50 seconds
|
|
45
|
+
class WhatsAppAgentService {
|
|
46
|
+
connections = new Map(); // key: unipileChatId
|
|
47
|
+
sessionTimers = new Map(); // key: sessionId
|
|
48
|
+
messageBufferTimers = new Map(); // key: sessionId
|
|
49
|
+
idleTimeoutMs = 2 * 60 * 60 * 1000; // 2 hours (matches session inactivity timeout)
|
|
50
|
+
cleanupInterval = null;
|
|
51
|
+
sweepInterval = null;
|
|
52
|
+
inactivitySweepInterval = null;
|
|
53
|
+
costSweepInterval = null;
|
|
54
|
+
async initialize() {
|
|
55
|
+
this.cleanupInterval = setInterval(() => this.cleanupIdle(), 60_000);
|
|
56
|
+
this.sweepInterval = setInterval(() => {
|
|
57
|
+
this.sweepStuckSessions().catch(err => {
|
|
58
|
+
logger_1.default.error('Stuck session sweep failed', { error: err.message });
|
|
59
|
+
});
|
|
60
|
+
}, 5 * 60_000);
|
|
61
|
+
this.inactivitySweepInterval = setInterval(() => {
|
|
62
|
+
this.sweepInactiveSessions().catch(err => {
|
|
63
|
+
logger_1.default.error('Inactive session sweep failed', { error: err.message });
|
|
64
|
+
});
|
|
65
|
+
}, 5 * 60_000);
|
|
66
|
+
this.costSweepInterval = setInterval(() => {
|
|
67
|
+
this.sweepConversationCosts().catch(err => {
|
|
68
|
+
logger_1.default.error('Cost sweep failed', { error: err.message });
|
|
69
|
+
});
|
|
70
|
+
}, 10 * 60_000);
|
|
71
|
+
setInterval(() => {
|
|
72
|
+
this.sweepExpiredOperatorRequests().catch(err => {
|
|
73
|
+
logger_1.default.error('Operator request expiry sweep failed', { error: err.message });
|
|
74
|
+
});
|
|
75
|
+
}, 15 * 60_000);
|
|
76
|
+
await this.recoverSessions();
|
|
77
|
+
logger_1.default.info('WhatsAppAgentService initialized');
|
|
78
|
+
}
|
|
79
|
+
// ─── Session Management ──────────────────────────────────
|
|
80
|
+
async startSession(params) {
|
|
81
|
+
const { session, gracePeriodSeconds, agentId, tenantId } = params;
|
|
82
|
+
const sessionId = session._id.toString();
|
|
83
|
+
// Cancel any existing timer for this session
|
|
84
|
+
this.cancelSession(sessionId);
|
|
85
|
+
const timer = setTimeout(async () => {
|
|
86
|
+
this.sessionTimers.delete(sessionId);
|
|
87
|
+
await this.onGracePeriodExpired(sessionId, agentId, tenantId);
|
|
88
|
+
}, gracePeriodSeconds * 1000);
|
|
89
|
+
this.sessionTimers.set(sessionId, timer);
|
|
90
|
+
logger_1.default.info('Session grace period started', {
|
|
91
|
+
sessionId,
|
|
92
|
+
chatId: session.chat_id,
|
|
93
|
+
gracePeriodSeconds,
|
|
94
|
+
deadline: session.grace_deadline?.toISOString(),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
cancelSession(sessionId) {
|
|
98
|
+
const timer = this.sessionTimers.get(sessionId);
|
|
99
|
+
if (timer) {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
this.sessionTimers.delete(sessionId);
|
|
102
|
+
logger_1.default.info('Session timer cancelled', { sessionId });
|
|
103
|
+
}
|
|
104
|
+
this.cancelMessageBuffer(sessionId);
|
|
105
|
+
}
|
|
106
|
+
cancelMessageBuffer(sessionId) {
|
|
107
|
+
const timer = this.messageBufferTimers.get(sessionId);
|
|
108
|
+
if (timer) {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
this.messageBufferTimers.delete(sessionId);
|
|
111
|
+
logger_1.default.debug('Message buffer timer cancelled', { sessionId });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async resolveSessionAsHuman(session) {
|
|
115
|
+
this.cancelSession(session._id.toString());
|
|
116
|
+
session.status = 'resolved';
|
|
117
|
+
session.resolved_by = 'human';
|
|
118
|
+
session.resolved_at = new Date();
|
|
119
|
+
await session.save();
|
|
120
|
+
await whatsapp_chat_model_1.default.updateOne({ _id: session.chat_id }, { active_session_id: null });
|
|
121
|
+
logger_1.default.info('Session resolved by human response', { sessionId: session._id });
|
|
122
|
+
this.updateContactProfileSummary(session._id.toString()).catch(err => {
|
|
123
|
+
logger_1.default.error('Profile summary update failed after human resolve', { error: err.message });
|
|
124
|
+
});
|
|
125
|
+
this.generateSessionLabels(session._id.toString()).catch(err => {
|
|
126
|
+
logger_1.default.error('Session labelling failed after human resolve', { error: err.message });
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Find unanswered contact messages in a session and trigger AI response.
|
|
131
|
+
* Used by grace period expiry and the "immediate response" release option.
|
|
132
|
+
*/
|
|
133
|
+
async replayPendingMessages(params) {
|
|
134
|
+
const { tenantId, agentId, session } = params;
|
|
135
|
+
const sessionId = session._id.toString();
|
|
136
|
+
// Skip AI if any human has ever messaged in this chat
|
|
137
|
+
if (await this.hasHumanMessageInChat(session.chat_id)) {
|
|
138
|
+
logger_1.default.info('Skipping AI replay — human has previously messaged in this chat', { sessionId });
|
|
139
|
+
return { pendingCount: 0 };
|
|
140
|
+
}
|
|
141
|
+
// Skip AI if outside shift hours
|
|
142
|
+
const tenant = await tenant_model_1.default.findById(tenantId).lean();
|
|
143
|
+
if (tenant && !this.isWithinAiShift(tenant)) {
|
|
144
|
+
logger_1.default.info('Skipping AI replay — outside AI shift hours', { sessionId });
|
|
145
|
+
return { pendingCount: 0 };
|
|
146
|
+
}
|
|
147
|
+
// Simple session-scoped query — no backwards scanning
|
|
148
|
+
const pendingMessages = await whatsapp_message_model_1.default.find({
|
|
149
|
+
session_id: session._id,
|
|
150
|
+
sender: 'contact',
|
|
151
|
+
}).sort({ createdAt: 1 }).lean();
|
|
152
|
+
if (pendingMessages.length === 0) {
|
|
153
|
+
logger_1.default.info('No pending messages to replay', { sessionId });
|
|
154
|
+
return { pendingCount: 0 };
|
|
155
|
+
}
|
|
156
|
+
const combinedText = pendingMessages.map(m => m.text).join('\n');
|
|
157
|
+
logger_1.default.info('Replaying pending messages for AI response', {
|
|
158
|
+
sessionId,
|
|
159
|
+
messageCount: pendingMessages.length,
|
|
160
|
+
});
|
|
161
|
+
const chat = await whatsapp_chat_model_1.default.findById(session.chat_id);
|
|
162
|
+
if (!chat)
|
|
163
|
+
return { pendingCount: 0 };
|
|
164
|
+
await this.triggerAiResponse({
|
|
165
|
+
tenantId,
|
|
166
|
+
agentId,
|
|
167
|
+
chat,
|
|
168
|
+
session,
|
|
169
|
+
messageText: combinedText,
|
|
170
|
+
});
|
|
171
|
+
return { pendingCount: pendingMessages.length };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Called when the message buffer timer expires (20s after last contact message).
|
|
175
|
+
* Collects all unanswered contact messages and sends them combined to AI.
|
|
176
|
+
*/
|
|
177
|
+
async onMessageBufferExpired(sessionId, tenantId, agentId) {
|
|
178
|
+
this.messageBufferTimers.delete(sessionId);
|
|
179
|
+
const session = await whatsapp_session_model_1.default.findById(sessionId);
|
|
180
|
+
if (!session || session.status !== 'active' || session.taken_over_by) {
|
|
181
|
+
logger_1.default.debug('Buffer expired but session not active for AI', { sessionId });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const chat = await whatsapp_chat_model_1.default.findById(session.chat_id);
|
|
185
|
+
if (!chat)
|
|
186
|
+
return;
|
|
187
|
+
// Skip AI if any human has ever messaged in this chat
|
|
188
|
+
if (await this.hasHumanMessageInChat(session.chat_id)) {
|
|
189
|
+
logger_1.default.info('Buffer expired but skipping AI — human has previously messaged', { sessionId });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Skip AI if outside shift hours
|
|
193
|
+
const tenant = await tenant_model_1.default.findById(session.tenant_id).lean();
|
|
194
|
+
if (tenant && !this.isWithinAiShift(tenant)) {
|
|
195
|
+
logger_1.default.info('Buffer expired but outside AI shift hours — skipping AI', { sessionId });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// Find the last AI response in this session
|
|
199
|
+
const lastAiMsg = await whatsapp_message_model_1.default.findOne({
|
|
200
|
+
session_id: session._id,
|
|
201
|
+
sender: 'human',
|
|
202
|
+
sender_name: 'AI',
|
|
203
|
+
}).sort({ createdAt: -1 }).lean();
|
|
204
|
+
// Get all contact messages after the last AI response (or all if no AI response yet)
|
|
205
|
+
const query = {
|
|
206
|
+
session_id: session._id,
|
|
207
|
+
sender: 'contact',
|
|
208
|
+
};
|
|
209
|
+
if (lastAiMsg) {
|
|
210
|
+
query.createdAt = { $gt: lastAiMsg.createdAt };
|
|
211
|
+
}
|
|
212
|
+
const pendingMessages = await whatsapp_message_model_1.default.find(query).sort({ createdAt: 1 }).lean();
|
|
213
|
+
if (pendingMessages.length === 0) {
|
|
214
|
+
logger_1.default.debug('Buffer expired but no pending messages', { sessionId });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const combinedText = pendingMessages.map(m => m.text).join('\n');
|
|
218
|
+
logger_1.default.info('Message buffer expired, sending combined message to AI', {
|
|
219
|
+
sessionId,
|
|
220
|
+
messageCount: pendingMessages.length,
|
|
221
|
+
combinedLength: combinedText.length,
|
|
222
|
+
});
|
|
223
|
+
await this.triggerAiResponse({
|
|
224
|
+
tenantId,
|
|
225
|
+
agentId,
|
|
226
|
+
chat,
|
|
227
|
+
session,
|
|
228
|
+
messageText: combinedText,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
async onGracePeriodExpired(sessionId, agentId, tenantId) {
|
|
232
|
+
const session = await whatsapp_session_model_1.default.findById(sessionId);
|
|
233
|
+
if (!session)
|
|
234
|
+
return;
|
|
235
|
+
if (session.status !== 'waiting') {
|
|
236
|
+
logger_1.default.info('Grace period expired but session already resolved', {
|
|
237
|
+
sessionId,
|
|
238
|
+
status: session.status,
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (!agentId) {
|
|
243
|
+
session.status = 'resolved';
|
|
244
|
+
session.resolved_by = 'manual';
|
|
245
|
+
session.resolved_at = new Date();
|
|
246
|
+
await session.save();
|
|
247
|
+
await whatsapp_chat_model_1.default.updateOne({ _id: session.chat_id }, { active_session_id: null });
|
|
248
|
+
logger_1.default.info('Grace period expired but no agent configured', { sessionId });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// Skip AI if any human has ever messaged in this chat
|
|
252
|
+
if (await this.hasHumanMessageInChat(session.chat_id)) {
|
|
253
|
+
session.status = 'resolved';
|
|
254
|
+
session.resolved_by = 'manual';
|
|
255
|
+
session.resolved_at = new Date();
|
|
256
|
+
await session.save();
|
|
257
|
+
await whatsapp_chat_model_1.default.updateOne({ _id: session.chat_id }, { active_session_id: null });
|
|
258
|
+
logger_1.default.info('Grace period expired but skipping AI — human has previously messaged', { sessionId });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// Skip AI if outside shift hours
|
|
262
|
+
const tenant = await tenant_model_1.default.findById(tenantId).lean();
|
|
263
|
+
if (tenant && !this.isWithinAiShift(tenant)) {
|
|
264
|
+
session.status = 'resolved';
|
|
265
|
+
session.resolved_by = 'manual';
|
|
266
|
+
session.resolved_at = new Date();
|
|
267
|
+
await session.save();
|
|
268
|
+
await whatsapp_chat_model_1.default.updateOne({ _id: session.chat_id }, { active_session_id: null });
|
|
269
|
+
logger_1.default.info('Grace period expired but outside AI shift hours — resolved as manual', { sessionId });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
// Transition to active (AI takes over)
|
|
273
|
+
session.status = 'active';
|
|
274
|
+
await session.save();
|
|
275
|
+
logger_1.default.info('Grace period expired, session now active (AI)', { sessionId, agentId });
|
|
276
|
+
await this.replayPendingMessages({ tenantId, agentId, session });
|
|
277
|
+
}
|
|
278
|
+
async recoverSessions() {
|
|
279
|
+
const waitingSessions = await whatsapp_session_model_1.default.find({ status: 'waiting' }).lean();
|
|
280
|
+
const now = Date.now();
|
|
281
|
+
for (const session of waitingSessions) {
|
|
282
|
+
const deadline = session.grace_deadline
|
|
283
|
+
? new Date(session.grace_deadline).getTime()
|
|
284
|
+
: 0;
|
|
285
|
+
const remaining = deadline - now;
|
|
286
|
+
const tenant = await tenant_model_1.default.findById(session.tenant_id).lean();
|
|
287
|
+
const agentId = tenant ? await resolveAgentId(tenant) : '';
|
|
288
|
+
const sessionId = session._id.toString();
|
|
289
|
+
if (remaining <= 0) {
|
|
290
|
+
logger_1.default.info('Recovering expired session', { sessionId });
|
|
291
|
+
await this.onGracePeriodExpired(sessionId, agentId, session.tenant_id.toString());
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
logger_1.default.info('Recovering active session', { sessionId, remainingMs: remaining });
|
|
295
|
+
const timer = setTimeout(async () => {
|
|
296
|
+
this.sessionTimers.delete(sessionId);
|
|
297
|
+
await this.onGracePeriodExpired(sessionId, agentId, session.tenant_id.toString());
|
|
298
|
+
}, remaining);
|
|
299
|
+
this.sessionTimers.set(sessionId, timer);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (waitingSessions.length > 0) {
|
|
303
|
+
logger_1.default.info('Session recovery complete', { recovered: waitingSessions.length });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Safety net: find sessions stuck in 'waiting' past their deadline
|
|
308
|
+
* that no longer have an in-memory timer.
|
|
309
|
+
*/
|
|
310
|
+
async sweepStuckSessions() {
|
|
311
|
+
const stuckSessions = await whatsapp_session_model_1.default.find({
|
|
312
|
+
status: 'waiting',
|
|
313
|
+
grace_deadline: { $lt: new Date() },
|
|
314
|
+
}).lean();
|
|
315
|
+
let swept = 0;
|
|
316
|
+
for (const session of stuckSessions) {
|
|
317
|
+
const sessionId = session._id.toString();
|
|
318
|
+
if (this.sessionTimers.has(sessionId))
|
|
319
|
+
continue;
|
|
320
|
+
swept++;
|
|
321
|
+
logger_1.default.warn('Sweeping stuck session', {
|
|
322
|
+
sessionId,
|
|
323
|
+
deadline: session.grace_deadline,
|
|
324
|
+
});
|
|
325
|
+
const tenant = await tenant_model_1.default.findById(session.tenant_id).lean();
|
|
326
|
+
const agentId = tenant ? await resolveAgentId(tenant) : '';
|
|
327
|
+
await this.onGracePeriodExpired(sessionId, agentId, session.tenant_id.toString());
|
|
328
|
+
}
|
|
329
|
+
if (swept > 0) {
|
|
330
|
+
logger_1.default.info('Stuck session sweep complete', { swept, total: stuckSessions.length });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Auto-resolve active sessions that have had no activity for SESSION_INACTIVITY_TIMEOUT_MS.
|
|
335
|
+
* Uses updatedAt as the activity indicator (updated on every message count increment).
|
|
336
|
+
*/
|
|
337
|
+
async sweepInactiveSessions() {
|
|
338
|
+
const cutoff = new Date(Date.now() - SESSION_INACTIVITY_TIMEOUT_MS);
|
|
339
|
+
const inactiveSessions = await whatsapp_session_model_1.default.find({
|
|
340
|
+
status: 'active',
|
|
341
|
+
awaiting_operator: { $ne: true },
|
|
342
|
+
updatedAt: { $lt: cutoff },
|
|
343
|
+
}).lean();
|
|
344
|
+
let resolved = 0;
|
|
345
|
+
for (const session of inactiveSessions) {
|
|
346
|
+
const sessionId = session._id.toString();
|
|
347
|
+
// Close the WS connection if still open
|
|
348
|
+
const chat = await whatsapp_chat_model_1.default.findById(session.chat_id);
|
|
349
|
+
if (chat) {
|
|
350
|
+
this.closeConnection(chat.unipile_chat_id);
|
|
351
|
+
}
|
|
352
|
+
// Resolve the session
|
|
353
|
+
await whatsapp_session_model_1.default.updateOne({ _id: session._id }, {
|
|
354
|
+
status: 'resolved',
|
|
355
|
+
resolved_by: 'timeout',
|
|
356
|
+
resolved_at: new Date(),
|
|
357
|
+
});
|
|
358
|
+
// Unlink from chat
|
|
359
|
+
await whatsapp_chat_model_1.default.updateOne({ _id: session.chat_id, active_session_id: session._id }, { active_session_id: null });
|
|
360
|
+
resolved++;
|
|
361
|
+
logger_1.default.info('Auto-resolved inactive session', {
|
|
362
|
+
sessionId,
|
|
363
|
+
chatId: session.chat_id,
|
|
364
|
+
lastActivity: session.updatedAt,
|
|
365
|
+
});
|
|
366
|
+
this.updateContactProfileSummary(sessionId).catch(err => {
|
|
367
|
+
logger_1.default.error('Profile summary update failed after inactive sweep', { error: err.message });
|
|
368
|
+
});
|
|
369
|
+
this.generateSessionLabels(sessionId).catch(err => {
|
|
370
|
+
logger_1.default.error('Session labelling failed after inactive sweep', { error: err.message });
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (resolved > 0) {
|
|
374
|
+
logger_1.default.info('Inactive session sweep complete', { resolved, total: inactiveSessions.length });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Fetch ElevenLabs credits for resolved sessions that have a conversation ID but no cost yet.
|
|
379
|
+
*/
|
|
380
|
+
async sweepConversationCosts() {
|
|
381
|
+
const sessions = await whatsapp_session_model_1.default.find({
|
|
382
|
+
elevenlabs_conversation_id: { $ne: null },
|
|
383
|
+
elevenlabs_cost: null,
|
|
384
|
+
status: 'resolved',
|
|
385
|
+
}).limit(20).lean();
|
|
386
|
+
let updated = 0;
|
|
387
|
+
for (const session of sessions) {
|
|
388
|
+
try {
|
|
389
|
+
const conv = await elevenlabs_service_1.default.getConversation(session.elevenlabs_conversation_id);
|
|
390
|
+
const convAny = conv;
|
|
391
|
+
const cost = convAny.metadata?.cost;
|
|
392
|
+
if (cost != null) {
|
|
393
|
+
await whatsapp_session_model_1.default.updateOne({ _id: session._id }, { elevenlabs_cost: cost });
|
|
394
|
+
updated++;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
const status = err.statusCode
|
|
399
|
+
|| err.status;
|
|
400
|
+
if (status === 404) {
|
|
401
|
+
await whatsapp_session_model_1.default.updateOne({ _id: session._id }, { elevenlabs_cost: 0 });
|
|
402
|
+
logger_1.default.warn('Conversation not found on ElevenLabs, set cost to 0', {
|
|
403
|
+
sessionId: session._id,
|
|
404
|
+
conversationId: session.elevenlabs_conversation_id,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
logger_1.default.warn('Failed to fetch conversation cost', {
|
|
409
|
+
sessionId: session._id,
|
|
410
|
+
conversationId: session.elevenlabs_conversation_id,
|
|
411
|
+
error: err.message,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (updated > 0) {
|
|
417
|
+
logger_1.default.info('Conversation cost sweep complete', { updated, checked: sessions.length });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// ─── AI Condition Checks ─────────────────────────────────
|
|
421
|
+
/**
|
|
422
|
+
* Check if any real human (not AI bot) has ever sent a message in this chat.
|
|
423
|
+
* Used to permanently skip AI for chats where a human has already engaged.
|
|
424
|
+
*/
|
|
425
|
+
async hasHumanMessageInChat(chatId) {
|
|
426
|
+
const msg = await whatsapp_message_model_1.default.findOne({
|
|
427
|
+
chat_id: chatId,
|
|
428
|
+
sender: 'human',
|
|
429
|
+
sender_name: { $ne: 'AI' },
|
|
430
|
+
}).select('_id').lean();
|
|
431
|
+
return !!msg;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Check whether the AI agent should be active right now.
|
|
435
|
+
* The schedule represents **clinic working hours** (when humans handle messages).
|
|
436
|
+
* The AI is active OUTSIDE those hours.
|
|
437
|
+
* If shift is not enabled, returns true (AI works 24/7).
|
|
438
|
+
* Fixed timezone: Europe/Istanbul.
|
|
439
|
+
*/
|
|
440
|
+
isWithinAiShift(tenant) {
|
|
441
|
+
const shift = tenant.settings?.whatsapp_agent;
|
|
442
|
+
if (!shift?.ai_shift_enabled)
|
|
443
|
+
return true;
|
|
444
|
+
const now = new Date();
|
|
445
|
+
// Get current hour, minute and day in Europe/Istanbul
|
|
446
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
447
|
+
timeZone: 'Europe/Istanbul',
|
|
448
|
+
hour: '2-digit',
|
|
449
|
+
minute: '2-digit',
|
|
450
|
+
hour12: false,
|
|
451
|
+
}).formatToParts(now);
|
|
452
|
+
const hour = parseInt(parts.find(p => p.type === 'hour')?.value || '0', 10);
|
|
453
|
+
const minute = parseInt(parts.find(p => p.type === 'minute')?.value || '0', 10);
|
|
454
|
+
const dayName = new Intl.DateTimeFormat('en-US', {
|
|
455
|
+
timeZone: 'Europe/Istanbul',
|
|
456
|
+
weekday: 'long',
|
|
457
|
+
}).format(now);
|
|
458
|
+
const dayMap = {
|
|
459
|
+
Sunday: 0, Monday: 1, Tuesday: 2, Wednesday: 3,
|
|
460
|
+
Thursday: 4, Friday: 5, Saturday: 6,
|
|
461
|
+
};
|
|
462
|
+
const currentDay = dayMap[dayName] ?? now.getDay();
|
|
463
|
+
// Per-day schedule with legacy fallback
|
|
464
|
+
// Schedule entries = clinic working hours (AI should be OFF during these)
|
|
465
|
+
const schedule = shift.ai_shift_schedule?.length
|
|
466
|
+
? shift.ai_shift_schedule
|
|
467
|
+
: (shift.ai_shift_days || []).map(day => ({
|
|
468
|
+
day, start: shift.ai_shift_start || '09:00', end: shift.ai_shift_end || '18:00',
|
|
469
|
+
}));
|
|
470
|
+
// No clinic hours defined for today → AI active all day
|
|
471
|
+
const entry = schedule.find(e => e.day === currentDay);
|
|
472
|
+
if (!entry)
|
|
473
|
+
return true;
|
|
474
|
+
// Check if current time is within clinic hours
|
|
475
|
+
const currentMinutes = hour * 60 + minute;
|
|
476
|
+
const [startH, startM] = (entry.start || '09:00').split(':').map(Number);
|
|
477
|
+
const [endH, endM] = (entry.end || '18:00').split(':').map(Number);
|
|
478
|
+
const startMinutes = startH * 60 + startM;
|
|
479
|
+
const endMinutes = endH * 60 + endM;
|
|
480
|
+
let withinClinicHours;
|
|
481
|
+
if (startMinutes <= endMinutes) {
|
|
482
|
+
withinClinicHours = currentMinutes >= startMinutes && currentMinutes < endMinutes;
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
// Overnight clinic hours (e.g. 22:00-06:00)
|
|
486
|
+
withinClinicHours = currentMinutes >= startMinutes || currentMinutes < endMinutes;
|
|
487
|
+
}
|
|
488
|
+
// AI is active when clinic is CLOSED
|
|
489
|
+
return !withinClinicHours;
|
|
490
|
+
}
|
|
491
|
+
// ─── AI Response ──────────────────────────────────────────
|
|
492
|
+
/**
|
|
493
|
+
* Build conversation context from current session messages only.
|
|
494
|
+
* Contact profile info is now sent via dynamic_variables at connection time.
|
|
495
|
+
*/
|
|
496
|
+
async buildChatContext(chatId, currentSessionId) {
|
|
497
|
+
const messages = await whatsapp_message_model_1.default.find({ session_id: currentSessionId })
|
|
498
|
+
.sort({ createdAt: 1 })
|
|
499
|
+
.lean();
|
|
500
|
+
if (messages.length === 0)
|
|
501
|
+
return null;
|
|
502
|
+
// Also fetch operator requests for this session to weave into timeline
|
|
503
|
+
const operatorRequests = await operator_request_model_1.default.find({ session_id: currentSessionId })
|
|
504
|
+
.sort({ createdAt: 1 })
|
|
505
|
+
.lean();
|
|
506
|
+
const timeline = [];
|
|
507
|
+
for (const m of messages) {
|
|
508
|
+
const role = m.sender === 'contact' ? 'Patient' : 'Operator';
|
|
509
|
+
timeline.push({ at: m.createdAt, text: `${role}: ${m.text}` });
|
|
510
|
+
}
|
|
511
|
+
for (const req of operatorRequests) {
|
|
512
|
+
const mediaCount = req.forwarded_count || 1;
|
|
513
|
+
const failInfo = req.failed_count ? ` (${req.failed_count} iletilemedi)` : '';
|
|
514
|
+
timeline.push({
|
|
515
|
+
at: req.forwarded_at,
|
|
516
|
+
text: `[SİSTEM: Hastanın ${mediaCount} medyası doktora iletildi — Ref: ${req.ref_code}${failInfo}]`,
|
|
517
|
+
});
|
|
518
|
+
if (req.status === 'responded' && req.operator_response) {
|
|
519
|
+
timeline.push({
|
|
520
|
+
at: req.responded_at || req.updatedAt,
|
|
521
|
+
text: `[SİSTEM: Doktor yanıt verdi — Ref: ${req.ref_code}: "${req.operator_response}"]`,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
else if (req.status === 'expired') {
|
|
525
|
+
timeline.push({
|
|
526
|
+
at: req.updatedAt,
|
|
527
|
+
text: `[SİSTEM: Doktordan yanıt alınamadı — Ref: ${req.ref_code} süresi doldu]`,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// Sort by timestamp
|
|
532
|
+
timeline.sort((a, b) => new Date(a.at).getTime() - new Date(b.at).getTime());
|
|
533
|
+
const lines = timeline.map(e => e.text);
|
|
534
|
+
// Check first contact message for ad context (patient clicked an ad to start the chat)
|
|
535
|
+
const firstContactMsg = messages.find(m => m.sender === 'contact');
|
|
536
|
+
const adCtx = firstContactMsg?.ad_context;
|
|
537
|
+
if (adCtx) {
|
|
538
|
+
const adParts = [];
|
|
539
|
+
if (adCtx.ad?.title)
|
|
540
|
+
adParts.push(`Reklam başlığı: ${adCtx.ad.title}`);
|
|
541
|
+
if (adCtx.ad?.body)
|
|
542
|
+
adParts.push(`Reklam içeriği: ${adCtx.ad.body}`);
|
|
543
|
+
if (adCtx.source)
|
|
544
|
+
adParts.push(`Kaynak: ${adCtx.source}`);
|
|
545
|
+
if (adCtx.app)
|
|
546
|
+
adParts.push(`Platform: ${adCtx.app}`);
|
|
547
|
+
if (adCtx.ad?.source_url)
|
|
548
|
+
adParts.push(`Reklam URL: ${adCtx.ad.source_url}`);
|
|
549
|
+
if (adParts.length > 0) {
|
|
550
|
+
lines.unshift(`[SİSTEM: Hasta şu reklama tıklayarak WhatsApp'a geldi]\n${adParts.join('\n')}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return `Current conversation:\n${lines.join('\n')}`;
|
|
554
|
+
}
|
|
555
|
+
async sendChatContext(conn, chatId, sessionId) {
|
|
556
|
+
const context = await this.buildChatContext(chatId, sessionId);
|
|
557
|
+
if (!context)
|
|
558
|
+
return;
|
|
559
|
+
conn.ws.send(JSON.stringify({
|
|
560
|
+
type: 'contextual_update',
|
|
561
|
+
text: context,
|
|
562
|
+
}));
|
|
563
|
+
logger_1.default.info('Sent chat history context to ElevenLabs', {
|
|
564
|
+
sessionId,
|
|
565
|
+
chatId,
|
|
566
|
+
contextLength: context.length,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
async triggerAiResponse(params) {
|
|
570
|
+
const { agentId, chat, session, messageText } = params;
|
|
571
|
+
const unipileChatId = chat.unipile_chat_id;
|
|
572
|
+
const sessionId = session._id.toString();
|
|
573
|
+
let conn = this.connections.get(unipileChatId);
|
|
574
|
+
const isAlive = conn && conn.ws.readyState === ws_1.default.OPEN && conn.ready;
|
|
575
|
+
let isNewConnection = false;
|
|
576
|
+
let isReconnect = false;
|
|
577
|
+
if (!isAlive) {
|
|
578
|
+
if (conn) {
|
|
579
|
+
if (conn.pingTimer) {
|
|
580
|
+
clearInterval(conn.pingTimer);
|
|
581
|
+
conn.pingTimer = null;
|
|
582
|
+
}
|
|
583
|
+
conn.intentionallyClosed = true;
|
|
584
|
+
try {
|
|
585
|
+
conn.ws.close();
|
|
586
|
+
}
|
|
587
|
+
catch { /* ignore */ }
|
|
588
|
+
this.connections.delete(unipileChatId);
|
|
589
|
+
}
|
|
590
|
+
// Detect reconnection: session already had an ElevenLabs conversation
|
|
591
|
+
isReconnect = !!session.elevenlabs_conversation_id;
|
|
592
|
+
try {
|
|
593
|
+
conn = await this.createConnection(agentId, chat, session, { isReconnect });
|
|
594
|
+
this.connections.set(unipileChatId, conn);
|
|
595
|
+
isNewConnection = true;
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
logger_1.default.error('Failed to create EL connection', {
|
|
599
|
+
error: err.message,
|
|
600
|
+
unipileChatId,
|
|
601
|
+
sessionId,
|
|
602
|
+
});
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
conn.lastActivity = Date.now();
|
|
607
|
+
await whatsapp_session_model_1.default.updateOne({ _id: session._id }, { elevenlabs_last_interaction_at: new Date() });
|
|
608
|
+
// Send chat context via contextual_update for new connections
|
|
609
|
+
// (On reconnection with continuation enabled, this is redundant but harmless as backup)
|
|
610
|
+
if (isNewConnection) {
|
|
611
|
+
await this.sendChatContext(conn, chat._id.toString(), sessionId);
|
|
612
|
+
}
|
|
613
|
+
conn.ws.send(JSON.stringify({
|
|
614
|
+
type: 'user_message',
|
|
615
|
+
text: messageText,
|
|
616
|
+
}));
|
|
617
|
+
logger_1.default.info('AI triggered', { sessionId, text: messageText.substring(0, 50) });
|
|
618
|
+
}
|
|
619
|
+
async handleIncomingMessage(params) {
|
|
620
|
+
const { tenantId, agentId, chat, session, messageText, senderName, unipileMessageId, contentType } = params;
|
|
621
|
+
const unipileChatId = chat.unipile_chat_id;
|
|
622
|
+
const sessionId = session._id.toString();
|
|
623
|
+
// Store the incoming message with session_id
|
|
624
|
+
await whatsapp_message_model_1.default.create({
|
|
625
|
+
chat_id: chat._id,
|
|
626
|
+
session_id: session._id,
|
|
627
|
+
tenant_id: tenantId,
|
|
628
|
+
unipile_message_id: unipileMessageId || null,
|
|
629
|
+
sender: 'contact',
|
|
630
|
+
sender_name: senderName,
|
|
631
|
+
text: messageText,
|
|
632
|
+
media_type: contentType || null,
|
|
633
|
+
sent_via_unipile: false,
|
|
634
|
+
});
|
|
635
|
+
// Update chat + session metadata
|
|
636
|
+
chat.last_message_at = new Date();
|
|
637
|
+
chat.last_message_preview = messageText.substring(0, 100);
|
|
638
|
+
chat.message_count += 1;
|
|
639
|
+
await chat.save();
|
|
640
|
+
session.message_count += 1;
|
|
641
|
+
await session.save();
|
|
642
|
+
// Skip AI if session is human-taken-over or no agent
|
|
643
|
+
if (session.taken_over_by || !agentId) {
|
|
644
|
+
logger_1.default.info('Skipping AI response', {
|
|
645
|
+
sessionId,
|
|
646
|
+
hasTakeover: !!session.taken_over_by,
|
|
647
|
+
hasAgent: !!agentId,
|
|
648
|
+
});
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
// Skip AI if any human has ever messaged in this chat
|
|
652
|
+
if (await this.hasHumanMessageInChat(chat._id)) {
|
|
653
|
+
logger_1.default.info('Skipping AI — human has previously messaged in this chat', {
|
|
654
|
+
sessionId,
|
|
655
|
+
chatId: chat._id.toString(),
|
|
656
|
+
});
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
// Skip AI if outside shift hours
|
|
660
|
+
const tenant = await tenant_model_1.default.findById(tenantId).lean();
|
|
661
|
+
if (tenant && !this.isWithinAiShift(tenant)) {
|
|
662
|
+
logger_1.default.info('Skipping AI — outside AI shift hours', { sessionId });
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
// Debounce: reset the message buffer timer so the AI waits for the
|
|
666
|
+
// contact to finish typing before responding to all messages at once.
|
|
667
|
+
this.cancelMessageBuffer(sessionId);
|
|
668
|
+
const bufferMs = Math.floor(Math.random() * (MESSAGE_BUFFER_MAX_MS - MESSAGE_BUFFER_MIN_MS + 1)) + MESSAGE_BUFFER_MIN_MS;
|
|
669
|
+
const bufferTimer = setTimeout(() => {
|
|
670
|
+
this.onMessageBufferExpired(sessionId, tenantId, agentId).catch(err => {
|
|
671
|
+
logger_1.default.error('Message buffer handler failed', {
|
|
672
|
+
error: err.message,
|
|
673
|
+
sessionId,
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
}, bufferMs);
|
|
677
|
+
this.messageBufferTimers.set(sessionId, bufferTimer);
|
|
678
|
+
logger_1.default.info('Message buffered, AI will respond after pause', {
|
|
679
|
+
sessionId,
|
|
680
|
+
bufferMs,
|
|
681
|
+
text: messageText.substring(0, 50),
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
// ─── ElevenLabs Connection ────────────────────────────────
|
|
685
|
+
async buildUserSummary(contactProfileId) {
|
|
686
|
+
if (!contactProfileId)
|
|
687
|
+
return { summary: '', hasMeaningfulContext: false };
|
|
688
|
+
const profile = await whatsapp_contact_profile_model_1.default.findById(contactProfileId).lean();
|
|
689
|
+
if (!profile)
|
|
690
|
+
return { summary: '', hasMeaningfulContext: false };
|
|
691
|
+
// Meaningful context = data beyond just a name (ai_summary, notes, tags, preferred_language)
|
|
692
|
+
const hasMeaningfulContext = !!(profile.ai_summary || profile.notes || (profile.tags && profile.tags.length > 0) || profile.preferred_language);
|
|
693
|
+
// No meaningful context → return empty so the fallback default is used
|
|
694
|
+
if (!hasMeaningfulContext)
|
|
695
|
+
return { summary: '', hasMeaningfulContext: false };
|
|
696
|
+
const parts = [];
|
|
697
|
+
if (profile.display_name && profile.display_name !== 'Bilinmiyor') {
|
|
698
|
+
parts.push(`Ad: ${profile.display_name}`);
|
|
699
|
+
}
|
|
700
|
+
if (profile.preferred_language) {
|
|
701
|
+
parts.push(`Tercih edilen dil: ${profile.preferred_language}`);
|
|
702
|
+
}
|
|
703
|
+
if (profile.notes) {
|
|
704
|
+
parts.push(`Notlar: ${profile.notes.substring(0, 500)}`);
|
|
705
|
+
}
|
|
706
|
+
if (profile.ai_summary) {
|
|
707
|
+
parts.push(`Gecmis ozeti: ${profile.ai_summary}`);
|
|
708
|
+
}
|
|
709
|
+
if (profile.tags.length > 0) {
|
|
710
|
+
parts.push(`Etiketler: ${profile.tags.join(', ')}`);
|
|
711
|
+
}
|
|
712
|
+
return { summary: parts.join('\n'), hasMeaningfulContext };
|
|
713
|
+
}
|
|
714
|
+
async createConnection(agentId, chat, session, options) {
|
|
715
|
+
const sessionId = session._id.toString();
|
|
716
|
+
let isReconnect = options?.isReconnect || false;
|
|
717
|
+
logger_1.default.info('Creating new EL conversation', { agentId, sessionId, isReconnect });
|
|
718
|
+
// Build user summary from contact profile BEFORE connecting
|
|
719
|
+
const { summary: profileSummary } = await this.buildUserSummary(chat.contact_profile_id?.toString() || null);
|
|
720
|
+
const userSummary = profileSummary || 'Ilk defa iletisime gecen hasta. Hakkinda bilgi bulunmamaktadir.';
|
|
721
|
+
// On reconnection + tenant has chat_continuity_enabled, build continuation directive
|
|
722
|
+
let continuationDirective = '';
|
|
723
|
+
if (isReconnect) {
|
|
724
|
+
const tenant = await tenant_model_1.default.findById(session.tenant_id).lean();
|
|
725
|
+
if (!tenant?.enabled_features?.includes('chat_continuity')) {
|
|
726
|
+
logger_1.default.info('Prompt continuation disabled for tenant, skipping directive', { sessionId });
|
|
727
|
+
isReconnect = false;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (isReconnect) {
|
|
731
|
+
const chatHistory = await this.buildChatContext(chat._id.toString(), sessionId);
|
|
732
|
+
const lastAiMsg = await whatsapp_message_model_1.default.findOne({
|
|
733
|
+
session_id: session._id,
|
|
734
|
+
sender: 'human',
|
|
735
|
+
sender_name: 'AI',
|
|
736
|
+
}).sort({ createdAt: -1 }).lean();
|
|
737
|
+
const parts = [
|
|
738
|
+
'ÖNEMLİ — KONUŞMA DEVAMI:',
|
|
739
|
+
'Bu hasta ile önceden başlamış bir konuşma var. Teknik bir bağlantı yenilenmesi yaşandı.',
|
|
740
|
+
'Kendini TANITMA. Selam VERME. Hoşgeldin DEME. Konuşmaya kaldığın yerden DEVAM ET.',
|
|
741
|
+
lastAiMsg ? `En son gönderdiğin mesaj: "${lastAiMsg.text.substring(0, 300)}"` : '',
|
|
742
|
+
'',
|
|
743
|
+
chatHistory || '',
|
|
744
|
+
].filter(Boolean).join('\n');
|
|
745
|
+
continuationDirective = parts;
|
|
746
|
+
logger_1.default.info('Built continuation directive for reconnection', {
|
|
747
|
+
sessionId,
|
|
748
|
+
directiveLength: continuationDirective.length,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
const result = await elevenlabs_service_1.default.getSignedUrl({ agentId });
|
|
752
|
+
const signedUrl = result.signedUrl
|
|
753
|
+
|| result.signed_url
|
|
754
|
+
|| String(result);
|
|
755
|
+
const separator = signedUrl.includes('?') ? '&' : '?';
|
|
756
|
+
const wsUrl = `${signedUrl}${separator}textOnly=true`;
|
|
757
|
+
const ws = new ws_1.default(wsUrl);
|
|
758
|
+
const unipileChatId = chat.unipile_chat_id;
|
|
759
|
+
const conn = {
|
|
760
|
+
ws,
|
|
761
|
+
chatId: chat._id.toString(),
|
|
762
|
+
sessionId,
|
|
763
|
+
unipileChatId,
|
|
764
|
+
agentId,
|
|
765
|
+
lastActivity: Date.now(),
|
|
766
|
+
conversationId: null,
|
|
767
|
+
ready: false,
|
|
768
|
+
intentionallyClosed: false,
|
|
769
|
+
pingTimer: null,
|
|
770
|
+
};
|
|
771
|
+
// Wait for WS to open AND receive conversation_initiation_metadata
|
|
772
|
+
await new Promise((resolve, reject) => {
|
|
773
|
+
const timeout = setTimeout(() => {
|
|
774
|
+
ws.close();
|
|
775
|
+
reject(new Error('ElevenLabs WS initialization timeout (15s)'));
|
|
776
|
+
}, 15_000);
|
|
777
|
+
ws.on('open', () => {
|
|
778
|
+
logger_1.default.info('ElevenLabs WS TCP connected, sending init data...', { unipileChatId, sessionId });
|
|
779
|
+
ws.send(JSON.stringify({
|
|
780
|
+
type: 'conversation_initiation_client_data',
|
|
781
|
+
conversation_config_override: {
|
|
782
|
+
agent: { first_message: '' },
|
|
783
|
+
},
|
|
784
|
+
dynamic_variables: {
|
|
785
|
+
user_summary: userSummary,
|
|
786
|
+
current_date: new Date().toISOString().split('T')[0],
|
|
787
|
+
continuation_directive: continuationDirective,
|
|
788
|
+
},
|
|
789
|
+
}));
|
|
790
|
+
logger_1.default.info('Sent conversation_initiation_client_data', {
|
|
791
|
+
sessionId,
|
|
792
|
+
userSummaryLength: userSummary.length,
|
|
793
|
+
isReconnect,
|
|
794
|
+
continuationDirectiveLength: continuationDirective.length,
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
ws.on('error', (err) => {
|
|
798
|
+
clearTimeout(timeout);
|
|
799
|
+
reject(err);
|
|
800
|
+
});
|
|
801
|
+
ws.on('message', (data) => {
|
|
802
|
+
try {
|
|
803
|
+
const msg = JSON.parse(data.toString());
|
|
804
|
+
if (msg.type === 'conversation_initiation_metadata') {
|
|
805
|
+
const convId = msg.conversation_initiation_metadata_event?.conversation_id
|
|
806
|
+
|| msg.conversation_id;
|
|
807
|
+
if (convId) {
|
|
808
|
+
conn.conversationId = convId;
|
|
809
|
+
const updateOp = {
|
|
810
|
+
$set: {
|
|
811
|
+
elevenlabs_conversation_id: convId,
|
|
812
|
+
elevenlabs_conversation_created_at: new Date(),
|
|
813
|
+
elevenlabs_last_interaction_at: new Date(),
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
// Preserve previous conversation ID if this is a reconnection
|
|
817
|
+
if (session.elevenlabs_conversation_id && session.elevenlabs_conversation_id !== convId) {
|
|
818
|
+
updateOp.$push = { elevenlabs_previous_conversation_ids: session.elevenlabs_conversation_id };
|
|
819
|
+
}
|
|
820
|
+
whatsapp_session_model_1.default.updateOne({ _id: session._id }, updateOp).catch(() => { });
|
|
821
|
+
}
|
|
822
|
+
conn.ready = true;
|
|
823
|
+
clearTimeout(timeout);
|
|
824
|
+
logger_1.default.info('ElevenLabs conversation ready', {
|
|
825
|
+
sessionId,
|
|
826
|
+
conversationId: convId,
|
|
827
|
+
});
|
|
828
|
+
resolve();
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
catch (err) {
|
|
832
|
+
logger_1.default.error('Error parsing EL WS init message', { error: err.message });
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
// Start keepalive: TCP ping + EL user_activity to reset inactivity timer
|
|
837
|
+
conn.pingTimer = setInterval(() => {
|
|
838
|
+
if (conn.ws.readyState === ws_1.default.OPEN) {
|
|
839
|
+
conn.ws.ping();
|
|
840
|
+
conn.ws.send(JSON.stringify({ type: 'user_activity' }));
|
|
841
|
+
}
|
|
842
|
+
}, WS_PING_INTERVAL_MS);
|
|
843
|
+
// Persistent message handler for agent responses
|
|
844
|
+
ws.on('message', async (data) => {
|
|
845
|
+
try {
|
|
846
|
+
const msg = JSON.parse(data.toString());
|
|
847
|
+
if (msg.type === 'conversation_initiation_metadata')
|
|
848
|
+
return;
|
|
849
|
+
// Respond to EL ping with pong
|
|
850
|
+
if (msg.type === 'ping') {
|
|
851
|
+
const eventId = msg.ping_event?.event_id;
|
|
852
|
+
if (eventId != null) {
|
|
853
|
+
conn.ws.send(JSON.stringify({ type: 'pong', event_id: eventId }));
|
|
854
|
+
}
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
logger_1.default.info('EL WS event', { type: msg.type, sessionId, raw: JSON.stringify(msg).substring(0, 400) });
|
|
858
|
+
if (msg.type === 'client_tool_call') {
|
|
859
|
+
const { tool_call_id, tool_name, parameters } = msg.client_tool_call;
|
|
860
|
+
this.handleClientToolCall(conn, tool_call_id, tool_name, parameters)
|
|
861
|
+
.catch(err => logger_1.default.error('Client tool call failed', {
|
|
862
|
+
tool_name,
|
|
863
|
+
sessionId,
|
|
864
|
+
error: err.message,
|
|
865
|
+
}));
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (msg.type === 'agent_response') {
|
|
869
|
+
const responseText = msg.agent_response_event?.agent_response
|
|
870
|
+
|| msg.agent_response;
|
|
871
|
+
if (responseText) {
|
|
872
|
+
await whatsapp_session_model_1.default.updateOne({ _id: session._id }, { elevenlabs_last_interaction_at: new Date() });
|
|
873
|
+
await this.sendResponse(chat, session, responseText);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
catch (err) {
|
|
878
|
+
logger_1.default.error('Error handling EL WS message', {
|
|
879
|
+
error: err.message,
|
|
880
|
+
sessionId,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
ws.on('close', (code, reason) => {
|
|
885
|
+
if (conn.pingTimer) {
|
|
886
|
+
clearInterval(conn.pingTimer);
|
|
887
|
+
conn.pingTimer = null;
|
|
888
|
+
}
|
|
889
|
+
logger_1.default.info('ElevenLabs WS closed', { unipileChatId, sessionId, code, reason: reason?.toString() });
|
|
890
|
+
this.connections.delete(unipileChatId);
|
|
891
|
+
// No eager reconnect — triggerAiResponse handles lazy reconnect on next message
|
|
892
|
+
});
|
|
893
|
+
ws.on('error', (err) => {
|
|
894
|
+
logger_1.default.error('ElevenLabs WS error', { error: err.message, sessionId });
|
|
895
|
+
});
|
|
896
|
+
return conn;
|
|
897
|
+
}
|
|
898
|
+
async sendResponse(chat, session, text) {
|
|
899
|
+
try {
|
|
900
|
+
const tenant = await tenant_model_1.default.findById(chat.tenant_id).lean();
|
|
901
|
+
const provider = unipile_service_1.default.getProviderForTenant(tenant);
|
|
902
|
+
const result = await unipile_service_1.default.sendMessage(provider, chat.unipile_chat_id, text);
|
|
903
|
+
logger_1.default.info('AI response sent via bridge', { provider, sessionId: session._id, text: text.substring(0, 50) });
|
|
904
|
+
await whatsapp_message_model_1.default.create({
|
|
905
|
+
chat_id: chat._id,
|
|
906
|
+
session_id: session._id,
|
|
907
|
+
tenant_id: chat.tenant_id,
|
|
908
|
+
unipile_message_id: result.message_id || null,
|
|
909
|
+
sender: 'human',
|
|
910
|
+
sender_name: 'AI',
|
|
911
|
+
text,
|
|
912
|
+
sent_via_unipile: true,
|
|
913
|
+
});
|
|
914
|
+
await whatsapp_chat_model_1.default.updateOne({ _id: chat._id }, {
|
|
915
|
+
$inc: { message_count: 1 },
|
|
916
|
+
$set: { last_message_at: new Date(), last_message_preview: text.substring(0, 100) },
|
|
917
|
+
});
|
|
918
|
+
await whatsapp_session_model_1.default.updateOne({ _id: session._id }, { $inc: { message_count: 1 } });
|
|
919
|
+
}
|
|
920
|
+
catch (err) {
|
|
921
|
+
logger_1.default.error('Failed to send AI response via bridge', {
|
|
922
|
+
sessionId: session._id,
|
|
923
|
+
error: err.message,
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
// ─── Contact Profile Update ──────────────────────────────
|
|
928
|
+
/**
|
|
929
|
+
* After a session resolves, update the contact profile's ai_summary
|
|
930
|
+
* by sending the session transcript + previous summary to a dedicated
|
|
931
|
+
* ElevenLabs agent via simulateConversation REST API.
|
|
932
|
+
* Fire-and-forget — errors are logged but never block session resolution.
|
|
933
|
+
*/
|
|
934
|
+
async updateContactProfileSummary(sessionId) {
|
|
935
|
+
const session = await whatsapp_session_model_1.default.findById(sessionId).lean();
|
|
936
|
+
if (!session || session.message_count < 2) {
|
|
937
|
+
throw new Error('Session not found or has fewer than 2 messages');
|
|
938
|
+
}
|
|
939
|
+
const chat = await whatsapp_chat_model_1.default.findById(session.chat_id).lean();
|
|
940
|
+
if (!chat?.contact_profile_id) {
|
|
941
|
+
throw new Error('Chat has no linked contact profile');
|
|
942
|
+
}
|
|
943
|
+
const profile = await whatsapp_contact_profile_model_1.default.findById(chat.contact_profile_id).lean();
|
|
944
|
+
if (!profile) {
|
|
945
|
+
throw new Error('Contact profile not found');
|
|
946
|
+
}
|
|
947
|
+
// Build the transcript from session messages
|
|
948
|
+
const messages = await whatsapp_message_model_1.default.find({ session_id: session._id })
|
|
949
|
+
.sort({ createdAt: 1 })
|
|
950
|
+
.lean();
|
|
951
|
+
if (messages.length < 2) {
|
|
952
|
+
throw new Error('Session has fewer than 2 messages');
|
|
953
|
+
}
|
|
954
|
+
const transcript = messages
|
|
955
|
+
.map(m => {
|
|
956
|
+
const role = m.sender === 'contact' ? 'HASTA' : 'ASISTAN';
|
|
957
|
+
const ts = new Date(m.createdAt).toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' });
|
|
958
|
+
return `[${ts}] [${role}]: ${m.text}`;
|
|
959
|
+
})
|
|
960
|
+
.join('\n');
|
|
961
|
+
const now = new Date().toLocaleString('tr-TR', { timeZone: 'Europe/Istanbul' });
|
|
962
|
+
const previousSummary = profile.ai_summary || 'Henüz profil özeti yok.';
|
|
963
|
+
const systemMessage = [
|
|
964
|
+
'Sen bir hasta profil özeti oluşturma asistanısın.',
|
|
965
|
+
'Sana mevcut hasta profil özeti ve bir WhatsApp konuşma geçmişi verilecek.',
|
|
966
|
+
'',
|
|
967
|
+
'KURALLAR:',
|
|
968
|
+
'- [HASTA] etiketli satırlar hastanın yazdıklarıdır — SADECE buradan bilgi çıkar.',
|
|
969
|
+
'- [ASISTAN] etiketli satırlar bizim AI asistanımızın/operatörümüzün yazdıklarıdır — bunları hasta bilgisi olarak KULLANMA.',
|
|
970
|
+
'- "Yarın", "bugün", "haftaya" gibi göreceli tarihleri mesajın gönderildiği tarihe göre gerçek tarihe çevir (ör: 19.02.2026 tarihli mesajda "yarın" = 20.02.2026).',
|
|
971
|
+
'- Yanıtın SADECE güncellenmiş profil özeti olmalı. Başka açıklama, selamlama veya soru ekleme.',
|
|
972
|
+
'- Türkçe yaz.',
|
|
973
|
+
'- Özeti madde işaretleri ile yapılandır.',
|
|
974
|
+
'- Şu kategorileri kullan (bilgi varsa):',
|
|
975
|
+
' • Kişisel Bilgiler (ad, yaş, cinsiyet, meslek, konum vb.)',
|
|
976
|
+
' • Sağlık Durumu (şikayetler, tanılar, tedaviler, ilaçlar)',
|
|
977
|
+
' • Randevu Geçmişi (önceki/planlanan randevular, gerçek tarihler ile)',
|
|
978
|
+
' • İletişim Tercihleri (iletişim tarzı, hassasiyetler)',
|
|
979
|
+
' • Tercih Edilen Dil (hastanın mesajlarında kullandığı dil — ör: Türkçe, English, العربية, Русский)',
|
|
980
|
+
' • Önemli Notlar (özel durumlar, talepler, dikkat edilecekler)',
|
|
981
|
+
'- Önceki özette olan ama yeni konuşmada güncellenmeyen bilgileri koru.',
|
|
982
|
+
'- Yeni konuşmadan çıkan bilgilerle çelişen eski bilgileri güncelle.',
|
|
983
|
+
'- Spekülasyon yapma, sadece hastanın kendi söylediklerinden çıkarılabilecek bilgileri yaz.',
|
|
984
|
+
'- Özet 500 kelimeyi geçmesin.',
|
|
985
|
+
'- Özetin EN SON satırında, hastanın mesajlarında kullandığı dili şu formatta yaz: PREFERRED_LANGUAGE: <dil>',
|
|
986
|
+
' Örnek: PREFERRED_LANGUAGE: English',
|
|
987
|
+
' Eğer hasta Türkçe yazıyorsa: PREFERRED_LANGUAGE: Türkçe',
|
|
988
|
+
' Bu satır her zaman olmalı.',
|
|
989
|
+
].join('\n');
|
|
990
|
+
const userMessage = [
|
|
991
|
+
`Bugünün tarihi: ${now}`,
|
|
992
|
+
'',
|
|
993
|
+
`--- Mevcut hasta profili ---`,
|
|
994
|
+
previousSummary,
|
|
995
|
+
'',
|
|
996
|
+
`--- Konuşma geçmişi ---`,
|
|
997
|
+
transcript,
|
|
998
|
+
].join('\n');
|
|
999
|
+
const aiService = (0, ai_1.createAIService)();
|
|
1000
|
+
const result = await aiService.generateSummary([], {
|
|
1001
|
+
systemMessage,
|
|
1002
|
+
summaryPrompt: userMessage,
|
|
1003
|
+
language: 'tr',
|
|
1004
|
+
maxTokens: 1000,
|
|
1005
|
+
});
|
|
1006
|
+
const rawSummary = result.summary?.trim();
|
|
1007
|
+
if (!rawSummary) {
|
|
1008
|
+
throw new Error('AI did not return a summary response');
|
|
1009
|
+
}
|
|
1010
|
+
// Extract PREFERRED_LANGUAGE line and remove it from the summary text
|
|
1011
|
+
const langMatch = rawSummary.match(/PREFERRED_LANGUAGE:\s*(.+)/i);
|
|
1012
|
+
const preferredLanguage = langMatch ? langMatch[1].trim() : '';
|
|
1013
|
+
const summaryText = rawSummary.replace(/\n?PREFERRED_LANGUAGE:\s*.+/i, '').trim();
|
|
1014
|
+
const updateFields = { ai_summary: summaryText };
|
|
1015
|
+
if (preferredLanguage) {
|
|
1016
|
+
updateFields.preferred_language = preferredLanguage;
|
|
1017
|
+
}
|
|
1018
|
+
await whatsapp_contact_profile_model_1.default.updateOne({ _id: profile._id }, { $set: updateFields });
|
|
1019
|
+
logger_1.default.info('Contact profile summary updated', {
|
|
1020
|
+
sessionId,
|
|
1021
|
+
profileId: profile._id,
|
|
1022
|
+
summaryLength: summaryText.length,
|
|
1023
|
+
model: result.model,
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Generate session labels (sentiment, categories, summary) and create a Lead record.
|
|
1028
|
+
* Called after session resolves. Fire-and-forget.
|
|
1029
|
+
*/
|
|
1030
|
+
async generateSessionLabels(sessionId) {
|
|
1031
|
+
logger_1.default.info('generateSessionLabels called', { sessionId });
|
|
1032
|
+
const session = await whatsapp_session_model_1.default.findById(sessionId);
|
|
1033
|
+
if (!session || session.message_count < 2) {
|
|
1034
|
+
logger_1.default.info('generateSessionLabels skipped — not enough messages', { sessionId, message_count: session?.message_count });
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const messages = await whatsapp_message_model_1.default.find({ session_id: session._id })
|
|
1038
|
+
.sort({ createdAt: 1 }).lean();
|
|
1039
|
+
if (messages.length < 2) {
|
|
1040
|
+
logger_1.default.info('generateSessionLabels skipped — fewer than 2 messages found', { sessionId, found: messages.length });
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const chat = await whatsapp_chat_model_1.default.findById(session.chat_id).lean();
|
|
1044
|
+
const contactName = chat?.contact_name || 'Hasta';
|
|
1045
|
+
const transcript = messages
|
|
1046
|
+
.map(m => {
|
|
1047
|
+
const role = m.sender === 'contact' ? contactName : 'ASISTAN';
|
|
1048
|
+
return `[${role}]: ${m.text}`;
|
|
1049
|
+
})
|
|
1050
|
+
.join('\n');
|
|
1051
|
+
const openai = new openai_1.default({ apiKey: config_1.default.ai.openai.apiKey });
|
|
1052
|
+
const response = await openai.chat.completions.create({
|
|
1053
|
+
model: 'gpt-5.4-mini',
|
|
1054
|
+
messages: [
|
|
1055
|
+
{ role: 'system', content: 'Sen bir saglik kliniginin WhatsApp konusmalarini analiz eden uzman bir asistansin. Yanitini YALNIZCA gecerli JSON olarak ver.' },
|
|
1056
|
+
{ role: 'user', content: `Asagidaki WhatsApp konusmasini dikkatle oku. ODAK NOKTASI: Hastanin (kullanicinin) talep ve niyetini anla. Asistanin/AI'in verdigi cevaplari degil, HASTANIN ne istedigini, ne sordugunu, ne talep ettigini analiz et.
|
|
1057
|
+
|
|
1058
|
+
KURALLAR:
|
|
1059
|
+
- Yanitini YALNIZCA gecerli JSON olarak ver. Baska hicbir sey yazma (markdown, backtick, aciklama yok).
|
|
1060
|
+
- Tum metin alanlari Turkce olmali.
|
|
1061
|
+
- null yerine bos string kullanma — bilinmeyen degerler icin null yaz.
|
|
1062
|
+
- Kategoriyi HASTANIN TALEBINE gore sec, asistanin cevabina gore degil.
|
|
1063
|
+
|
|
1064
|
+
JSON SEMASI:
|
|
1065
|
+
{
|
|
1066
|
+
"ozet": "Hastanin talebi ve gorusme sonucu (2-3 cumle): hasta ne istedi/sordu, ne oldu",
|
|
1067
|
+
"sentiment": "positive | neutral | negative",
|
|
1068
|
+
"categories": ["asagidaki listeden en uygun TEK kategoriyi sec"],
|
|
1069
|
+
"lead": {
|
|
1070
|
+
"name": "Hastanin ismi (mesajda veya rehber ismi: ${contactName} — eger '${contactName}' gercek bir isim degilse veya sadece numara ise null yaz)",
|
|
1071
|
+
"service_of_interest": "Hastanin sordugu/ilgilendigi spesifik hizmet veya tedavi (burun estetigi, dis beyazlatma, sac ekimi vb. — genel soruylaysa null)",
|
|
1072
|
+
"summary": "Hastanin talebi ve gorusme sonucu: hasta ne istedi/sordu, sonuc ne oldu (1-2 cumle)"
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
KATEGORI TANIMLARI — hastanin asil talebine gore sec:
|
|
1077
|
+
- appointment_inquiry: Hasta randevu almak, degistirmek veya iptal etmek istiyor; muayene/tedavi icin gun/saat soruyor; "ne zaman gelebilirim" tarzi sorular
|
|
1078
|
+
- callback_required: Hasta bir konuda geri donus bekliyor; asistan "sizi ararız/bilgi veririz" demis; hasta "doktor beni arasin" veya "telefonla konusmak istiyorum" demis; henuz sonuclanan bir islem yok
|
|
1079
|
+
- other: Hastanin talebi yukaridaki kategorilere uymuyor — fiyat sorusu, genel bilgi, adres/ulasim, tesekkur, selamlasma vb.
|
|
1080
|
+
|
|
1081
|
+
SENTIMENT — hastanin tavrini ve gorusmenin sonucunu birlikte degerlendir:
|
|
1082
|
+
- positive: Hasta memnun ayrildi, istedigini aldi, randevu kesinlesti, sorulari cevaplandi
|
|
1083
|
+
- neutral: Standart bilgi alis-verisi, hasta ne memnun ne memnuniyetsiz
|
|
1084
|
+
- negative: Hasta sikayetci, kizgin, istedigini alamadi, gorusme olumsuz sonuclandi
|
|
1085
|
+
|
|
1086
|
+
Konusma:
|
|
1087
|
+
${transcript}` },
|
|
1088
|
+
],
|
|
1089
|
+
max_completion_tokens: 500,
|
|
1090
|
+
temperature: 0.3,
|
|
1091
|
+
});
|
|
1092
|
+
const content = response.choices[0].message.content || '';
|
|
1093
|
+
const update = {};
|
|
1094
|
+
try {
|
|
1095
|
+
const parsed = JSON.parse(content);
|
|
1096
|
+
update.llm_summary = parsed.ozet || content;
|
|
1097
|
+
if (['positive', 'neutral', 'negative'].includes(parsed.sentiment)) {
|
|
1098
|
+
update.sentiment = parsed.sentiment;
|
|
1099
|
+
}
|
|
1100
|
+
const cats = Array.isArray(parsed.categories) && parsed.categories.length > 0 ? parsed.categories : ['other'];
|
|
1101
|
+
update.categories = cats;
|
|
1102
|
+
// Create Lead record
|
|
1103
|
+
if (parsed.lead && chat) {
|
|
1104
|
+
try {
|
|
1105
|
+
await lead_model_1.default.create({
|
|
1106
|
+
tenant_id: session.tenant_id,
|
|
1107
|
+
source_type: 'whatsapp',
|
|
1108
|
+
source_id: session._id,
|
|
1109
|
+
chat_id: chat._id,
|
|
1110
|
+
name: parsed.lead.name || null,
|
|
1111
|
+
phone: chat.contact_phone || '',
|
|
1112
|
+
service_of_interest: parsed.lead.service_of_interest || null,
|
|
1113
|
+
channel: 'whatsapp',
|
|
1114
|
+
categories: cats,
|
|
1115
|
+
summary: parsed.lead.summary || null,
|
|
1116
|
+
});
|
|
1117
|
+
logger_1.default.info('Lead created from WhatsApp session', { sessionId, phone: chat.contact_phone });
|
|
1118
|
+
}
|
|
1119
|
+
catch (err) {
|
|
1120
|
+
const code = err.code;
|
|
1121
|
+
if (code === 11000) {
|
|
1122
|
+
logger_1.default.info('Lead already exists for session', { sessionId });
|
|
1123
|
+
}
|
|
1124
|
+
else {
|
|
1125
|
+
throw err;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
catch {
|
|
1131
|
+
update.llm_summary = content || null;
|
|
1132
|
+
}
|
|
1133
|
+
await whatsapp_session_model_1.default.updateOne({ _id: session._id }, { $set: update });
|
|
1134
|
+
logger_1.default.info('Session labels generated', {
|
|
1135
|
+
sessionId,
|
|
1136
|
+
sentiment: update.sentiment,
|
|
1137
|
+
categories: update.categories,
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
// ─── Connection Management ────────────────────────────────
|
|
1141
|
+
closeConnection(unipileChatId) {
|
|
1142
|
+
const conn = this.connections.get(unipileChatId);
|
|
1143
|
+
if (conn) {
|
|
1144
|
+
if (conn.pingTimer) {
|
|
1145
|
+
clearInterval(conn.pingTimer);
|
|
1146
|
+
conn.pingTimer = null;
|
|
1147
|
+
}
|
|
1148
|
+
conn.intentionallyClosed = true;
|
|
1149
|
+
conn.ws.close();
|
|
1150
|
+
this.connections.delete(unipileChatId);
|
|
1151
|
+
this.cancelMessageBuffer(conn.sessionId);
|
|
1152
|
+
logger_1.default.info('Closed EL connection for chat', { unipileChatId });
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
// ─── Client Tool Call Handling ───────────────────────────
|
|
1156
|
+
async handleClientToolCall(conn, toolCallId, toolName, parameters) {
|
|
1157
|
+
let result;
|
|
1158
|
+
let isError = false;
|
|
1159
|
+
try {
|
|
1160
|
+
switch (toolName) {
|
|
1161
|
+
case 'forward_to_operator':
|
|
1162
|
+
result = await this.toolForwardToOperator(conn, parameters);
|
|
1163
|
+
break;
|
|
1164
|
+
case 'forward_conversation_to_operator':
|
|
1165
|
+
result = await this.toolForwardConversationToOperator(conn, parameters);
|
|
1166
|
+
break;
|
|
1167
|
+
case 'send_example_photo':
|
|
1168
|
+
result = await this.toolSendExamplePhoto(conn);
|
|
1169
|
+
break;
|
|
1170
|
+
case 'call_patient':
|
|
1171
|
+
result = await this.toolCallPatient(conn, parameters);
|
|
1172
|
+
break;
|
|
1173
|
+
case 'check_availability':
|
|
1174
|
+
result = await this.toolCheckAvailability(conn, parameters);
|
|
1175
|
+
break;
|
|
1176
|
+
case 'reserve_slot':
|
|
1177
|
+
result = await this.toolReserveSlot(conn, parameters);
|
|
1178
|
+
break;
|
|
1179
|
+
case 'list_reservations':
|
|
1180
|
+
result = await this.toolListReservations(conn, parameters);
|
|
1181
|
+
break;
|
|
1182
|
+
case 'cancel_reservation':
|
|
1183
|
+
result = await this.toolCancelReservation(conn, parameters);
|
|
1184
|
+
break;
|
|
1185
|
+
case 'check_patient_history':
|
|
1186
|
+
result = await this.toolCheckPatientHistory(conn, parameters);
|
|
1187
|
+
break;
|
|
1188
|
+
case 'reschedule_reservation':
|
|
1189
|
+
result = await this.toolRescheduleReservation(conn, parameters);
|
|
1190
|
+
break;
|
|
1191
|
+
default:
|
|
1192
|
+
result = JSON.stringify({ error: `Unknown tool: ${toolName}` });
|
|
1193
|
+
isError = true;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
catch (err) {
|
|
1197
|
+
result = JSON.stringify({ error: err.message });
|
|
1198
|
+
isError = true;
|
|
1199
|
+
}
|
|
1200
|
+
conn.ws.send(JSON.stringify({
|
|
1201
|
+
type: 'client_tool_result',
|
|
1202
|
+
tool_call_id: toolCallId,
|
|
1203
|
+
result,
|
|
1204
|
+
is_error: isError,
|
|
1205
|
+
}));
|
|
1206
|
+
logger_1.default.info('Client tool call completed', {
|
|
1207
|
+
toolName,
|
|
1208
|
+
toolCallId,
|
|
1209
|
+
sessionId: conn.sessionId,
|
|
1210
|
+
resultPreview: result.substring(0, 200),
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
async toolSendExamplePhoto(conn) {
|
|
1214
|
+
const session = await whatsapp_session_model_1.default.findById(conn.sessionId);
|
|
1215
|
+
if (!session)
|
|
1216
|
+
return JSON.stringify({ error: 'Session not found' });
|
|
1217
|
+
const chat = await whatsapp_chat_model_1.default.findById(conn.chatId);
|
|
1218
|
+
if (!chat)
|
|
1219
|
+
return JSON.stringify({ error: 'Chat not found' });
|
|
1220
|
+
const tenant = await tenant_model_1.default.findById(session.tenant_id);
|
|
1221
|
+
if (!tenant)
|
|
1222
|
+
return JSON.stringify({ error: 'Tenant not found' });
|
|
1223
|
+
const opSettings = tenant.settings?.whatsapp_agent;
|
|
1224
|
+
if (!opSettings?.operator_example_photo_enabled || !opSettings.operator_example_photo_base64) {
|
|
1225
|
+
return JSON.stringify({ error: 'Rehber fotoğraf yapılandırılmamış' });
|
|
1226
|
+
}
|
|
1227
|
+
const dataUrl = opSettings.operator_example_photo_base64;
|
|
1228
|
+
// Strip "data:image/...;base64," prefix
|
|
1229
|
+
const rawBase64 = dataUrl.replace(/^data:image\/[^;]+;base64,/, '');
|
|
1230
|
+
const caption = opSettings.operator_example_photo_caption || '';
|
|
1231
|
+
const provider = unipile_service_1.default.getProviderForTenant(tenant);
|
|
1232
|
+
try {
|
|
1233
|
+
await unipile_service_1.default.sendImage(provider, chat.unipile_chat_id, rawBase64, caption);
|
|
1234
|
+
// Pre-create message record so the Baileys echo webhook dedup catches it
|
|
1235
|
+
// and doesn't store it as an incoming 'contact' message
|
|
1236
|
+
await whatsapp_message_model_1.default.create({
|
|
1237
|
+
chat_id: chat._id,
|
|
1238
|
+
session_id: session._id,
|
|
1239
|
+
tenant_id: session.tenant_id,
|
|
1240
|
+
unipile_message_id: null,
|
|
1241
|
+
sender: 'ai',
|
|
1242
|
+
sender_name: 'AI',
|
|
1243
|
+
text: caption || '[Rehber fotoğraf]',
|
|
1244
|
+
media_type: 'imageMessage',
|
|
1245
|
+
sent_via_unipile: true,
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
catch (err) {
|
|
1249
|
+
logger_1.default.error('Failed to send example photo', { error: err.message });
|
|
1250
|
+
return JSON.stringify({ sent: false, error: err.message });
|
|
1251
|
+
}
|
|
1252
|
+
logger_1.default.info('Sent example photo to patient', { sessionId: conn.sessionId, chatId: conn.chatId });
|
|
1253
|
+
return JSON.stringify({ sent: true });
|
|
1254
|
+
}
|
|
1255
|
+
async toolForwardToOperator(conn, parameters) {
|
|
1256
|
+
const session = await whatsapp_session_model_1.default.findById(conn.sessionId);
|
|
1257
|
+
if (!session || session.status === 'resolved') {
|
|
1258
|
+
return JSON.stringify({ error: 'Session not found or already resolved' });
|
|
1259
|
+
}
|
|
1260
|
+
const chat = await whatsapp_chat_model_1.default.findById(conn.chatId);
|
|
1261
|
+
if (!chat) {
|
|
1262
|
+
return JSON.stringify({ error: 'Chat not found' });
|
|
1263
|
+
}
|
|
1264
|
+
const tenant = await tenant_model_1.default.findById(session.tenant_id);
|
|
1265
|
+
if (!tenant) {
|
|
1266
|
+
return JSON.stringify({ error: 'Tenant not found' });
|
|
1267
|
+
}
|
|
1268
|
+
const opSettings = tenant.settings?.whatsapp_agent;
|
|
1269
|
+
if (!opSettings?.operator_workflow_enabled || !opSettings.operator_phone) {
|
|
1270
|
+
return JSON.stringify({ error: 'Operator workflow not configured' });
|
|
1271
|
+
}
|
|
1272
|
+
const operatorPhone = opSettings.operator_phone;
|
|
1273
|
+
const prefix = opSettings.operator_ref_prefix || 'R';
|
|
1274
|
+
const timeoutHours = opSettings.operator_request_timeout_hours || 24;
|
|
1275
|
+
// Find ALL media messages from the patient in this session (oldest first)
|
|
1276
|
+
// Only include image/video/document types (skip stickers, contacts, locations etc.)
|
|
1277
|
+
const FORWARDABLE_TYPES = ['imageMessage', 'videoMessage', 'documentMessage'];
|
|
1278
|
+
const allMedia = await whatsapp_message_model_1.default.find({
|
|
1279
|
+
session_id: session._id,
|
|
1280
|
+
sender: 'contact',
|
|
1281
|
+
media_type: { $in: FORWARDABLE_TYPES },
|
|
1282
|
+
}).sort({ createdAt: 1 }).lean();
|
|
1283
|
+
logger_1.default.info('Media messages found for forwarding', {
|
|
1284
|
+
sessionId: session._id,
|
|
1285
|
+
count: allMedia.length,
|
|
1286
|
+
types: allMedia.map(m => m.media_type),
|
|
1287
|
+
ids: allMedia.map(m => m.unipile_message_id),
|
|
1288
|
+
});
|
|
1289
|
+
// Use session-level REF code (generated at session creation)
|
|
1290
|
+
const refCode = session.ref_code || await this.generateRefCode(tenant._id.toString(), prefix);
|
|
1291
|
+
// Create OperatorRequest with array fields
|
|
1292
|
+
await operator_request_model_1.default.create({
|
|
1293
|
+
tenant_id: tenant._id,
|
|
1294
|
+
session_id: session._id,
|
|
1295
|
+
chat_id: chat._id,
|
|
1296
|
+
ref_code: refCode,
|
|
1297
|
+
request_type: 'operator_review',
|
|
1298
|
+
status: 'pending',
|
|
1299
|
+
forwarded_media_types: allMedia.map(m => m.media_type || 'unknown'),
|
|
1300
|
+
forwarded_at: new Date(),
|
|
1301
|
+
forwarded_to_phone: operatorPhone,
|
|
1302
|
+
patient_message_ids: allMedia.map(m => m._id),
|
|
1303
|
+
forwarded_count: allMedia.length,
|
|
1304
|
+
failed_count: 0,
|
|
1305
|
+
expires_at: new Date(Date.now() + timeoutHours * 60 * 60 * 1000),
|
|
1306
|
+
});
|
|
1307
|
+
// Mark session as awaiting operator
|
|
1308
|
+
session.awaiting_operator = true;
|
|
1309
|
+
await session.save();
|
|
1310
|
+
const note = parameters.message || '';
|
|
1311
|
+
const provider = unipile_service_1.default.getProviderForTenant(tenant);
|
|
1312
|
+
const total = allMedia.length;
|
|
1313
|
+
let forwardedOk = 0;
|
|
1314
|
+
let failedCount = 0;
|
|
1315
|
+
try {
|
|
1316
|
+
if (total > 0) {
|
|
1317
|
+
for (let i = 0; i < total; i++) {
|
|
1318
|
+
const media = allMedia[i];
|
|
1319
|
+
const caption = i === 0
|
|
1320
|
+
? `REF: ${refCode}\nHasta: ${chat.contact_phone}\nHasta adı: ${chat.contact_name}${total > 1 ? `\nFotoğraf: 1/${total}` : ''}${note ? '\nNot: ' + note : ''}`
|
|
1321
|
+
: `REF: ${refCode} (${i + 1}/${total})`;
|
|
1322
|
+
try {
|
|
1323
|
+
await unipile_service_1.default.forwardMedia(provider, chat.unipile_chat_id, media.unipile_message_id, operatorPhone, caption);
|
|
1324
|
+
forwardedOk++;
|
|
1325
|
+
}
|
|
1326
|
+
catch (err) {
|
|
1327
|
+
failedCount++;
|
|
1328
|
+
logger_1.default.warn('Failed to forward media to operator', {
|
|
1329
|
+
refCode, index: i + 1, total,
|
|
1330
|
+
messageId: media.unipile_message_id,
|
|
1331
|
+
error: err.message,
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
// Small delay between sends to avoid WA rate limiting
|
|
1335
|
+
if (i < total - 1) {
|
|
1336
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
// No media — just send text to operator
|
|
1342
|
+
const accountId = tenant.settings.whatsapp_agent.unipile_account_id;
|
|
1343
|
+
if (accountId) {
|
|
1344
|
+
const operatorJid = operatorPhone.replace(/[^0-9]/g, '') + '@s.whatsapp.net';
|
|
1345
|
+
const operatorChatId = `${accountId}:${operatorJid}`;
|
|
1346
|
+
const caption = `REF: ${refCode}\nHasta: ${chat.contact_phone}\nHasta adı: ${chat.contact_name}${note ? '\nNot: ' + note : ''}`;
|
|
1347
|
+
await unipile_service_1.default.sendMessage(provider, operatorChatId, caption);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
catch (err) {
|
|
1352
|
+
logger_1.default.error('Failed to forward to operator', { refCode, error: err.message });
|
|
1353
|
+
return JSON.stringify({ ref_code: refCode, forwarded: false, error: err.message });
|
|
1354
|
+
}
|
|
1355
|
+
// Update failed count on the request
|
|
1356
|
+
if (failedCount > 0) {
|
|
1357
|
+
await operator_request_model_1.default.updateOne({ ref_code: refCode, tenant_id: tenant._id }, { $set: { failed_count: failedCount } });
|
|
1358
|
+
}
|
|
1359
|
+
logger_1.default.info('Forwarded to operator via tool call', {
|
|
1360
|
+
refCode, sessionId: conn.sessionId, total_media: total,
|
|
1361
|
+
forwarded_ok: forwardedOk, failed: failedCount,
|
|
1362
|
+
});
|
|
1363
|
+
return JSON.stringify({
|
|
1364
|
+
ref_code: refCode,
|
|
1365
|
+
forwarded: true,
|
|
1366
|
+
total_media: total,
|
|
1367
|
+
forwarded_ok: forwardedOk,
|
|
1368
|
+
failed: failedCount,
|
|
1369
|
+
...(failedCount > 0 ? { failed_reason: `${failedCount} medya iletilemedi (önbellek süresi dolmuş olabilir)` } : {}),
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
async toolForwardConversationToOperator(conn, parameters) {
|
|
1373
|
+
const session = await whatsapp_session_model_1.default.findById(conn.sessionId);
|
|
1374
|
+
if (!session || session.status === 'resolved') {
|
|
1375
|
+
return JSON.stringify({ error: 'Session not found or already resolved' });
|
|
1376
|
+
}
|
|
1377
|
+
const chat = await whatsapp_chat_model_1.default.findById(conn.chatId);
|
|
1378
|
+
if (!chat)
|
|
1379
|
+
return JSON.stringify({ error: 'Chat not found' });
|
|
1380
|
+
const tenant = await tenant_model_1.default.findById(session.tenant_id);
|
|
1381
|
+
if (!tenant)
|
|
1382
|
+
return JSON.stringify({ error: 'Tenant not found' });
|
|
1383
|
+
if (!tenant.enabled_features.includes('conversation_forwarding')) {
|
|
1384
|
+
return JSON.stringify({ error: 'Conversation forwarding not enabled' });
|
|
1385
|
+
}
|
|
1386
|
+
const forwardPhone = tenant.settings?.whatsapp_agent?.conversation_forwarding_phone;
|
|
1387
|
+
if (!forwardPhone) {
|
|
1388
|
+
return JSON.stringify({ error: 'Conversation forwarding phone not configured' });
|
|
1389
|
+
}
|
|
1390
|
+
const summary = parameters.summary || '';
|
|
1391
|
+
const rawMessages = parameters.patient_messages;
|
|
1392
|
+
const patientMessages = Array.isArray(rawMessages)
|
|
1393
|
+
? rawMessages
|
|
1394
|
+
: (typeof rawMessages === 'string' && rawMessages ? [rawMessages] : []);
|
|
1395
|
+
if (!summary && patientMessages.length === 0) {
|
|
1396
|
+
return JSON.stringify({ error: 'summary or patient_messages required' });
|
|
1397
|
+
}
|
|
1398
|
+
const refCode = session.ref_code || await this.generateRefCode(tenant._id.toString(), tenant.settings?.whatsapp_agent?.operator_ref_prefix || 'R');
|
|
1399
|
+
const timeoutHours = tenant.settings?.whatsapp_agent?.operator_request_timeout_hours || 24;
|
|
1400
|
+
await operator_request_model_1.default.create({
|
|
1401
|
+
tenant_id: tenant._id,
|
|
1402
|
+
session_id: session._id,
|
|
1403
|
+
chat_id: chat._id,
|
|
1404
|
+
ref_code: refCode,
|
|
1405
|
+
request_type: 'conversation_review',
|
|
1406
|
+
status: 'pending',
|
|
1407
|
+
forwarded_at: new Date(),
|
|
1408
|
+
forwarded_to_phone: forwardPhone,
|
|
1409
|
+
forwarded_count: patientMessages.length,
|
|
1410
|
+
expires_at: new Date(Date.now() + timeoutHours * 60 * 60 * 1000),
|
|
1411
|
+
});
|
|
1412
|
+
session.awaiting_operator = true;
|
|
1413
|
+
if (!session.ref_code)
|
|
1414
|
+
session.ref_code = refCode;
|
|
1415
|
+
await session.save();
|
|
1416
|
+
// Build message for the doctor
|
|
1417
|
+
const lines = [`REF: ${refCode}`, `Hasta: ${chat.contact_phone}`, `Hasta adı: ${chat.contact_name}`];
|
|
1418
|
+
if (summary)
|
|
1419
|
+
lines.push('', `Özet: ${summary}`);
|
|
1420
|
+
if (patientMessages.length > 0) {
|
|
1421
|
+
lines.push('', 'Hasta mesajları:');
|
|
1422
|
+
for (const msg of patientMessages) {
|
|
1423
|
+
lines.push(`- ${msg}`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
const messageText = lines.join('\n');
|
|
1427
|
+
const provider = unipile_service_1.default.getProviderForTenant(tenant);
|
|
1428
|
+
const accountId = tenant.settings.whatsapp_agent.unipile_account_id;
|
|
1429
|
+
try {
|
|
1430
|
+
if (accountId) {
|
|
1431
|
+
const forwardJid = forwardPhone.replace(/[^0-9]/g, '') + '@s.whatsapp.net';
|
|
1432
|
+
const forwardChatId = `${accountId}:${forwardJid}`;
|
|
1433
|
+
await unipile_service_1.default.sendMessage(provider, forwardChatId, messageText);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
catch (err) {
|
|
1437
|
+
logger_1.default.error('Failed to forward conversation to operator', { refCode, error: err.message });
|
|
1438
|
+
return JSON.stringify({ ref_code: refCode, forwarded: false, error: err.message });
|
|
1439
|
+
}
|
|
1440
|
+
logger_1.default.info('Forwarded conversation to operator', {
|
|
1441
|
+
refCode, sessionId: conn.sessionId, messageCount: patientMessages.length,
|
|
1442
|
+
});
|
|
1443
|
+
return JSON.stringify({
|
|
1444
|
+
ref_code: refCode,
|
|
1445
|
+
forwarded: true,
|
|
1446
|
+
message_count: patientMessages.length,
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
async generateRefCode(tenantId, prefix) {
|
|
1450
|
+
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
1451
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
1452
|
+
const rand = Array.from({ length: 4 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
|
|
1453
|
+
const code = `${prefix}-${rand}`;
|
|
1454
|
+
const [inRequest, inSession] = await Promise.all([
|
|
1455
|
+
operator_request_model_1.default.exists({ ref_code: code, tenant_id: tenantId }),
|
|
1456
|
+
whatsapp_session_model_1.default.exists({ ref_code: code, tenant_id: tenantId }),
|
|
1457
|
+
]);
|
|
1458
|
+
if (!inRequest && !inSession)
|
|
1459
|
+
return code;
|
|
1460
|
+
}
|
|
1461
|
+
return `${prefix}-${Date.now().toString(36).slice(-4).toUpperCase()}`;
|
|
1462
|
+
}
|
|
1463
|
+
// ─── Outbound Call Tool ──────────────────────────────────
|
|
1464
|
+
async toolCallPatient(conn, parameters) {
|
|
1465
|
+
const session = await whatsapp_session_model_1.default.findById(conn.sessionId);
|
|
1466
|
+
if (!session || session.status === 'resolved') {
|
|
1467
|
+
return JSON.stringify({ error: 'Session not found or already resolved' });
|
|
1468
|
+
}
|
|
1469
|
+
const chat = await whatsapp_chat_model_1.default.findById(conn.chatId);
|
|
1470
|
+
if (!chat) {
|
|
1471
|
+
return JSON.stringify({ error: 'Chat not found' });
|
|
1472
|
+
}
|
|
1473
|
+
const tenant = await tenant_model_1.default.findById(session.tenant_id);
|
|
1474
|
+
if (!tenant) {
|
|
1475
|
+
return JSON.stringify({ error: 'Tenant not found' });
|
|
1476
|
+
}
|
|
1477
|
+
const callSettings = tenant.settings?.whatsapp_agent;
|
|
1478
|
+
if (!callSettings?.outbound_call_enabled) {
|
|
1479
|
+
return JSON.stringify({ error: 'Outbound calling not enabled' });
|
|
1480
|
+
}
|
|
1481
|
+
if (!callSettings.outbound_call_phone_number_id) {
|
|
1482
|
+
return JSON.stringify({ error: 'Outbound call not fully configured (missing phone_number_id)' });
|
|
1483
|
+
}
|
|
1484
|
+
// Resolve the voice agent — use dedicated call agent or fall back to chat agent
|
|
1485
|
+
let voiceAgentId = null;
|
|
1486
|
+
if (callSettings.outbound_call_agent_id) {
|
|
1487
|
+
const callAgent = await agent_model_1.default.findById(callSettings.outbound_call_agent_id).lean();
|
|
1488
|
+
if (callAgent?.is_active) {
|
|
1489
|
+
voiceAgentId = callAgent.elevenlabs_agent_id;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
if (!voiceAgentId) {
|
|
1493
|
+
voiceAgentId = conn.agentId;
|
|
1494
|
+
}
|
|
1495
|
+
if (!voiceAgentId) {
|
|
1496
|
+
return JSON.stringify({ error: 'No agent available for voice call' });
|
|
1497
|
+
}
|
|
1498
|
+
// Format patient phone as E.164 for Twilio (e.g. +905551234567)
|
|
1499
|
+
const rawPhone = chat.contact_phone.replace(/[^0-9+]/g, '');
|
|
1500
|
+
const patientPhone = rawPhone.startsWith('+') ? rawPhone : `+${rawPhone}`;
|
|
1501
|
+
if (patientPhone.length < 8) {
|
|
1502
|
+
return JSON.stringify({ error: 'Patient phone number not available' });
|
|
1503
|
+
}
|
|
1504
|
+
// Build full conversation context (messages + operator events) for the voice agent
|
|
1505
|
+
const reason = parameters.reason || '';
|
|
1506
|
+
const chatContext = await this.buildChatContext(conn.chatId, conn.sessionId) || '';
|
|
1507
|
+
try {
|
|
1508
|
+
const result = await elevenlabs_service_1.default.sipTrunkOutboundCall({
|
|
1509
|
+
agentId: voiceAgentId,
|
|
1510
|
+
agentPhoneNumberId: callSettings.outbound_call_phone_number_id,
|
|
1511
|
+
toNumber: patientPhone,
|
|
1512
|
+
conversationInitiationClientData: {
|
|
1513
|
+
dynamicVariables: {
|
|
1514
|
+
patient_name: chat.contact_name || 'Hasta',
|
|
1515
|
+
patient_phone: patientPhone,
|
|
1516
|
+
conversation_history: chatContext,
|
|
1517
|
+
call_reason: reason,
|
|
1518
|
+
},
|
|
1519
|
+
},
|
|
1520
|
+
});
|
|
1521
|
+
logger_1.default.info('Outbound call initiated via tool', {
|
|
1522
|
+
sessionId: conn.sessionId,
|
|
1523
|
+
patientPhone,
|
|
1524
|
+
conversationId: result?.conversationId,
|
|
1525
|
+
sipCallId: result?.sipCallId,
|
|
1526
|
+
voiceAgentId,
|
|
1527
|
+
message: result?.message,
|
|
1528
|
+
});
|
|
1529
|
+
// Parse SIP failure from message (e.g. "INVITE failed: sip status: 486: User Busy (SIP 486)")
|
|
1530
|
+
const sipFailure = this.parseSipFailure(result?.message || '');
|
|
1531
|
+
if (sipFailure) {
|
|
1532
|
+
return JSON.stringify({
|
|
1533
|
+
success: false,
|
|
1534
|
+
reason: sipFailure,
|
|
1535
|
+
conversation_id: result?.conversationId || null,
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
return JSON.stringify({
|
|
1539
|
+
success: true,
|
|
1540
|
+
conversation_id: result?.conversationId || null,
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
catch (err) {
|
|
1544
|
+
logger_1.default.error('Failed to initiate outbound call', {
|
|
1545
|
+
sessionId: conn.sessionId,
|
|
1546
|
+
patientPhone,
|
|
1547
|
+
error: err.message,
|
|
1548
|
+
});
|
|
1549
|
+
return JSON.stringify({ error: 'Call failed: ' + err.message });
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Parse SIP error from the outbound call response message.
|
|
1554
|
+
* Returns a human-readable Turkish reason if it's a SIP failure, null if call succeeded.
|
|
1555
|
+
*/
|
|
1556
|
+
parseSipFailure(message) {
|
|
1557
|
+
if (!message)
|
|
1558
|
+
return null;
|
|
1559
|
+
const sipMatch = message.match(/sip\s*status:\s*(\d{3})/i);
|
|
1560
|
+
if (!sipMatch)
|
|
1561
|
+
return null;
|
|
1562
|
+
const code = parseInt(sipMatch[1], 10);
|
|
1563
|
+
// 2xx = success
|
|
1564
|
+
if (code >= 200 && code < 300)
|
|
1565
|
+
return null;
|
|
1566
|
+
const reasons = {
|
|
1567
|
+
486: 'Hasta meşgul, telefonu kullanıyor',
|
|
1568
|
+
487: 'Arama iptal edildi',
|
|
1569
|
+
480: 'Hasta şu an ulaşılamıyor',
|
|
1570
|
+
408: 'Hasta yanıt vermedi, zaman aşımı',
|
|
1571
|
+
603: 'Hasta aramayı reddetti',
|
|
1572
|
+
404: 'Numara bulunamadı',
|
|
1573
|
+
503: 'Telefon hizmeti geçici olarak kullanılamıyor',
|
|
1574
|
+
504: 'Bağlantı zaman aşımına uğradı',
|
|
1575
|
+
};
|
|
1576
|
+
return reasons[code] || `Arama başarısız oldu (SIP ${code})`;
|
|
1577
|
+
}
|
|
1578
|
+
// ─── Google Calendar Tools ──────────────────────────────
|
|
1579
|
+
async toolCheckAvailability(conn, parameters) {
|
|
1580
|
+
const session = await whatsapp_session_model_1.default.findById(conn.sessionId);
|
|
1581
|
+
if (!session)
|
|
1582
|
+
return JSON.stringify({ error: 'Session not found' });
|
|
1583
|
+
const tenant = await tenant_model_1.default.findById(session.tenant_id);
|
|
1584
|
+
if (!tenant)
|
|
1585
|
+
return JSON.stringify({ error: 'Tenant not found' });
|
|
1586
|
+
const gcal = tenant.settings?.whatsapp_agent?.google_calendar;
|
|
1587
|
+
if (!gcal?.connected || !gcal?.calendar_id) {
|
|
1588
|
+
return JSON.stringify({ error: 'Google Calendar not configured' });
|
|
1589
|
+
}
|
|
1590
|
+
const date = parameters.date;
|
|
1591
|
+
if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
1592
|
+
return JSON.stringify({ error: 'Invalid date format. Use YYYY-MM-DD' });
|
|
1593
|
+
}
|
|
1594
|
+
const today = new Date().toISOString().split('T')[0];
|
|
1595
|
+
if (date < today) {
|
|
1596
|
+
return JSON.stringify({ error: `Geçmiş tarih için randevu oluşturulamaz. Bugünün tarihi: ${today}` });
|
|
1597
|
+
}
|
|
1598
|
+
const result = await google_calendar_service_1.default.checkAvailability(tenant._id.toString(), date);
|
|
1599
|
+
if (result.slots.length === 0) {
|
|
1600
|
+
return JSON.stringify({
|
|
1601
|
+
date,
|
|
1602
|
+
available_slots: [],
|
|
1603
|
+
message: result.message || 'Bu tarihte müsait randevu yok',
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
return JSON.stringify({
|
|
1607
|
+
date,
|
|
1608
|
+
available_slots: result.slots,
|
|
1609
|
+
appointment_duration_minutes: gcal.appointment_duration_minutes,
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
async toolReserveSlot(conn, parameters) {
|
|
1613
|
+
const session = await whatsapp_session_model_1.default.findById(conn.sessionId);
|
|
1614
|
+
if (!session)
|
|
1615
|
+
return JSON.stringify({ error: 'Session not found' });
|
|
1616
|
+
const tenant = await tenant_model_1.default.findById(session.tenant_id);
|
|
1617
|
+
if (!tenant)
|
|
1618
|
+
return JSON.stringify({ error: 'Tenant not found' });
|
|
1619
|
+
const gcal = tenant.settings?.whatsapp_agent?.google_calendar;
|
|
1620
|
+
if (!gcal?.connected || !gcal?.calendar_id) {
|
|
1621
|
+
return JSON.stringify({ error: 'Google Calendar not configured' });
|
|
1622
|
+
}
|
|
1623
|
+
const chat = await whatsapp_chat_model_1.default.findById(conn.chatId);
|
|
1624
|
+
const patientName = parameters.patient_name
|
|
1625
|
+
|| chat?.contact_name
|
|
1626
|
+
|| 'Bilinmeyen Hasta';
|
|
1627
|
+
const patientPhone = chat?.contact_phone || '';
|
|
1628
|
+
const isoStart = parameters.iso_start;
|
|
1629
|
+
const isoEnd = parameters.iso_end;
|
|
1630
|
+
const notes = parameters.notes || '';
|
|
1631
|
+
if (!isoStart || !isoEnd) {
|
|
1632
|
+
return JSON.stringify({ error: 'iso_start and iso_end are required' });
|
|
1633
|
+
}
|
|
1634
|
+
// Pre-generate appointment ID so we can include the detail link in the calendar event
|
|
1635
|
+
const appointmentId = new mongoose_1.default.Types.ObjectId();
|
|
1636
|
+
const result = await google_calendar_service_1.default.reserveSlot(tenant._id.toString(), { iso_start: isoStart, iso_end: isoEnd, patient_name: patientName, patient_phone: patientPhone, notes, appointment_id: appointmentId.toString() });
|
|
1637
|
+
// Persist appointment locally for analytics
|
|
1638
|
+
appointment_model_1.default.create({
|
|
1639
|
+
_id: appointmentId,
|
|
1640
|
+
tenant_id: session.tenant_id,
|
|
1641
|
+
chat_id: session.chat_id,
|
|
1642
|
+
session_id: session._id,
|
|
1643
|
+
google_event_id: result.event_id,
|
|
1644
|
+
patient_name: patientName,
|
|
1645
|
+
patient_phone: patientPhone,
|
|
1646
|
+
start_time: new Date(result.start),
|
|
1647
|
+
end_time: new Date(result.end),
|
|
1648
|
+
duration_minutes: gcal.appointment_duration_minutes,
|
|
1649
|
+
notes,
|
|
1650
|
+
status: 'scheduled',
|
|
1651
|
+
}).catch(err => logger_1.default.error('Failed to persist appointment', { error: err.message }));
|
|
1652
|
+
// SMS notification to doctor (fire-and-forget)
|
|
1653
|
+
const smsSettings = tenant.settings?.whatsapp_agent;
|
|
1654
|
+
if (smsSettings?.sms_notification_enabled && smsSettings.sms_notification_phones?.length) {
|
|
1655
|
+
const startDt = new Date(result.start);
|
|
1656
|
+
const tz = gcal.timezone || 'Europe/Istanbul';
|
|
1657
|
+
netgsm_service_1.default.notifyAppointmentCreated({
|
|
1658
|
+
phones: smsSettings.sms_notification_phones,
|
|
1659
|
+
patientName,
|
|
1660
|
+
patientPhone,
|
|
1661
|
+
date: startDt.toLocaleDateString('tr-TR', { day: 'numeric', month: 'long', year: 'numeric', weekday: 'long', timeZone: tz }),
|
|
1662
|
+
time: startDt.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit', timeZone: tz }),
|
|
1663
|
+
notes,
|
|
1664
|
+
detailUrl: `${config_1.default.google.frontendUrl}/appointments/${appointmentId}`,
|
|
1665
|
+
}).catch(err => logger_1.default.error('SMS notification failed (create)', { error: err.message }));
|
|
1666
|
+
}
|
|
1667
|
+
return JSON.stringify({
|
|
1668
|
+
reserved: true,
|
|
1669
|
+
event_id: result.event_id,
|
|
1670
|
+
start: result.start,
|
|
1671
|
+
end: result.end,
|
|
1672
|
+
patient_name: patientName,
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
async toolListReservations(conn, _parameters) {
|
|
1676
|
+
const session = await whatsapp_session_model_1.default.findById(conn.sessionId);
|
|
1677
|
+
if (!session)
|
|
1678
|
+
return JSON.stringify({ error: 'Session not found' });
|
|
1679
|
+
const tenant = await tenant_model_1.default.findById(session.tenant_id);
|
|
1680
|
+
if (!tenant)
|
|
1681
|
+
return JSON.stringify({ error: 'Tenant not found' });
|
|
1682
|
+
const gcal = tenant.settings?.whatsapp_agent?.google_calendar;
|
|
1683
|
+
if (!gcal?.connected || !gcal?.calendar_id) {
|
|
1684
|
+
return JSON.stringify({ error: 'Google Calendar not configured' });
|
|
1685
|
+
}
|
|
1686
|
+
const chat = await whatsapp_chat_model_1.default.findById(conn.chatId);
|
|
1687
|
+
const patientPhone = chat?.contact_phone || '';
|
|
1688
|
+
if (!patientPhone) {
|
|
1689
|
+
return JSON.stringify({ error: 'Patient phone not available' });
|
|
1690
|
+
}
|
|
1691
|
+
const reservations = await google_calendar_service_1.default.listReservations(tenant._id.toString(), patientPhone);
|
|
1692
|
+
return JSON.stringify({
|
|
1693
|
+
reservations,
|
|
1694
|
+
count: reservations.length,
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
async toolCancelReservation(conn, parameters) {
|
|
1698
|
+
const session = await whatsapp_session_model_1.default.findById(conn.sessionId);
|
|
1699
|
+
if (!session)
|
|
1700
|
+
return JSON.stringify({ error: 'Session not found' });
|
|
1701
|
+
const tenant = await tenant_model_1.default.findById(session.tenant_id);
|
|
1702
|
+
if (!tenant)
|
|
1703
|
+
return JSON.stringify({ error: 'Tenant not found' });
|
|
1704
|
+
const gcal = tenant.settings?.whatsapp_agent?.google_calendar;
|
|
1705
|
+
if (!gcal?.connected || !gcal?.calendar_id) {
|
|
1706
|
+
return JSON.stringify({ error: 'Google Calendar not configured' });
|
|
1707
|
+
}
|
|
1708
|
+
const chat = await whatsapp_chat_model_1.default.findById(conn.chatId);
|
|
1709
|
+
const patientPhone = chat?.contact_phone || '';
|
|
1710
|
+
const eventId = parameters.event_id;
|
|
1711
|
+
if (!eventId) {
|
|
1712
|
+
return JSON.stringify({ error: 'event_id is required' });
|
|
1713
|
+
}
|
|
1714
|
+
const cancelled = await google_calendar_service_1.default.cancelReservation(tenant._id.toString(), eventId, patientPhone);
|
|
1715
|
+
if (cancelled) {
|
|
1716
|
+
// Update local DB
|
|
1717
|
+
const appt = await appointment_model_1.default.findOneAndUpdate({ google_event_id: eventId, tenant_id: session.tenant_id }, { status: 'cancelled', cancelled_at: new Date() }).lean();
|
|
1718
|
+
// SMS notification to doctor (fire-and-forget)
|
|
1719
|
+
const smsSettings = tenant.settings?.whatsapp_agent;
|
|
1720
|
+
if (appt && smsSettings?.sms_notification_enabled && smsSettings.sms_notification_phones?.length) {
|
|
1721
|
+
const tz = gcal.timezone || 'Europe/Istanbul';
|
|
1722
|
+
netgsm_service_1.default.notifyAppointmentCancelled({
|
|
1723
|
+
phones: smsSettings.sms_notification_phones,
|
|
1724
|
+
patientName: appt.patient_name,
|
|
1725
|
+
patientPhone,
|
|
1726
|
+
date: appt.start_time.toLocaleDateString('tr-TR', { day: 'numeric', month: 'long', weekday: 'long', timeZone: tz }),
|
|
1727
|
+
time: appt.start_time.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit', timeZone: tz }),
|
|
1728
|
+
}).catch(err => logger_1.default.error('SMS notification failed (cancel)', { error: err.message }));
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
return JSON.stringify({ cancelled, event_id: eventId });
|
|
1732
|
+
}
|
|
1733
|
+
async toolRescheduleReservation(conn, parameters) {
|
|
1734
|
+
const session = await whatsapp_session_model_1.default.findById(conn.sessionId);
|
|
1735
|
+
if (!session)
|
|
1736
|
+
return JSON.stringify({ error: 'Session not found' });
|
|
1737
|
+
const tenant = await tenant_model_1.default.findById(session.tenant_id);
|
|
1738
|
+
if (!tenant)
|
|
1739
|
+
return JSON.stringify({ error: 'Tenant not found' });
|
|
1740
|
+
const gcal = tenant.settings?.whatsapp_agent?.google_calendar;
|
|
1741
|
+
if (!gcal?.connected || !gcal?.calendar_id) {
|
|
1742
|
+
return JSON.stringify({ error: 'Google Calendar not configured' });
|
|
1743
|
+
}
|
|
1744
|
+
const chat = await whatsapp_chat_model_1.default.findById(conn.chatId);
|
|
1745
|
+
const patientPhone = chat?.contact_phone || '';
|
|
1746
|
+
const patientName = parameters.patient_name || chat?.contact_name || 'Bilinmeyen Hasta';
|
|
1747
|
+
const oldEventId = parameters.old_event_id;
|
|
1748
|
+
const newIsoStart = parameters.new_iso_start;
|
|
1749
|
+
const newIsoEnd = parameters.new_iso_end;
|
|
1750
|
+
const notes = parameters.notes || '';
|
|
1751
|
+
if (!oldEventId) {
|
|
1752
|
+
return JSON.stringify({ error: 'old_event_id is required' });
|
|
1753
|
+
}
|
|
1754
|
+
if (!newIsoStart || !newIsoEnd) {
|
|
1755
|
+
return JSON.stringify({ error: 'new_iso_start and new_iso_end are required' });
|
|
1756
|
+
}
|
|
1757
|
+
// Fetch old appointment info for SMS before cancelling
|
|
1758
|
+
const oldAppt = await appointment_model_1.default.findOne({ google_event_id: oldEventId, tenant_id: session.tenant_id }).lean();
|
|
1759
|
+
// Step 1: Cancel old reservation
|
|
1760
|
+
try {
|
|
1761
|
+
await google_calendar_service_1.default.cancelReservation(tenant._id.toString(), oldEventId, patientPhone);
|
|
1762
|
+
appointment_model_1.default.updateOne({ google_event_id: oldEventId, tenant_id: session.tenant_id }, { status: 'cancelled', cancelled_at: new Date() }).catch(err => logger_1.default.error('Failed to update appointment status', { error: err.message }));
|
|
1763
|
+
logger_1.default.info('Reschedule: old reservation cancelled', { oldEventId, sessionId: conn.sessionId });
|
|
1764
|
+
}
|
|
1765
|
+
catch (err) {
|
|
1766
|
+
return JSON.stringify({ error: `Eski randevu iptal edilemedi: ${err.message}` });
|
|
1767
|
+
}
|
|
1768
|
+
// Step 2: Create new reservation
|
|
1769
|
+
const appointmentId = new mongoose_1.default.Types.ObjectId();
|
|
1770
|
+
try {
|
|
1771
|
+
const result = await google_calendar_service_1.default.reserveSlot(tenant._id.toString(), { iso_start: newIsoStart, iso_end: newIsoEnd, patient_name: patientName, patient_phone: patientPhone, notes, appointment_id: appointmentId.toString() });
|
|
1772
|
+
appointment_model_1.default.create({
|
|
1773
|
+
_id: appointmentId,
|
|
1774
|
+
tenant_id: session.tenant_id,
|
|
1775
|
+
chat_id: session.chat_id,
|
|
1776
|
+
session_id: session._id,
|
|
1777
|
+
google_event_id: result.event_id,
|
|
1778
|
+
patient_name: patientName,
|
|
1779
|
+
patient_phone: patientPhone,
|
|
1780
|
+
start_time: new Date(result.start),
|
|
1781
|
+
end_time: new Date(result.end),
|
|
1782
|
+
duration_minutes: gcal.appointment_duration_minutes,
|
|
1783
|
+
notes,
|
|
1784
|
+
status: 'scheduled',
|
|
1785
|
+
}).catch(err => logger_1.default.error('Failed to persist rescheduled appointment', { error: err.message }));
|
|
1786
|
+
// SMS notification to doctor (fire-and-forget)
|
|
1787
|
+
const smsSettings = tenant.settings?.whatsapp_agent;
|
|
1788
|
+
if (smsSettings?.sms_notification_enabled && smsSettings.sms_notification_phones?.length) {
|
|
1789
|
+
const tz = gcal.timezone || 'Europe/Istanbul';
|
|
1790
|
+
const newStartDt = new Date(result.start);
|
|
1791
|
+
netgsm_service_1.default.notifyAppointmentRescheduled({
|
|
1792
|
+
phones: smsSettings.sms_notification_phones,
|
|
1793
|
+
patientName,
|
|
1794
|
+
patientPhone,
|
|
1795
|
+
oldDate: oldAppt
|
|
1796
|
+
? oldAppt.start_time.toLocaleDateString('tr-TR', { day: 'numeric', month: 'long', weekday: 'long', timeZone: tz })
|
|
1797
|
+
: 'Bilinmiyor',
|
|
1798
|
+
oldTime: oldAppt
|
|
1799
|
+
? oldAppt.start_time.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit', timeZone: tz })
|
|
1800
|
+
: '',
|
|
1801
|
+
newDate: newStartDt.toLocaleDateString('tr-TR', { day: 'numeric', month: 'long', weekday: 'long', timeZone: tz }),
|
|
1802
|
+
newTime: newStartDt.toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit', timeZone: tz }),
|
|
1803
|
+
detailUrl: `${config_1.default.google.frontendUrl}/appointments/${appointmentId}`,
|
|
1804
|
+
}).catch(err => logger_1.default.error('SMS notification failed (reschedule)', { error: err.message }));
|
|
1805
|
+
}
|
|
1806
|
+
return JSON.stringify({
|
|
1807
|
+
rescheduled: true,
|
|
1808
|
+
old_event_id: oldEventId,
|
|
1809
|
+
new_event_id: result.event_id,
|
|
1810
|
+
start: result.start,
|
|
1811
|
+
end: result.end,
|
|
1812
|
+
patient_name: patientName,
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
catch (err) {
|
|
1816
|
+
return JSON.stringify({ error: `Eski randevu iptal edildi ancak yeni randevu oluşturulamadı: ${err.message}. Lütfen tekrar deneyin.` });
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
async toolCheckPatientHistory(conn, _parameters) {
|
|
1820
|
+
const session = await whatsapp_session_model_1.default.findById(conn.sessionId);
|
|
1821
|
+
if (!session)
|
|
1822
|
+
return JSON.stringify({ error: 'Session not found' });
|
|
1823
|
+
const chat = await whatsapp_chat_model_1.default.findById(conn.chatId);
|
|
1824
|
+
const patientPhone = chat?.contact_phone || '';
|
|
1825
|
+
if (!patientPhone) {
|
|
1826
|
+
return JSON.stringify({ error: 'Patient phone not available' });
|
|
1827
|
+
}
|
|
1828
|
+
const tenantId = session.tenant_id;
|
|
1829
|
+
// Find all chats for this patient phone under this tenant
|
|
1830
|
+
const patientChats = await whatsapp_chat_model_1.default.find({ tenant_id: tenantId, contact_phone: patientPhone }, { _id: 1 }).lean();
|
|
1831
|
+
const chatIds = patientChats.map(c => c._id);
|
|
1832
|
+
// Parallel queries
|
|
1833
|
+
const [contactProfile, appointments, operatorRequests, sessions] = await Promise.all([
|
|
1834
|
+
// Contact profile
|
|
1835
|
+
whatsapp_contact_profile_model_1.default.findOne({ tenant_id: tenantId, phone: patientPhone }, { display_name: 1, notes: 1, tags: 1, ai_summary: 1, preferred_language: 1, metadata: 1, first_seen_at: 1, last_seen_at: 1, total_sessions: 1, total_messages: 1 }).lean(),
|
|
1836
|
+
// Appointments: last 10 (any status)
|
|
1837
|
+
appointment_model_1.default.find({ tenant_id: tenantId, patient_phone: patientPhone }, { start_time: 1, end_time: 1, duration_minutes: 1, status: 1, notes: 1, cancelled_at: 1, createdAt: 1 }).sort({ start_time: -1 }).limit(10).lean(),
|
|
1838
|
+
// Operator requests (photo reviews etc.): last 10
|
|
1839
|
+
operator_request_model_1.default.find({ tenant_id: tenantId, chat_id: { $in: chatIds } }, { ref_code: 1, request_type: 1, status: 1, operator_response: 1, forwarded_at: 1, responded_at: 1 }).sort({ createdAt: -1 }).limit(10).lean(),
|
|
1840
|
+
// Past sessions: last 10
|
|
1841
|
+
whatsapp_session_model_1.default.find({ tenant_id: tenantId, chat_id: { $in: chatIds } }, { status: 1, started_at: 1, resolved_at: 1, resolved_by: 1, message_count: 1 }).sort({ started_at: -1 }).limit(10).lean(),
|
|
1842
|
+
]);
|
|
1843
|
+
const now = new Date();
|
|
1844
|
+
return JSON.stringify({
|
|
1845
|
+
patient_phone: patientPhone,
|
|
1846
|
+
contact_profile: contactProfile ? {
|
|
1847
|
+
display_name: contactProfile.display_name,
|
|
1848
|
+
notes: contactProfile.notes || undefined,
|
|
1849
|
+
tags: contactProfile.tags.length > 0 ? contactProfile.tags : undefined,
|
|
1850
|
+
ai_summary: contactProfile.ai_summary || undefined,
|
|
1851
|
+
preferred_language: contactProfile.preferred_language || undefined,
|
|
1852
|
+
metadata: contactProfile.metadata && Object.keys(contactProfile.metadata).length > 0 ? contactProfile.metadata : undefined,
|
|
1853
|
+
first_seen: contactProfile.first_seen_at?.toISOString().split('T')[0],
|
|
1854
|
+
last_seen: contactProfile.last_seen_at?.toISOString().split('T')[0],
|
|
1855
|
+
total_sessions: contactProfile.total_sessions,
|
|
1856
|
+
total_messages: contactProfile.total_messages,
|
|
1857
|
+
} : null,
|
|
1858
|
+
appointments: appointments.map(a => ({
|
|
1859
|
+
date: a.start_time.toISOString().split('T')[0],
|
|
1860
|
+
time: a.start_time.toISOString().split('T')[1].substring(0, 5),
|
|
1861
|
+
duration_minutes: a.duration_minutes,
|
|
1862
|
+
status: a.status,
|
|
1863
|
+
notes: a.notes || undefined,
|
|
1864
|
+
is_upcoming: a.start_time > now && a.status === 'scheduled',
|
|
1865
|
+
})),
|
|
1866
|
+
operator_requests: operatorRequests.map(r => ({
|
|
1867
|
+
ref_code: r.ref_code,
|
|
1868
|
+
type: r.request_type,
|
|
1869
|
+
status: r.status,
|
|
1870
|
+
response: r.operator_response || undefined,
|
|
1871
|
+
date: r.forwarded_at.toISOString().split('T')[0],
|
|
1872
|
+
})),
|
|
1873
|
+
total_sessions: sessions.length,
|
|
1874
|
+
total_appointments: appointments.length,
|
|
1875
|
+
total_cancelled: appointments.filter(a => a.status === 'cancelled').length,
|
|
1876
|
+
upcoming_appointments: appointments.filter(a => a.start_time > now && a.status === 'scheduled').length,
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
// ─── Operator Workflow ────────────────────────────────────
|
|
1880
|
+
/**
|
|
1881
|
+
* Inject the operator's (doctor's) response into the patient's AI session.
|
|
1882
|
+
* Creates a new WS connection if the previous one timed out during the wait.
|
|
1883
|
+
*/
|
|
1884
|
+
async injectOperatorResponse(params) {
|
|
1885
|
+
const { tenantId, sessionId, chatId, operatorResponse, refCode, hasDocument } = params;
|
|
1886
|
+
// Reload session and chat from DB
|
|
1887
|
+
const session = await whatsapp_session_model_1.default.findById(sessionId);
|
|
1888
|
+
if (!session || session.status === 'resolved') {
|
|
1889
|
+
logger_1.default.warn('Session not found or already resolved for operator response', { sessionId, refCode });
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
const chat = await whatsapp_chat_model_1.default.findById(chatId);
|
|
1893
|
+
if (!chat) {
|
|
1894
|
+
logger_1.default.warn('Chat not found for operator response', { chatId, refCode });
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
// Resolve agent ID
|
|
1898
|
+
const tenant = await tenant_model_1.default.findById(tenantId).lean();
|
|
1899
|
+
if (!tenant)
|
|
1900
|
+
return;
|
|
1901
|
+
const agentId = await resolveAgentId(tenant);
|
|
1902
|
+
if (!agentId) {
|
|
1903
|
+
logger_1.default.warn('No active agent for operator response injection', { tenantId, refCode });
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
const unipileChatId = chat.unipile_chat_id;
|
|
1907
|
+
let conn = this.connections.get(unipileChatId);
|
|
1908
|
+
const isAlive = conn && conn.ws.readyState === ws_1.default.OPEN && conn.ready;
|
|
1909
|
+
let isNewConnection = false;
|
|
1910
|
+
if (!isAlive) {
|
|
1911
|
+
// Connection died while waiting for doctor — recreate it
|
|
1912
|
+
if (conn) {
|
|
1913
|
+
if (conn.pingTimer) {
|
|
1914
|
+
clearInterval(conn.pingTimer);
|
|
1915
|
+
conn.pingTimer = null;
|
|
1916
|
+
}
|
|
1917
|
+
conn.intentionallyClosed = true;
|
|
1918
|
+
try {
|
|
1919
|
+
conn.ws.close();
|
|
1920
|
+
}
|
|
1921
|
+
catch { /* ignore */ }
|
|
1922
|
+
this.connections.delete(unipileChatId);
|
|
1923
|
+
}
|
|
1924
|
+
try {
|
|
1925
|
+
conn = await this.createConnection(agentId, chat, session, { isReconnect: true });
|
|
1926
|
+
this.connections.set(unipileChatId, conn);
|
|
1927
|
+
isNewConnection = true;
|
|
1928
|
+
}
|
|
1929
|
+
catch (err) {
|
|
1930
|
+
logger_1.default.error('Failed to create EL connection for operator response', {
|
|
1931
|
+
error: err.message,
|
|
1932
|
+
sessionId,
|
|
1933
|
+
refCode,
|
|
1934
|
+
});
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
conn.lastActivity = Date.now();
|
|
1939
|
+
await whatsapp_session_model_1.default.updateOne({ _id: session._id }, { elevenlabs_last_interaction_at: new Date() });
|
|
1940
|
+
// Send chat context as backup (continuation directive handles it in system prompt if enabled)
|
|
1941
|
+
if (isNewConnection) {
|
|
1942
|
+
await this.sendChatContext(conn, chat._id.toString(), sessionId);
|
|
1943
|
+
}
|
|
1944
|
+
// Inject the operator's response
|
|
1945
|
+
const docNote = hasDocument
|
|
1946
|
+
? ' Ayrıca hastaya bir belge/dosya da gönderildi — hastaya belgeyi kontrol etmesini söyle.'
|
|
1947
|
+
: '';
|
|
1948
|
+
conn.ws.send(JSON.stringify({
|
|
1949
|
+
type: 'contextual_update',
|
|
1950
|
+
text: `Estetisyen yanıt verdi (Ref: ${refCode}): ${operatorResponse}.${docNote} Bu bilgiyi hastaya doğal ve sıcak bir şekilde ilet.`,
|
|
1951
|
+
}));
|
|
1952
|
+
// Send user_message to prod AI to generate a response
|
|
1953
|
+
conn.ws.send(JSON.stringify({
|
|
1954
|
+
type: 'user_message',
|
|
1955
|
+
text: 'Estetisyenden yanıt geldi.',
|
|
1956
|
+
}));
|
|
1957
|
+
// Clear awaiting_operator flag
|
|
1958
|
+
session.awaiting_operator = false;
|
|
1959
|
+
await session.save();
|
|
1960
|
+
logger_1.default.info('Injected operator response into AI session', {
|
|
1961
|
+
sessionId,
|
|
1962
|
+
refCode,
|
|
1963
|
+
isNewConnection,
|
|
1964
|
+
responsePreview: operatorResponse.substring(0, 100),
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Sweep expired operator requests: set to 'expired', clear awaiting_operator,
|
|
1969
|
+
* and notify the AI to inform the patient.
|
|
1970
|
+
*/
|
|
1971
|
+
async sweepExpiredOperatorRequests() {
|
|
1972
|
+
const expiredRequests = await operator_request_model_1.default.find({
|
|
1973
|
+
status: 'pending',
|
|
1974
|
+
expires_at: { $lt: new Date() },
|
|
1975
|
+
}).lean();
|
|
1976
|
+
for (const request of expiredRequests) {
|
|
1977
|
+
await operator_request_model_1.default.updateOne({ _id: request._id }, { status: 'expired' });
|
|
1978
|
+
// Clear awaiting_operator on the session
|
|
1979
|
+
const session = await whatsapp_session_model_1.default.findById(request.session_id);
|
|
1980
|
+
if (session && session.status === 'active') {
|
|
1981
|
+
session.awaiting_operator = false;
|
|
1982
|
+
await session.save();
|
|
1983
|
+
// Try to notify AI
|
|
1984
|
+
const chat = await whatsapp_chat_model_1.default.findById(request.chat_id);
|
|
1985
|
+
if (chat) {
|
|
1986
|
+
const conn = this.connections.get(chat.unipile_chat_id);
|
|
1987
|
+
if (conn && conn.ws.readyState === ws_1.default.OPEN && conn.ready) {
|
|
1988
|
+
conn.ws.send(JSON.stringify({
|
|
1989
|
+
type: 'contextual_update',
|
|
1990
|
+
text: `Doktordan yanıt alınamadı (Ref: ${request.ref_code} süresi doldu). Hastaya durumu bildir ve daha sonra dönüş yapılacağını söyle.`,
|
|
1991
|
+
}));
|
|
1992
|
+
conn.ws.send(JSON.stringify({
|
|
1993
|
+
type: 'user_message',
|
|
1994
|
+
text: 'Doktordan henüz yanıt gelmedi.',
|
|
1995
|
+
}));
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
logger_1.default.info('Expired operator request', {
|
|
2000
|
+
refCode: request.ref_code,
|
|
2001
|
+
sessionId: request.session_id,
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
if (expiredRequests.length > 0) {
|
|
2005
|
+
logger_1.default.info('Operator request expiry sweep complete', { expired: expiredRequests.length });
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
cleanupIdle() {
|
|
2009
|
+
const now = Date.now();
|
|
2010
|
+
for (const [unipileChatId, conn] of this.connections) {
|
|
2011
|
+
if (now - conn.lastActivity > this.idleTimeoutMs) {
|
|
2012
|
+
logger_1.default.info('Closing idle EL connection', { unipileChatId, sessionId: conn.sessionId });
|
|
2013
|
+
if (conn.pingTimer) {
|
|
2014
|
+
clearInterval(conn.pingTimer);
|
|
2015
|
+
conn.pingTimer = null;
|
|
2016
|
+
}
|
|
2017
|
+
conn.intentionallyClosed = true;
|
|
2018
|
+
conn.ws.close();
|
|
2019
|
+
this.connections.delete(unipileChatId);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
getActiveConnectionCount() {
|
|
2024
|
+
return this.connections.size;
|
|
2025
|
+
}
|
|
2026
|
+
shutdown() {
|
|
2027
|
+
if (this.cleanupInterval)
|
|
2028
|
+
clearInterval(this.cleanupInterval);
|
|
2029
|
+
if (this.sweepInterval)
|
|
2030
|
+
clearInterval(this.sweepInterval);
|
|
2031
|
+
if (this.inactivitySweepInterval)
|
|
2032
|
+
clearInterval(this.inactivitySweepInterval);
|
|
2033
|
+
if (this.costSweepInterval)
|
|
2034
|
+
clearInterval(this.costSweepInterval);
|
|
2035
|
+
for (const [, timer] of this.sessionTimers) {
|
|
2036
|
+
clearTimeout(timer);
|
|
2037
|
+
}
|
|
2038
|
+
this.sessionTimers.clear();
|
|
2039
|
+
for (const [, timer] of this.messageBufferTimers) {
|
|
2040
|
+
clearTimeout(timer);
|
|
2041
|
+
}
|
|
2042
|
+
this.messageBufferTimers.clear();
|
|
2043
|
+
for (const [, conn] of this.connections) {
|
|
2044
|
+
if (conn.pingTimer)
|
|
2045
|
+
clearInterval(conn.pingTimer);
|
|
2046
|
+
conn.intentionallyClosed = true;
|
|
2047
|
+
conn.ws.close();
|
|
2048
|
+
}
|
|
2049
|
+
this.connections.clear();
|
|
2050
|
+
logger_1.default.info('WhatsAppAgentService shut down');
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
const whatsappAgentService = new WhatsAppAgentService();
|
|
2054
|
+
exports.default = whatsappAgentService;
|
|
2055
|
+
//# sourceMappingURL=whatsapp-agent.service.js.map
|