feat(reports): breakdown auto/manual no BotReports + nomes corretos
Engrenagem fechada e nomes que casam com o que a métrica mede: Cards superiores (taxas): - "Tempo de resolução" -> "Resolvidas pelo bot %" Tooltip: bot resolveu sozinho (sem humano via Chatwoot ou WhatsApp) / total - "Taxa de entrega" -> "Transferidas pra humano %" Tooltip: agora soma auto (Jasmine chamou) + manual (humano respondeu sem handoff explícito). Junto com a resolução, fecha ~100%. Cards de detalhe (segunda linha, contagem absoluta): - "Resolvidas pelo bot" — quantas o bot fechou sozinho - "Transferência automática (Jasmine)" — bot_handoff explícito (loop, timeout, max turns, intent) - "Tomada manual (agente)" — humano respondeu (UI ou WhatsApp echo) SEM a Jasmine ter chamado bot_handoff. Era o "bucket fantasma" antes. Backend: - BotMetricsBuilder.metrics inclui bot_resolutions_count, auto_handoffs_count, manual_takeovers_count - handoff_rate agora é (auto + manual) / total — daí a engrenagem fechar - manual_takeovers_count: conversas com mensagem outgoing humana (sender_type='User' OR NULL) MENOS as que tiveram conversation_bot_handoff Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e9b8b6e587
commit
617eadbfe4
@ -12,7 +12,10 @@ class V2::Reports::BotMetricsBuilder
|
|||||||
conversation_count: bot_conversations.count,
|
conversation_count: bot_conversations.count,
|
||||||
message_count: bot_messages.count,
|
message_count: bot_messages.count,
|
||||||
resolution_rate: bot_resolution_rate.to_i,
|
resolution_rate: bot_resolution_rate.to_i,
|
||||||
handoff_rate: bot_handoff_rate.to_i
|
handoff_rate: total_handoff_rate.to_i,
|
||||||
|
bot_resolutions_count: bot_resolutions_count,
|
||||||
|
auto_handoffs_count: auto_handoffs_count,
|
||||||
|
manual_takeovers_count: manual_takeovers_count
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -44,11 +47,40 @@ class V2::Reports::BotMetricsBuilder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def bot_resolutions_count
|
def bot_resolutions_count
|
||||||
base_reporting_events.joins(:conversation).select(:conversation_id).where(name: :conversation_bot_resolved).distinct.count
|
@bot_resolutions_count ||= base_reporting_events.joins(:conversation)
|
||||||
|
.select(:conversation_id)
|
||||||
|
.where(name: :conversation_bot_resolved)
|
||||||
|
.distinct.count
|
||||||
end
|
end
|
||||||
|
|
||||||
def bot_handoffs_count
|
# Auto handoff = Jasmine called bot_handoff! explicitly (loop, timeout, max_turns, intent)
|
||||||
base_reporting_events.joins(:conversation).select(:conversation_id).where(name: :conversation_bot_handoff).distinct.count
|
def auto_handoffs_count
|
||||||
|
@auto_handoffs_count ||= base_reporting_events.joins(:conversation)
|
||||||
|
.select(:conversation_id)
|
||||||
|
.where(name: :conversation_bot_handoff)
|
||||||
|
.distinct.count
|
||||||
|
end
|
||||||
|
|
||||||
|
# Manual takeover = a human replied (via Chatwoot UI or WhatsApp echo) WITHOUT a bot_handoff
|
||||||
|
# event being emitted for the same conversation. The bot itself uses sender_type 'Captain::Assistant',
|
||||||
|
# so it's never counted here.
|
||||||
|
def manual_takeovers_count
|
||||||
|
@manual_takeovers_count ||= begin
|
||||||
|
conv_ids_with_human_reply = bot_conversations
|
||||||
|
.joins(:messages)
|
||||||
|
.where(messages: { message_type: :outgoing })
|
||||||
|
.where('messages.sender_type = ? OR messages.sender_type IS NULL', 'User')
|
||||||
|
.distinct
|
||||||
|
.pluck(:id)
|
||||||
|
|
||||||
|
conv_ids_with_auto_handoff = ReportingEvent.unscope(:order)
|
||||||
|
.where(name: 'conversation_bot_handoff',
|
||||||
|
conversation_id: conv_ids_with_human_reply)
|
||||||
|
.distinct
|
||||||
|
.pluck(:conversation_id)
|
||||||
|
|
||||||
|
(conv_ids_with_human_reply - conv_ids_with_auto_handoff).count
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def bot_resolution_rate
|
def bot_resolution_rate
|
||||||
@ -57,9 +89,10 @@ class V2::Reports::BotMetricsBuilder
|
|||||||
bot_resolutions_count.to_f / bot_conversations.count * 100
|
bot_resolutions_count.to_f / bot_conversations.count * 100
|
||||||
end
|
end
|
||||||
|
|
||||||
def bot_handoff_rate
|
# Total handoff = auto + manual (the gear that closes the math now)
|
||||||
|
def total_handoff_rate
|
||||||
return 0 if bot_conversations.count.zero?
|
return 0 if bot_conversations.count.zero?
|
||||||
|
|
||||||
bot_handoffs_count.to_f / bot_conversations.count * 100
|
(auto_handoffs_count + manual_takeovers_count).to_f / bot_conversations.count * 100
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -36,11 +36,11 @@
|
|||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"BOT_RESOLUTION_COUNT": {
|
"BOT_RESOLUTION_COUNT": {
|
||||||
"NAME": "Resolution Count",
|
"NAME": "Resolved by bot",
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"BOT_HANDOFF_COUNT": {
|
"BOT_HANDOFF_COUNT": {
|
||||||
"NAME": "Handoff Count",
|
"NAME": "Transferred to human",
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"REPLY_TIME": {
|
"REPLY_TIME": {
|
||||||
@ -534,20 +534,32 @@
|
|||||||
"HEADER": "Bot Reports",
|
"HEADER": "Bot Reports",
|
||||||
"METRIC": {
|
"METRIC": {
|
||||||
"TOTAL_CONVERSATIONS": {
|
"TOTAL_CONVERSATIONS": {
|
||||||
"LABEL": "No. of Conversations",
|
"LABEL": "Conversations",
|
||||||
"TOOLTIP": "Total number of conversations handled by the bot"
|
"TOOLTIP": "Total number of conversations handled by the bot in the period"
|
||||||
},
|
},
|
||||||
"TOTAL_RESPONSES": {
|
"TOTAL_RESPONSES": {
|
||||||
"LABEL": "Total Responses",
|
"LABEL": "Outgoing messages",
|
||||||
"TOOLTIP": "Total number of responses sent by the bot"
|
"TOOLTIP": "Total number of outgoing messages — includes the bot AND humans (Chatwoot UI or WhatsApp echo)"
|
||||||
},
|
},
|
||||||
"RESOLUTION_RATE": {
|
"RESOLUTION_RATE": {
|
||||||
"LABEL": "Resolution Rate",
|
"LABEL": "Resolved by bot %",
|
||||||
"TOOLTIP": "Total number of conversations resolved by the bot / Total number of conversations handled by the bot * 100"
|
"TOOLTIP": "Conversations the bot resolved alone (no human reply, via UI or WhatsApp) ÷ total conversations × 100"
|
||||||
},
|
},
|
||||||
"HANDOFF_RATE": {
|
"HANDOFF_RATE": {
|
||||||
"LABEL": "Handoff Rate",
|
"LABEL": "Transferred to human %",
|
||||||
"TOOLTIP": "Total number of conversations handed off to agents / Total number of conversations handled by the bot * 100"
|
"TOOLTIP": "Conversations transferred to human (auto by Jasmine + manual takeover) ÷ total conversations × 100. Together with the resolution rate, the gear closes the math (the rest are still open, snoozed, or abandoned)."
|
||||||
|
},
|
||||||
|
"BOT_RESOLUTIONS": {
|
||||||
|
"LABEL": "Resolved by bot",
|
||||||
|
"TOOLTIP": "Absolute count: conversations the bot closed alone, with no human reply (UI or WhatsApp)"
|
||||||
|
},
|
||||||
|
"AUTO_HANDOFFS": {
|
||||||
|
"LABEL": "Auto handoff (Jasmine)",
|
||||||
|
"TOOLTIP": "Conversations where Jasmine explicitly called bot_handoff! — typically tool loop, timeout, max turns reached or LLM intent classified as handoff"
|
||||||
|
},
|
||||||
|
"MANUAL_TAKEOVERS": {
|
||||||
|
"LABEL": "Manual takeover (agent)",
|
||||||
|
"TOOLTIP": "Conversations where a human replied (Chatwoot UI or WhatsApp echo) without Jasmine triggering bot_handoff! first — the agent took over silently"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -36,11 +36,11 @@
|
|||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"BOT_RESOLUTION_COUNT": {
|
"BOT_RESOLUTION_COUNT": {
|
||||||
"NAME": "Contagem de Resolução",
|
"NAME": "Resolvidas pelo bot",
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"BOT_HANDOFF_COUNT": {
|
"BOT_HANDOFF_COUNT": {
|
||||||
"NAME": "Contagem de transferências",
|
"NAME": "Transferidas para humano",
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"REPLY_TIME": {
|
"REPLY_TIME": {
|
||||||
@ -466,20 +466,32 @@
|
|||||||
"HEADER": "Relatórios do Bot",
|
"HEADER": "Relatórios do Bot",
|
||||||
"METRIC": {
|
"METRIC": {
|
||||||
"TOTAL_CONVERSATIONS": {
|
"TOTAL_CONVERSATIONS": {
|
||||||
"LABEL": "Nº de Conversas",
|
"LABEL": "Conversas",
|
||||||
"TOOLTIP": "Número total de conversas tratadas pelo bot"
|
"TOOLTIP": "Total de conversas atendidas pelo bot no período"
|
||||||
},
|
},
|
||||||
"TOTAL_RESPONSES": {
|
"TOTAL_RESPONSES": {
|
||||||
"LABEL": "Total de respostas",
|
"LABEL": "Mensagens enviadas",
|
||||||
"TOOLTIP": "Número total de respostas enviadas pelo bot"
|
"TOOLTIP": "Total de mensagens enviadas — inclui o bot E humanos (via Chatwoot ou eco do WhatsApp)"
|
||||||
},
|
},
|
||||||
"RESOLUTION_RATE": {
|
"RESOLUTION_RATE": {
|
||||||
"LABEL": "Tempo de resolução",
|
"LABEL": "Resolvidas pelo bot %",
|
||||||
"TOOLTIP": "Número total de conversas resolvidas pelo bot / número total de conversas manipuladas pelo bot * 100"
|
"TOOLTIP": "Conversas que o bot resolveu sozinho (sem humano respondendo, via Chatwoot ou WhatsApp) ÷ total de conversas × 100"
|
||||||
},
|
},
|
||||||
"HANDOFF_RATE": {
|
"HANDOFF_RATE": {
|
||||||
"LABEL": "Taxa de entrega",
|
"LABEL": "Transferidas pra humano %",
|
||||||
"TOOLTIP": "Número total de conversas entregues a agentes / número total de conversas mantidas pelo bot * 100"
|
"TOOLTIP": "Conversas transferidas pra humano (auto pela Jasmine + tomada manual) ÷ total de conversas × 100. Junto com a taxa de resolução fecha a engrenagem (o resto está aberto, em snooze ou abandonado)."
|
||||||
|
},
|
||||||
|
"BOT_RESOLUTIONS": {
|
||||||
|
"LABEL": "Resolvidas pelo bot",
|
||||||
|
"TOOLTIP": "Contagem absoluta: conversas que o bot fechou sozinho, sem humano respondendo (via Chatwoot ou WhatsApp)"
|
||||||
|
},
|
||||||
|
"AUTO_HANDOFFS": {
|
||||||
|
"LABEL": "Transferência automática (Jasmine)",
|
||||||
|
"TOOLTIP": "Conversas em que a Jasmine chamou bot_handoff! explicitamente — geralmente loop de ferramenta, timeout, limite de turnos ou intent do LLM classificado como handoff"
|
||||||
|
},
|
||||||
|
"MANUAL_TAKEOVERS": {
|
||||||
|
"LABEL": "Tomada manual (agente)",
|
||||||
|
"TOOLTIP": "Conversas em que um humano respondeu (Chatwoot ou eco do WhatsApp) SEM a Jasmine ter chamado bot_handoff! antes — o agente assumiu silenciosamente"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -12,8 +12,11 @@ const props = defineProps({
|
|||||||
|
|
||||||
const conversationCount = ref('0');
|
const conversationCount = ref('0');
|
||||||
const messageCount = ref('0');
|
const messageCount = ref('0');
|
||||||
const resolutionRate = ref('0');
|
const botResolutionRate = ref('0');
|
||||||
const handoffRate = ref('0');
|
const humanTransferRate = ref('0');
|
||||||
|
const botResolutionsCount = ref('0');
|
||||||
|
const autoHandoffsCount = ref('0');
|
||||||
|
const manualTakeoversCount = ref('0');
|
||||||
|
|
||||||
const formatToPercent = value => {
|
const formatToPercent = value => {
|
||||||
return value ? `${value}%` : '--';
|
return value ? `${value}%` : '--';
|
||||||
@ -26,8 +29,17 @@ const fetchMetrics = () => {
|
|||||||
ReportsAPI.getBotMetrics(props.filters).then(response => {
|
ReportsAPI.getBotMetrics(props.filters).then(response => {
|
||||||
conversationCount.value = response.data.conversation_count.toLocaleString();
|
conversationCount.value = response.data.conversation_count.toLocaleString();
|
||||||
messageCount.value = response.data.message_count.toLocaleString();
|
messageCount.value = response.data.message_count.toLocaleString();
|
||||||
resolutionRate.value = response.data.resolution_rate.toString();
|
botResolutionRate.value = response.data.resolution_rate.toString();
|
||||||
handoffRate.value = response.data.handoff_rate.toString();
|
humanTransferRate.value = response.data.handoff_rate.toString();
|
||||||
|
botResolutionsCount.value = (
|
||||||
|
response.data.bot_resolutions_count || 0
|
||||||
|
).toLocaleString();
|
||||||
|
autoHandoffsCount.value = (
|
||||||
|
response.data.auto_handoffs_count || 0
|
||||||
|
).toLocaleString();
|
||||||
|
manualTakeoversCount.value = (
|
||||||
|
response.data.manual_takeovers_count || 0
|
||||||
|
).toLocaleString();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,6 +49,7 @@ onMounted(fetchMetrics);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
class="flex flex-wrap mx-0 shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2 px-6 py-5"
|
class="flex flex-wrap mx-0 shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2 px-6 py-5"
|
||||||
>
|
>
|
||||||
@ -55,14 +68,38 @@ onMounted(fetchMetrics);
|
|||||||
<ReportMetricCard
|
<ReportMetricCard
|
||||||
:label="$t('BOT_REPORTS.METRIC.RESOLUTION_RATE.LABEL')"
|
:label="$t('BOT_REPORTS.METRIC.RESOLUTION_RATE.LABEL')"
|
||||||
:info-text="$t('BOT_REPORTS.METRIC.RESOLUTION_RATE.TOOLTIP')"
|
:info-text="$t('BOT_REPORTS.METRIC.RESOLUTION_RATE.TOOLTIP')"
|
||||||
:value="formatToPercent(resolutionRate)"
|
:value="formatToPercent(botResolutionRate)"
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
/>
|
/>
|
||||||
<ReportMetricCard
|
<ReportMetricCard
|
||||||
:label="$t('BOT_REPORTS.METRIC.HANDOFF_RATE.LABEL')"
|
:label="$t('BOT_REPORTS.METRIC.HANDOFF_RATE.LABEL')"
|
||||||
:info-text="$t('BOT_REPORTS.METRIC.HANDOFF_RATE.TOOLTIP')"
|
:info-text="$t('BOT_REPORTS.METRIC.HANDOFF_RATE.TOOLTIP')"
|
||||||
:value="formatToPercent(handoffRate)"
|
:value="formatToPercent(humanTransferRate)"
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap mx-0 shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2 px-6 py-5"
|
||||||
|
>
|
||||||
|
<ReportMetricCard
|
||||||
|
:label="$t('BOT_REPORTS.METRIC.BOT_RESOLUTIONS.LABEL')"
|
||||||
|
:info-text="$t('BOT_REPORTS.METRIC.BOT_RESOLUTIONS.TOOLTIP')"
|
||||||
|
:value="botResolutionsCount"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<ReportMetricCard
|
||||||
|
:label="$t('BOT_REPORTS.METRIC.AUTO_HANDOFFS.LABEL')"
|
||||||
|
:info-text="$t('BOT_REPORTS.METRIC.AUTO_HANDOFFS.TOOLTIP')"
|
||||||
|
:value="autoHandoffsCount"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<ReportMetricCard
|
||||||
|
:label="$t('BOT_REPORTS.METRIC.MANUAL_TAKEOVERS.LABEL')"
|
||||||
|
:info-text="$t('BOT_REPORTS.METRIC.MANUAL_TAKEOVERS.TOOLTIP')"
|
||||||
|
:value="manualTakeoversCount"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user