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
|
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,
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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