fix(reports): tratar resposta humana via WhatsApp como interação humana
Bug do BotReports descoberto pelo Rodrigo: - A regra de "conversation_bot_resolved" só desqualificava conversas com outgoing.sender_type='User' (atendente respondendo pelo Chatwoot UI) - Mas mensagem outgoing vinda do webhook WhatsApp com IsFromMe=true (atendente respondeu direto pelo celular do hotel) é gravada com sender=nil - Resultado: a Jasmine ganhava crédito mesmo quando humano respondia fora do Chatwoot. Taxa de resolução pelo bot inflada. Fix prospectivo: - ReportingEventListener#create_bot_resolved_event agora desqualifica via human_outgoing_messages? (sender_type='User' OU sender_type IS NULL) - Captain::Assistant (a Jasmine) usa sender_type='Captain::Assistant' e segue fora do filtro, como antes - Spec novo cobrindo o caso WhatsApp echo Retroativo: - lib/tasks/rebuild_bot_resolved.rake — task idempotente que purga reporting_events de conversation_bot_resolved gerados sob a regra antiga. - DRY-RUN por padrão, APPLY=true pra deletar, ACCOUNT_ID pra restringir, SNAPSHOT_PATH pra trilha de auditoria Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
429567495f
commit
7cd2ea1258
@ -133,11 +133,22 @@ class ReportingEventListener < BaseListener
|
||||
|
||||
def create_bot_resolved_event(conversation, reporting_event)
|
||||
return unless conversation.inbox.active_bot?
|
||||
# We don't want to create a bot_resolved event if there is user interaction on the conversation
|
||||
return if conversation.messages.exists?(message_type: :outgoing, sender_type: 'User')
|
||||
# We don't want to create a bot_resolved event if there is human interaction on the conversation.
|
||||
# Human interaction = outgoing message either from a User (replied via Chatwoot UI) OR from a
|
||||
# nil sender (replied directly via the connected WhatsApp app — webhook echo with IsFromMe=true,
|
||||
# see app/services/whatsapp/incoming_message_wuzapi_service.rb#build_message).
|
||||
# The bot itself uses sender_type 'Captain::Assistant' (or 'AgentBot'), so it stays excluded from this filter.
|
||||
return if human_outgoing_messages?(conversation)
|
||||
|
||||
bot_resolved_event = reporting_event.dup
|
||||
bot_resolved_event.name = 'conversation_bot_resolved'
|
||||
bot_resolved_event.save!
|
||||
end
|
||||
|
||||
def human_outgoing_messages?(conversation)
|
||||
conversation.messages
|
||||
.where(message_type: :outgoing)
|
||||
.where('sender_type = ? OR sender_type IS NULL', 'User')
|
||||
.any?
|
||||
end
|
||||
end
|
||||
|
||||
79
lib/tasks/rebuild_bot_resolved.rake
Normal file
79
lib/tasks/rebuild_bot_resolved.rake
Normal file
@ -0,0 +1,79 @@
|
||||
# Rebuild conversation_bot_resolved retroactively
|
||||
#
|
||||
# Removes 'conversation_bot_resolved' reporting events that were created with the
|
||||
# old (buggy) classification rule, where outgoing messages with sender_type=NULL
|
||||
# (= human replied directly via the connected WhatsApp app, not via Chatwoot UI)
|
||||
# were not considered as human interaction. The bot was incorrectly credited.
|
||||
#
|
||||
# What this task does:
|
||||
# 1. Finds all conversation_bot_resolved events whose conversations contain at
|
||||
# least one outgoing message with sender_id IS NULL (= replied externally).
|
||||
# 2. (Optional) Saves the count + ids to a snapshot file before deleting.
|
||||
# 3. Deletes those events. The classification will be re-evaluated correctly
|
||||
# for any *future* resolution because the listener was fixed.
|
||||
#
|
||||
# Idempotent: re-running on a clean dataset is a no-op.
|
||||
#
|
||||
# Usage:
|
||||
# # Dry-run (default) — counts only, no delete
|
||||
# bundle exec rake reports:rebuild_bot_resolved
|
||||
#
|
||||
# # Actually delete
|
||||
# APPLY=true bundle exec rake reports:rebuild_bot_resolved
|
||||
#
|
||||
# # Restrict to a specific account (recommended on multi-tenant prod)
|
||||
# APPLY=true ACCOUNT_ID=1 bundle exec rake reports:rebuild_bot_resolved
|
||||
#
|
||||
# # Snapshot ids to a file before deleting (audit trail)
|
||||
# APPLY=true SNAPSHOT_PATH=/tmp/bot_resolved_purge.csv bundle exec rake reports:rebuild_bot_resolved
|
||||
|
||||
namespace :reports do
|
||||
desc 'Remove buggy conversation_bot_resolved events created before the sender-NIL fix'
|
||||
task rebuild_bot_resolved: :environment do
|
||||
apply = ENV['APPLY'].to_s.casecmp('true').zero?
|
||||
account_id = ENV['ACCOUNT_ID'].presence&.to_i
|
||||
snapshot_path = ENV['SNAPSHOT_PATH'].presence
|
||||
|
||||
base_scope = ReportingEvent.where(name: 'conversation_bot_resolved')
|
||||
base_scope = base_scope.where(account_id: account_id) if account_id
|
||||
|
||||
affected_conversation_ids = Message
|
||||
.where(message_type: :outgoing, sender_id: nil)
|
||||
.distinct
|
||||
.pluck(:conversation_id)
|
||||
|
||||
bad_events = base_scope.where(conversation_id: affected_conversation_ids)
|
||||
|
||||
total_events = base_scope.count
|
||||
purge_count = bad_events.count
|
||||
|
||||
puts '=== Rebuild conversation_bot_resolved ==='
|
||||
puts "Account filter: #{account_id || 'ALL'}"
|
||||
puts "Total bot_resolved events in scope: #{total_events}"
|
||||
puts "Events to purge (had outgoing with sender_id NULL): #{purge_count}"
|
||||
puts "Mode: #{apply ? 'APPLY (will delete)' : 'DRY-RUN (no changes)'}"
|
||||
|
||||
if purge_count.zero?
|
||||
puts 'Nothing to do — already clean.'
|
||||
next
|
||||
end
|
||||
|
||||
if snapshot_path
|
||||
File.open(snapshot_path, 'w') do |f|
|
||||
f.puts 'reporting_event_id,conversation_id,account_id,inbox_id,created_at'
|
||||
bad_events.find_each(batch_size: 500) do |re|
|
||||
f.puts [re.id, re.conversation_id, re.account_id, re.inbox_id, re.created_at.iso8601].join(',')
|
||||
end
|
||||
end
|
||||
puts "Snapshot written to #{snapshot_path}"
|
||||
end
|
||||
|
||||
unless apply
|
||||
puts 'DRY-RUN finished. Re-run with APPLY=true to delete.'
|
||||
next
|
||||
end
|
||||
|
||||
deleted = bad_events.delete_all
|
||||
puts "Deleted #{deleted} reporting_events. Bot resolution metrics will reflect the new count immediately."
|
||||
end
|
||||
end
|
||||
@ -62,6 +62,14 @@ describe ReportingEventListener do
|
||||
listener.conversation_resolved(event)
|
||||
expect(account.reporting_events.where(name: 'conversation_bot_resolved').count).to be 0
|
||||
end
|
||||
|
||||
it 'does not create a conversation_bot_resolved event if a human replied directly via the connected WhatsApp app' do
|
||||
# outgoing message with sender_type=NULL = WhatsApp echo (IsFromMe=true) — human replied outside Chatwoot UI
|
||||
create(:message, :bot_message, account: account, inbox: agent_bot_inbox, conversation: bot_resolved_conversation)
|
||||
event = Events::Base.new('conversation.resolved', Time.zone.now, conversation: bot_resolved_conversation)
|
||||
listener.conversation_resolved(event)
|
||||
expect(account.reporting_events.where(name: 'conversation_bot_resolved').count).to be 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user