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,
|
||||
message_count: bot_messages.count,
|
||||
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
|
||||
|
||||
@ -44,11 +47,40 @@ class V2::Reports::BotMetricsBuilder
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def bot_handoffs_count
|
||||
base_reporting_events.joins(:conversation).select(:conversation_id).where(name: :conversation_bot_handoff).distinct.count
|
||||
# Auto handoff = Jasmine called bot_handoff! explicitly (loop, timeout, max_turns, intent)
|
||||
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
|
||||
|
||||
def bot_resolution_rate
|
||||
@ -57,9 +89,10 @@ class V2::Reports::BotMetricsBuilder
|
||||
bot_resolutions_count.to_f / bot_conversations.count * 100
|
||||
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?
|
||||
|
||||
bot_handoffs_count.to_f / bot_conversations.count * 100
|
||||
(auto_handoffs_count + manual_takeovers_count).to_f / bot_conversations.count * 100
|
||||
end
|
||||
end
|
||||
|
||||
@ -36,11 +36,11 @@
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"BOT_RESOLUTION_COUNT": {
|
||||
"NAME": "Resolution Count",
|
||||
"NAME": "Resolved by bot",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"BOT_HANDOFF_COUNT": {
|
||||
"NAME": "Handoff Count",
|
||||
"NAME": "Transferred to human",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"REPLY_TIME": {
|
||||
@ -534,20 +534,32 @@
|
||||
"HEADER": "Bot Reports",
|
||||
"METRIC": {
|
||||
"TOTAL_CONVERSATIONS": {
|
||||
"LABEL": "No. of Conversations",
|
||||
"TOOLTIP": "Total number of conversations handled by the bot"
|
||||
"LABEL": "Conversations",
|
||||
"TOOLTIP": "Total number of conversations handled by the bot in the period"
|
||||
},
|
||||
"TOTAL_RESPONSES": {
|
||||
"LABEL": "Total Responses",
|
||||
"TOOLTIP": "Total number of responses sent by the bot"
|
||||
"LABEL": "Outgoing messages",
|
||||
"TOOLTIP": "Total number of outgoing messages — includes the bot AND humans (Chatwoot UI or WhatsApp echo)"
|
||||
},
|
||||
"RESOLUTION_RATE": {
|
||||
"LABEL": "Resolution Rate",
|
||||
"TOOLTIP": "Total number of conversations resolved by the bot / Total number of conversations handled by the bot * 100"
|
||||
"LABEL": "Resolved by bot %",
|
||||
"TOOLTIP": "Conversations the bot resolved alone (no human reply, via UI or WhatsApp) ÷ total conversations × 100"
|
||||
},
|
||||
"HANDOFF_RATE": {
|
||||
"LABEL": "Handoff Rate",
|
||||
"TOOLTIP": "Total number of conversations handed off to agents / Total number of conversations handled by the bot * 100"
|
||||
"LABEL": "Transferred to human %",
|
||||
"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 )"
|
||||
},
|
||||
"BOT_RESOLUTION_COUNT": {
|
||||
"NAME": "Contagem de Resolução",
|
||||
"NAME": "Resolvidas pelo bot",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"BOT_HANDOFF_COUNT": {
|
||||
"NAME": "Contagem de transferências",
|
||||
"NAME": "Transferidas para humano",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"REPLY_TIME": {
|
||||
@ -466,20 +466,32 @@
|
||||
"HEADER": "Relatórios do Bot",
|
||||
"METRIC": {
|
||||
"TOTAL_CONVERSATIONS": {
|
||||
"LABEL": "Nº de Conversas",
|
||||
"TOOLTIP": "Número total de conversas tratadas pelo bot"
|
||||
"LABEL": "Conversas",
|
||||
"TOOLTIP": "Total de conversas atendidas pelo bot no período"
|
||||
},
|
||||
"TOTAL_RESPONSES": {
|
||||
"LABEL": "Total de respostas",
|
||||
"TOOLTIP": "Número total de respostas enviadas pelo bot"
|
||||
"LABEL": "Mensagens enviadas",
|
||||
"TOOLTIP": "Total de mensagens enviadas — inclui o bot E humanos (via Chatwoot ou eco do WhatsApp)"
|
||||
},
|
||||
"RESOLUTION_RATE": {
|
||||
"LABEL": "Tempo de resolução",
|
||||
"TOOLTIP": "Número total de conversas resolvidas pelo bot / número total de conversas manipuladas pelo bot * 100"
|
||||
"LABEL": "Resolvidas pelo bot %",
|
||||
"TOOLTIP": "Conversas que o bot resolveu sozinho (sem humano respondendo, via Chatwoot ou WhatsApp) ÷ total de conversas × 100"
|
||||
},
|
||||
"HANDOFF_RATE": {
|
||||
"LABEL": "Taxa de entrega",
|
||||
"TOOLTIP": "Número total de conversas entregues a agentes / número total de conversas mantidas pelo bot * 100"
|
||||
"LABEL": "Transferidas pra humano %",
|
||||
"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 messageCount = ref('0');
|
||||
const resolutionRate = ref('0');
|
||||
const handoffRate = ref('0');
|
||||
const botResolutionRate = ref('0');
|
||||
const humanTransferRate = ref('0');
|
||||
const botResolutionsCount = ref('0');
|
||||
const autoHandoffsCount = ref('0');
|
||||
const manualTakeoversCount = ref('0');
|
||||
|
||||
const formatToPercent = value => {
|
||||
return value ? `${value}%` : '--';
|
||||
@ -26,8 +29,17 @@ const fetchMetrics = () => {
|
||||
ReportsAPI.getBotMetrics(props.filters).then(response => {
|
||||
conversationCount.value = response.data.conversation_count.toLocaleString();
|
||||
messageCount.value = response.data.message_count.toLocaleString();
|
||||
resolutionRate.value = response.data.resolution_rate.toString();
|
||||
handoffRate.value = response.data.handoff_rate.toString();
|
||||
botResolutionRate.value = response.data.resolution_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,32 +49,57 @@ onMounted(fetchMetrics);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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.TOTAL_CONVERSATIONS.LABEL')"
|
||||
:info-text="$t('BOT_REPORTS.METRIC.TOTAL_CONVERSATIONS.TOOLTIP')"
|
||||
:value="conversationCount"
|
||||
class="flex-1"
|
||||
/>
|
||||
<ReportMetricCard
|
||||
:label="$t('BOT_REPORTS.METRIC.TOTAL_RESPONSES.LABEL')"
|
||||
:info-text="$t('BOT_REPORTS.METRIC.TOTAL_RESPONSES.TOOLTIP')"
|
||||
:value="messageCount"
|
||||
class="flex-1"
|
||||
/>
|
||||
<ReportMetricCard
|
||||
:label="$t('BOT_REPORTS.METRIC.RESOLUTION_RATE.LABEL')"
|
||||
:info-text="$t('BOT_REPORTS.METRIC.RESOLUTION_RATE.TOOLTIP')"
|
||||
:value="formatToPercent(resolutionRate)"
|
||||
class="flex-1"
|
||||
/>
|
||||
<ReportMetricCard
|
||||
:label="$t('BOT_REPORTS.METRIC.HANDOFF_RATE.LABEL')"
|
||||
:info-text="$t('BOT_REPORTS.METRIC.HANDOFF_RATE.TOOLTIP')"
|
||||
:value="formatToPercent(handoffRate)"
|
||||
class="flex-1"
|
||||
/>
|
||||
<div class="flex flex-col gap-4">
|
||||
<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.TOTAL_CONVERSATIONS.LABEL')"
|
||||
:info-text="$t('BOT_REPORTS.METRIC.TOTAL_CONVERSATIONS.TOOLTIP')"
|
||||
:value="conversationCount"
|
||||
class="flex-1"
|
||||
/>
|
||||
<ReportMetricCard
|
||||
:label="$t('BOT_REPORTS.METRIC.TOTAL_RESPONSES.LABEL')"
|
||||
:info-text="$t('BOT_REPORTS.METRIC.TOTAL_RESPONSES.TOOLTIP')"
|
||||
:value="messageCount"
|
||||
class="flex-1"
|
||||
/>
|
||||
<ReportMetricCard
|
||||
:label="$t('BOT_REPORTS.METRIC.RESOLUTION_RATE.LABEL')"
|
||||
:info-text="$t('BOT_REPORTS.METRIC.RESOLUTION_RATE.TOOLTIP')"
|
||||
:value="formatToPercent(botResolutionRate)"
|
||||
class="flex-1"
|
||||
/>
|
||||
<ReportMetricCard
|
||||
:label="$t('BOT_REPORTS.METRIC.HANDOFF_RATE.LABEL')"
|
||||
:info-text="$t('BOT_REPORTS.METRIC.HANDOFF_RATE.TOOLTIP')"
|
||||
:value="formatToPercent(humanTransferRate)"
|
||||
class="flex-1"
|
||||
/>
|
||||
</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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user