FEAT: Roteamento para atendentes melhorado
All checks were successful
Deploy Dev / deploy (push) Successful in 3s

This commit is contained in:
Rafael Alves Lopes 2026-05-26 12:12:56 -03:00
parent d495698c02
commit 2fd19e9bae
2 changed files with 116 additions and 17 deletions

View File

@ -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 *
`,
[

View File

@ -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<string, string> = {
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);