fix(captain): respect report date filters
This commit is contained in:
parent
e94cadbdf6
commit
358114d04d
@ -1,13 +1,6 @@
|
||||
class Api::V1::Accounts::Captain::Reports::InsightsController < Api::V1::Accounts::BaseController
|
||||
def index
|
||||
unit_id = params[:unit_id].present? ? params[:unit_id].to_i : nil
|
||||
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)
|
||||
insights = filtered_insights.order(period_start: :desc).limit(12)
|
||||
|
||||
render json: insights.map { |i| format_insight(i) }
|
||||
end
|
||||
@ -39,6 +32,22 @@ class Api::V1::Accounts::Captain::Reports::InsightsController < Api::V1::Account
|
||||
|
||||
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)
|
||||
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?
|
||||
@ -77,6 +86,14 @@ class Api::V1::Accounts::Captain::Reports::InsightsController < Api::V1::Account
|
||||
default
|
||||
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)
|
||||
{
|
||||
id: insight.id,
|
||||
|
||||
@ -253,12 +253,22 @@ const fetchLpStats = async () => {
|
||||
|
||||
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 = () => {
|
||||
if (pollInterval) return;
|
||||
pollInterval = setInterval(async () => {
|
||||
await store.dispatch('captainReports/fetchInsights', {
|
||||
inbox_id: selectedInboxId.value,
|
||||
});
|
||||
await fetchInsightsForSelectedFilters();
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
@ -289,6 +299,7 @@ watch(activeTab, async tab => {
|
||||
watch([customStartDate, customEndDate], async () => {
|
||||
if (selectedPeriod.value !== 'custom') return;
|
||||
if (!customStartDate.value || !customEndDate.value) return;
|
||||
await fetchInsightsForSelectedFilters();
|
||||
if (activeTab.value === 'landing_pages') await fetchLpStats();
|
||||
if (activeTab.value === 'operational') await fetchOperational();
|
||||
if (activeTab.value === 'executive') await fetchExecutive();
|
||||
@ -308,7 +319,7 @@ watch(
|
||||
onMounted(async () => {
|
||||
await store.dispatch('inboxes/get');
|
||||
await store.dispatch('captainAssistants/get');
|
||||
await store.dispatch('captainReports/fetchInsights', {});
|
||||
await fetchInsightsForSelectedFilters();
|
||||
if (hasProcessingInsights.value) startPolling();
|
||||
await fetchLpStats();
|
||||
});
|
||||
@ -320,9 +331,7 @@ onUnmounted(() => {
|
||||
const onFilterChange = async event => {
|
||||
const value = event.target.value;
|
||||
selectedInboxId.value = value ? Number(value) : null;
|
||||
await store.dispatch('captainReports/fetchInsights', {
|
||||
inbox_id: selectedInboxId.value,
|
||||
});
|
||||
await fetchInsightsForSelectedFilters();
|
||||
if (activeTab.value === 'landing_pages') await fetchLpStats();
|
||||
if (activeTab.value === 'operational') await fetchOperational();
|
||||
if (activeTab.value === 'executive') await fetchExecutive();
|
||||
@ -330,6 +339,7 @@ const onFilterChange = async event => {
|
||||
|
||||
const onPeriodChange = async event => {
|
||||
selectedPeriod.value = event.target.value;
|
||||
await fetchInsightsForSelectedFilters();
|
||||
if (activeTab.value === 'landing_pages') await fetchLpStats();
|
||||
if (activeTab.value === 'operational') await fetchOperational();
|
||||
if (activeTab.value === 'executive') await fetchExecutive();
|
||||
|
||||
@ -46,17 +46,21 @@ class Captain::Reports::GenerateInsightsJob < ApplicationJob
|
||||
account: account,
|
||||
unit: unit,
|
||||
inbox: inbox,
|
||||
conversations: conversations
|
||||
conversations: conversations,
|
||||
period_start: period_start,
|
||||
period_end: period_end
|
||||
).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)
|
||||
end
|
||||
|
||||
def fetch_conversations(account, unit, inbox, period_start, period_end)
|
||||
scope = account.conversations
|
||||
.where(created_at: period_start.beginning_of_day..period_end.end_of_day)
|
||||
.includes(:messages)
|
||||
.joins(: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
|
||||
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?
|
||||
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
|
||||
|
||||
@ -3,12 +3,14 @@ class Captain::Llm::ConversationInsightService < Llm::BaseAiService
|
||||
|
||||
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()
|
||||
@account = account
|
||||
@unit = unit
|
||||
@inbox = inbox
|
||||
@conversations = conversations
|
||||
@period_start = period_start
|
||||
@period_end = period_end
|
||||
end
|
||||
|
||||
# Analisa as conversas e retorna o payload de insights
|
||||
@ -24,10 +26,10 @@ class Captain::Llm::ConversationInsightService < Llm::BaseAiService
|
||||
|
||||
private
|
||||
|
||||
attr_reader :account, :unit, :inbox, :conversations
|
||||
attr_reader :account, :unit, :inbox, :conversations, :period_start, :period_end
|
||||
|
||||
def build_chunks
|
||||
texts = conversations.map(&:to_llm_text).reject(&:blank?)
|
||||
texts = conversations.map { |conversation| conversation_text(conversation) }.reject(&:blank?)
|
||||
return [] if texts.empty?
|
||||
|
||||
chunks = []
|
||||
@ -48,6 +50,38 @@ class Captain::Llm::ConversationInsightService < Llm::BaseAiService
|
||||
chunks
|
||||
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)
|
||||
response = instrument_llm_call(instrumentation_params) do
|
||||
chat
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
Loading…
Reference in New Issue
Block a user