diff --git a/app/builders/v2/reports/bot_metrics_builder.rb b/app/builders/v2/reports/bot_metrics_builder.rb index d96ab2ccb..a14c09f86 100644 --- a/app/builders/v2/reports/bot_metrics_builder.rb +++ b/app/builders/v2/reports/bot_metrics_builder.rb @@ -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 diff --git a/app/javascript/dashboard/i18n/locale/en/report.json b/app/javascript/dashboard/i18n/locale/en/report.json index 3cdc09275..fe4b0b0c1 100644 --- a/app/javascript/dashboard/i18n/locale/en/report.json +++ b/app/javascript/dashboard/i18n/locale/en/report.json @@ -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" } } }, diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/report.json b/app/javascript/dashboard/i18n/locale/pt_BR/report.json index f9c717f34..c7a80a6fb 100644 --- a/app/javascript/dashboard/i18n/locale/pt_BR/report.json +++ b/app/javascript/dashboard/i18n/locale/pt_BR/report.json @@ -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" } } }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/BotMetrics.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/BotMetrics.vue index e8d8eccf9..e8458f21b 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/BotMetrics.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/BotMetrics.vue @@ -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);