From 2fd19e9bae63dbb44d777a00de31d7dfeb4dced0 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Tue, 26 May 2026 12:12:56 -0300 Subject: [PATCH] FEAT: Roteamento para atendentes melhorado --- src/modules/admin/knowledge-base.service.ts | 4 +- .../whatsapp/whatsapp-assignment.service.ts | 129 ++++++++++++++++-- 2 files changed, 116 insertions(+), 17 deletions(-) diff --git a/src/modules/admin/knowledge-base.service.ts b/src/modules/admin/knowledge-base.service.ts index 387e0a6..9155133 100644 --- a/src/modules/admin/knowledge-base.service.ts +++ b/src/modules/admin/knowledge-base.service.ts @@ -31,7 +31,7 @@ export class KnowledgeBaseService implements OnModuleInit { ALTER TABLE bot_triage_intents ADD COLUMN IF NOT EXISTS response_message TEXT, ADD COLUMN IF NOT EXISTS resolution_question TEXT, - ADD COLUMN IF NOT EXISTS escalation_message TEXT NOT NULL DEFAULT 'Certo, vou encaminhar seu atendimento para um especialista no assunto.'; + ADD COLUMN IF NOT EXISTS escalation_message TEXT NOT NULL DEFAULT 'Certo, vou encaminhar seu atendimento para o time responsável.'; `).catch(() => undefined); await this.database.query(` @@ -529,7 +529,7 @@ export class KnowledgeBaseService implements OnModuleInit { audience_id, label, area_id, keywords, response_message, resolution_question, escalation_message, sort_order, active, updated_at ) - VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, 'Certo, vou encaminhar seu atendimento para um especialista no assunto.'), $8, TRUE, CURRENT_TIMESTAMP) + VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, 'Certo, vou encaminhar seu atendimento para o time responsável.'), $8, TRUE, CURRENT_TIMESTAMP) RETURNING * `, [ diff --git a/src/modules/whatsapp/whatsapp-assignment.service.ts b/src/modules/whatsapp/whatsapp-assignment.service.ts index 370929f..b555380 100644 --- a/src/modules/whatsapp/whatsapp-assignment.service.ts +++ b/src/modules/whatsapp/whatsapp-assignment.service.ts @@ -421,7 +421,7 @@ export class WhatsappAssignmentService implements OnModuleInit { return builderFlowResult; } - const configuredFlowResult = await this.routeConfiguredFlow(chatId, cleanMessage, current, messageId); + const configuredFlowResult = await this.routeConfiguredFlow(chatId, cleanMessage, current, messageId, variables); if (configuredFlowResult) { return configuredFlowResult; } @@ -429,7 +429,14 @@ export class WhatsappAssignmentService implements OnModuleInit { const detectedArea = await this.detectKnownArea(cleanMessage); if (detectedArea) { - const assignment = await this.queueChat(chatId, detectedArea.id, `Roteado automaticamente pelo ${VIRTUAL_AGENT_NAME}`); + const assignment = await this.queueChat( + chatId, + detectedArea.id, + this.buildVirtualAgentTransferNote({ + name: variables.nome, + intentLabel: detectedArea.name, + }), + ); await this.markBotRoute(chatId, messageId); const hasPreviousTriage = current?.status === 'bot_triage'; const botMessage = hasPreviousTriage @@ -453,7 +460,14 @@ export class WhatsappAssignmentService implements OnModuleInit { if (attempts >= 2) { const supportArea = await this.getAreaByName('Suporte'); - const assignment = await this.queueChat(chatId, supportArea.id, 'Roteado para suporte por falta de classificacao'); + const assignment = await this.queueChat( + chatId, + supportArea.id, + this.buildVirtualAgentTransferNote({ + name: variables.nome, + fallbackReason: 'não teve a solicitação classificada pelo Agente Virtual', + }), + ); await this.markBotRoute(chatId, messageId); return { @@ -590,7 +604,11 @@ export class WhatsappAssignmentService implements OnModuleInit { const assignment = await this.queueChat( chatId, areaId, - `Roteado pelo fluxo do ${VIRTUAL_AGENT_NAME}: ${matchedChild.title}`, + this.buildVirtualAgentTransferNote({ + name: variables.nome, + audienceLabel: currentNode.title, + intentLabel: matchedChild.title, + }), ); await this.markBotRoute(chatId, messageId); @@ -601,7 +619,7 @@ export class WhatsappAssignmentService implements OnModuleInit { 'Atendente virtual', VIRTUAL_AGENT_NAME, this.applyBotVariables( - matchedChild.message_text || `Certo, vou encaminhar seu atendimento para ${matchedChild.area_nome || 'a especialidade correta'}.`, + matchedChild.message_text || 'Certo, vou encaminhar seu atendimento para o time responsável.', variables, ), ), @@ -628,7 +646,11 @@ export class WhatsappAssignmentService implements OnModuleInit { const assignment = await this.queueChat( chatId, fallbackAreaId, - `Fallback do fluxo do ${VIRTUAL_AGENT_NAME}: ${currentNode.title}`, + this.buildVirtualAgentTransferNote({ + name: variables.nome, + audienceLabel: currentNode.title, + fallbackReason: 'não teve a solicitação classificada pelo Agente Virtual', + }), ); await this.markBotRoute(chatId, messageId); @@ -659,7 +681,13 @@ export class WhatsappAssignmentService implements OnModuleInit { }; } - private async routeConfiguredFlow(chatId: string, message: string, current: any, messageId?: string) { + private async routeConfiguredFlow( + chatId: string, + message: string, + current: any, + messageId?: string, + variables: BotMessageVariables = {}, + ) { const flow = await this.getActiveTriageFlow(); if (!flow || !flow.audiences.length) return null; @@ -694,7 +722,7 @@ export class WhatsappAssignmentService implements OnModuleInit { } if (current.triage_step === 'resolution' && current.triage_intent_id) { - return this.routeConfiguredResolution(chatId, message, flow, current, messageId); + return this.routeConfiguredResolution(chatId, message, flow, current, messageId, variables); } return this.routeConfiguredAudience(chatId, message, flow, current, messageId); @@ -778,14 +806,29 @@ export class WhatsappAssignmentService implements OnModuleInit { }; } - private async routeConfiguredResolution(chatId: string, message: string, flow: TriageFlow, current: any, messageId?: string) { - const intent = flow.audiences - .flatMap((audience) => audience.intents) - .find((item) => Number(item.id) === Number(current.triage_intent_id)); + private async routeConfiguredResolution( + chatId: string, + message: string, + flow: TriageFlow, + current: any, + messageId?: string, + variables: BotMessageVariables = {}, + ) { + const audience = flow.audiences.find((item) => + item.intents.some((intentItem) => Number(intentItem.id) === Number(current.triage_intent_id)), + ); + const intent = audience?.intents.find((item) => Number(item.id) === Number(current.triage_intent_id)); if (!intent) { const fallbackAreaId = flow.fallback_area_id || (await this.getAreaByName('Suporte')).id; - const assignment = await this.queueChat(chatId, fallbackAreaId, `Fallback configurado pelo ${VIRTUAL_AGENT_NAME}`); + const assignment = await this.queueChat( + chatId, + fallbackAreaId, + this.buildVirtualAgentTransferNote({ + name: variables.nome, + fallbackReason: 'não teve a solicitação classificada pelo Agente Virtual', + }), + ); await this.markBotRoute(chatId, messageId); return { assignment, @@ -846,7 +889,11 @@ export class WhatsappAssignmentService implements OnModuleInit { const assignment = await this.queueChat( chatId, intent.area_id, - `Cliente solicitou especialista apos orientacao do ${VIRTUAL_AGENT_NAME}: ${intent.label}`, + this.buildVirtualAgentTransferNote({ + name: variables.nome, + audienceLabel: audience?.label, + intentLabel: intent.label, + }), ); await this.markBotRoute(chatId, messageId); @@ -856,7 +903,7 @@ export class WhatsappAssignmentService implements OnModuleInit { botMessage: this.formatSenderMessage( 'Atendente virtual', VIRTUAL_AGENT_NAME, - intent.escalation_message || `Certo, vou encaminhar seu atendimento para ${intent.area_nome}.`, + intent.escalation_message || 'Certo, vou encaminhar seu atendimento para o time responsável.', ), }; } @@ -1249,6 +1296,58 @@ export class WhatsappAssignmentService implements OnModuleInit { return ['ferias'].includes(normalized) ? 'as' : 'o'; } + private buildVirtualAgentTransferNote(input: { + name?: string | null; + audienceLabel?: string | null; + intentLabel?: string | null; + fallbackReason?: string; + }) { + const subject = this.getFirstName(input.name) || 'Cliente'; + const audienceDescription = this.getAudienceDescription(input.audienceLabel); + const topic = this.getTopicLabel(input.intentLabel); + + if (audienceDescription && topic) { + return `${subject} ${audienceDescription} e quer saber sobre ${topic}`; + } + + if (topic) { + return `${subject} quer saber sobre ${topic}`; + } + + if (audienceDescription && input.fallbackReason) { + return `${subject} ${audienceDescription} e ${input.fallbackReason}`; + } + + if (input.fallbackReason) { + return `${subject} ${input.fallbackReason}`; + } + + return `Roteado pelo ${VIRTUAL_AGENT_NAME}`; + } + + private getAudienceDescription(value?: string | null) { + const normalized = this.normalize(value || ''); + + if (!normalized) return ''; + if (normalized.includes('ex-colaborador') || normalized.includes('ex colaborador')) return 'é um ex-colaborador'; + if (normalized.includes('candidato') || normalized.includes('vaga')) return 'é um candidato'; + if (normalized.includes('colaborador') || normalized.includes('funcionario')) return 'é um colaborador'; + + return ''; + } + + private getTopicLabel(value?: string | null) { + const text = this.cleanVariable(value); + const normalized = this.normalize(text); + const knownTopics: Record = { + beneficios: 'Benefícios', + ferias: 'Férias', + rescisao: 'Rescisão', + }; + + return knownTopics[normalized] || text; + } + private applyBotVariables(message: string, variables: BotMessageVariables) { const firstName = this.getFirstName(variables.nome); const fullName = this.cleanVariable(variables.nome);