fix(captain): respect report date filters

This commit is contained in:
Rodribm10 2026-05-17 14:00:22 -03:00
parent e94cadbdf6
commit 358114d04d
6 changed files with 184 additions and 23 deletions

View File

@ -1,13 +1,6 @@
class Api::V1::Accounts::Captain::Reports::InsightsController < Api::V1::Accounts::BaseController class Api::V1::Accounts::Captain::Reports::InsightsController < Api::V1::Accounts::BaseController
def index def index
unit_id = params[:unit_id].present? ? params[:unit_id].to_i : nil insights = filtered_insights.order(period_start: :desc).limit(12)
inbox_id = params[:inbox_id].present? ? params[:inbox_id].to_i : nil
scope = Captain::ConversationInsight.where(account_id: Current.account.id)
scope = scope.where(captain_unit_id: unit_id) if unit_id
scope = scope.where(inbox_id: inbox_id) if inbox_id
insights = scope.order(period_start: :desc).limit(12)
render json: insights.map { |i| format_insight(i) } render json: insights.map { |i| format_insight(i) }
end end
@ -39,6 +32,22 @@ class Api::V1::Accounts::Captain::Reports::InsightsController < Api::V1::Account
private private
def filtered_insights
scope = Captain::ConversationInsight.where(account_id: Current.account.id)
scope = scope.where(captain_unit_id: filter_unit_id) if filter_unit_id
scope = scope.where(inbox_id: filter_inbox_id) if filter_inbox_id
scope = scope.for_period(*requested_period) if requested_period
scope
end
def filter_unit_id
params[:unit_id].presence&.to_i
end
def filter_inbox_id
params[:inbox_id].presence&.to_i
end
def enqueue_insight(unit_id, inbox_id, period_start, period_end) def enqueue_insight(unit_id, inbox_id, period_start, period_end)
insight = find_or_init_insight(unit_id, inbox_id, period_start, period_end) insight = find_or_init_insight(unit_id, inbox_id, period_start, period_end)
return render json: { status: 'processing', message: 'Análise já está em andamento' } if insight.processing? return render json: { status: 'processing', message: 'Análise já está em andamento' } if insight.processing?
@ -77,6 +86,14 @@ class Api::V1::Accounts::Captain::Reports::InsightsController < Api::V1::Account
default default
end end
def requested_period
return nil if params[:period_start].blank? || params[:period_end].blank?
[Date.parse(params[:period_start].to_s), Date.parse(params[:period_end].to_s)]
rescue ArgumentError, TypeError
nil
end
def format_insight(insight) def format_insight(insight)
{ {
id: insight.id, id: insight.id,

View File

@ -253,12 +253,22 @@ const fetchLpStats = async () => {
let pollInterval = null; let pollInterval = null;
const insightFilterParams = () => {
const { period_start, period_end } = getPeriodDates(selectedPeriod.value);
return {
...(selectedInboxId.value && { inbox_id: selectedInboxId.value }),
...(period_start && period_end && { period_start, period_end }),
};
};
const fetchInsightsForSelectedFilters = async () => {
await store.dispatch('captainReports/fetchInsights', insightFilterParams());
};
const startPolling = () => { const startPolling = () => {
if (pollInterval) return; if (pollInterval) return;
pollInterval = setInterval(async () => { pollInterval = setInterval(async () => {
await store.dispatch('captainReports/fetchInsights', { await fetchInsightsForSelectedFilters();
inbox_id: selectedInboxId.value,
});
}, 10000); }, 10000);
}; };
@ -289,6 +299,7 @@ watch(activeTab, async tab => {
watch([customStartDate, customEndDate], async () => { watch([customStartDate, customEndDate], async () => {
if (selectedPeriod.value !== 'custom') return; if (selectedPeriod.value !== 'custom') return;
if (!customStartDate.value || !customEndDate.value) return; if (!customStartDate.value || !customEndDate.value) return;
await fetchInsightsForSelectedFilters();
if (activeTab.value === 'landing_pages') await fetchLpStats(); if (activeTab.value === 'landing_pages') await fetchLpStats();
if (activeTab.value === 'operational') await fetchOperational(); if (activeTab.value === 'operational') await fetchOperational();
if (activeTab.value === 'executive') await fetchExecutive(); if (activeTab.value === 'executive') await fetchExecutive();
@ -308,7 +319,7 @@ watch(
onMounted(async () => { onMounted(async () => {
await store.dispatch('inboxes/get'); await store.dispatch('inboxes/get');
await store.dispatch('captainAssistants/get'); await store.dispatch('captainAssistants/get');
await store.dispatch('captainReports/fetchInsights', {}); await fetchInsightsForSelectedFilters();
if (hasProcessingInsights.value) startPolling(); if (hasProcessingInsights.value) startPolling();
await fetchLpStats(); await fetchLpStats();
}); });
@ -320,9 +331,7 @@ onUnmounted(() => {
const onFilterChange = async event => { const onFilterChange = async event => {
const value = event.target.value; const value = event.target.value;
selectedInboxId.value = value ? Number(value) : null; selectedInboxId.value = value ? Number(value) : null;
await store.dispatch('captainReports/fetchInsights', { await fetchInsightsForSelectedFilters();
inbox_id: selectedInboxId.value,
});
if (activeTab.value === 'landing_pages') await fetchLpStats(); if (activeTab.value === 'landing_pages') await fetchLpStats();
if (activeTab.value === 'operational') await fetchOperational(); if (activeTab.value === 'operational') await fetchOperational();
if (activeTab.value === 'executive') await fetchExecutive(); if (activeTab.value === 'executive') await fetchExecutive();
@ -330,6 +339,7 @@ const onFilterChange = async event => {
const onPeriodChange = async event => { const onPeriodChange = async event => {
selectedPeriod.value = event.target.value; selectedPeriod.value = event.target.value;
await fetchInsightsForSelectedFilters();
if (activeTab.value === 'landing_pages') await fetchLpStats(); if (activeTab.value === 'landing_pages') await fetchLpStats();
if (activeTab.value === 'operational') await fetchOperational(); if (activeTab.value === 'operational') await fetchOperational();
if (activeTab.value === 'executive') await fetchExecutive(); if (activeTab.value === 'executive') await fetchExecutive();

View File

@ -46,17 +46,21 @@ class Captain::Reports::GenerateInsightsJob < ApplicationJob
account: account, account: account,
unit: unit, unit: unit,
inbox: inbox, inbox: inbox,
conversations: conversations conversations: conversations,
period_start: period_start,
period_end: period_end
).analyze ).analyze
insight.update!(messages_count: conversations.sum { |conv| conv.messages.count }) insight.update!(messages_count: messages_in_period(conversations, period_start, period_end).count)
insight.mark_done!(payload) insight.mark_done!(payload)
end end
def fetch_conversations(account, unit, inbox, period_start, period_end) def fetch_conversations(account, unit, inbox, period_start, period_end)
scope = account.conversations scope = account.conversations
.where(created_at: period_start.beginning_of_day..period_end.end_of_day) .joins(:messages)
.includes(:messages) .where(messages: { created_at: period_start.beginning_of_day..period_end.end_of_day })
.where.not(messages: { message_type: [Message.message_types[:activity], Message.message_types[:template]] })
.where(messages: { private: false })
if inbox if inbox
scope = scope.where(inbox_id: inbox.id) scope = scope.where(inbox_id: inbox.id)
@ -65,6 +69,19 @@ class Captain::Reports::GenerateInsightsJob < ApplicationJob
scope = scope.where(inbox_id: inbox_ids) if inbox_ids.any? scope = scope.where(inbox_id: inbox_ids) if inbox_ids.any?
end end
scope.to_a account.conversations
.where(id: scope.select(:id).distinct)
.includes(:inbox, :contact, :messages)
.to_a
end
def messages_in_period(conversations, period_start, period_end)
conversation_ids = conversations.map(&:id)
return Message.none if conversation_ids.empty?
Message.where(conversation_id: conversation_ids)
.where(created_at: period_start.beginning_of_day..period_end.end_of_day)
.where.not(message_type: %i[activity template])
.where(private: false)
end end
end end

View File

@ -3,12 +3,14 @@ class Captain::Llm::ConversationInsightService < Llm::BaseAiService
MAX_CHARS_PER_CHUNK = 40_000 MAX_CHARS_PER_CHUNK = 40_000
def initialize(account:, conversations:, unit: nil, inbox: nil) def initialize(account:, conversations:, unit: nil, inbox: nil, period_start: nil, period_end: nil)
super() super()
@account = account @account = account
@unit = unit @unit = unit
@inbox = inbox @inbox = inbox
@conversations = conversations @conversations = conversations
@period_start = period_start
@period_end = period_end
end end
# Analisa as conversas e retorna o payload de insights # Analisa as conversas e retorna o payload de insights
@ -24,10 +26,10 @@ class Captain::Llm::ConversationInsightService < Llm::BaseAiService
private private
attr_reader :account, :unit, :inbox, :conversations attr_reader :account, :unit, :inbox, :conversations, :period_start, :period_end
def build_chunks def build_chunks
texts = conversations.map(&:to_llm_text).reject(&:blank?) texts = conversations.map { |conversation| conversation_text(conversation) }.reject(&:blank?)
return [] if texts.empty? return [] if texts.empty?
chunks = [] chunks = []
@ -48,6 +50,38 @@ class Captain::Llm::ConversationInsightService < Llm::BaseAiService
chunks chunks
end end
def conversation_text(conversation)
return conversation.to_llm_text unless period_start && period_end
messages = conversation.messages
.where(created_at: period_start.beginning_of_day..period_end.end_of_day)
.where.not(message_type: %i[activity template])
.where(private: false)
.order(created_at: :asc)
return nil if messages.empty?
[
"Conversation ID: ##{conversation.display_id}",
"Channel: #{conversation.inbox.channel.name}",
'Message History:',
messages.map { |message| format_message(message) }.join
].join("\n")
end
def format_message(message)
sender = case message.sender_type
when 'User'
'Support Agent'
when 'Contact'
'User'
else
'Bot'
end
"#{sender}: #{message.content_for_llm}\n"
end
def analyze_chunk(chunk) def analyze_chunk(chunk)
response = instrument_llm_call(instrumentation_params) do response = instrument_llm_call(instrumentation_params) do
chat chat

View File

@ -0,0 +1,53 @@
require 'rails_helper'
RSpec.describe Captain::Reports::GenerateInsightsJob do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:period_start) { Date.new(2026, 5, 17) }
let(:period_end) { Date.new(2026, 5, 17) }
let(:payload) do
{
'top_topics' => [],
'ai_failures' => [],
'faq_gaps' => [],
'sentiment' => { 'positive_count' => 0, 'negative_count' => 0, 'neutral_count' => 0, 'summary' => '' },
'highlights' => { 'praises' => [], 'complaints' => [] },
'most_requested_suites' => [],
'price_reactions' => { 'summary' => '', 'objections_count' => 0 },
'customer_opportunities' => [],
'recommendations' => [],
'period_summary' => 'Resumo do dia.'
}
end
it 'analyzes conversations with messages in the requested period and counts only period messages' do
conversation_with_today_messages = create(:conversation, account: account, inbox: inbox, created_at: 3.days.ago)
create(:message, account: account, inbox: inbox, conversation: conversation_with_today_messages, content: 'mensagem antiga',
created_at: period_start.prev_day.noon)
create(:message, account: account, inbox: inbox, conversation: conversation_with_today_messages, content: 'mensagem hoje',
created_at: period_start.noon)
create(:message, account: account, inbox: inbox, conversation: conversation_with_today_messages, content: 'resposta hoje',
message_type: 'outgoing', created_at: period_start.noon + 5.minutes)
conversation_without_today_messages = create(:conversation, account: account, inbox: inbox, created_at: period_start.noon)
create(:message, account: account, inbox: inbox, conversation: conversation_without_today_messages, content: 'fora do periodo',
created_at: period_start.prev_day.noon)
service = instance_double(Captain::Llm::ConversationInsightService, analyze: payload)
expect(Captain::Llm::ConversationInsightService).to receive(:new)
.with(hash_including(conversations: [conversation_with_today_messages], period_start: period_start, period_end: period_end))
.and_return(service)
described_class.perform_now(account.id, nil, period_start, period_end, inbox.id)
insight = Captain::ConversationInsight.find_by!(
account_id: account.id,
inbox_id: inbox.id,
period_start: period_start,
period_end: period_end
)
expect(insight.conversations_count).to eq(1)
expect(insight.messages_count).to eq(2)
expect(insight).to be_done
end
end

View File

@ -0,0 +1,30 @@
require 'rails_helper'
RSpec.describe Captain::Llm::ConversationInsightService do
describe '#build_chunks' do
it 'formats only messages inside the requested period' do
account = create(:account)
inbox = create(:inbox, account: account)
period_start = Date.new(2026, 5, 17)
conversation = create(:conversation, account: account, inbox: inbox)
create(:message, account: account, inbox: inbox, conversation: conversation, content: 'mensagem antiga',
created_at: period_start.prev_day.noon)
create(:message, account: account, inbox: inbox, conversation: conversation, content: 'mensagem do periodo',
created_at: period_start.noon)
service = described_class.new(
account: account,
inbox: inbox,
conversations: [conversation],
period_start: period_start,
period_end: period_start
)
text = service.send(:build_chunks).join("\n")
expect(text).to include('mensagem do periodo')
expect(text).not_to include('mensagem antiga')
end
end
end