diff --git a/config/schedule.yml b/config/schedule.yml index 689a48d82..b66848fa7 100644 --- a/config/schedule.yml +++ b/config/schedule.yml @@ -101,3 +101,9 @@ landing_hosts_promotion_sync_scheduler_job: cron: '0 * * * *' class: 'LandingHosts::PromotionSyncSchedulerJob' queue: scheduled_jobs + +# every 10 minutes - detects silent conversations for memory extraction +captain_contact_memory_silence_detector_job: + cron: '*/10 * * * *' + class: 'Captain::ContactMemories::SilenceDetectorJob' + queue: scheduled_jobs diff --git a/enterprise/app/jobs/captain/contact_memories/silence_detector_job.rb b/enterprise/app/jobs/captain/contact_memories/silence_detector_job.rb new file mode 100644 index 000000000..98617f988 --- /dev/null +++ b/enterprise/app/jobs/captain/contact_memories/silence_detector_job.rb @@ -0,0 +1,33 @@ +class Captain::ContactMemories::SilenceDetectorJob < ApplicationJob + queue_as :scheduled_jobs + + SILENCE_THRESHOLD = 30.minutes + + def perform + Account.where("custom_attributes->>'captain_contact_memory_extraction_enabled' = 'true'").find_each do |account| + elegible_conversation_ids(account).each do |conv_id| + Captain::ContactMemories::ExtractFromConversationJob.perform_later(conv_id) + end + end + end + + private + + def elegible_conversation_ids(account) + Conversation + .where(account_id: account.id) + .joins(:messages) + .where.not(id: already_extracted_ids(account)) + .group('conversations.id') + .having('MAX(messages.created_at) < ?', SILENCE_THRESHOLD.ago) + .pluck(:id) + end + + def already_extracted_ids(account) + Captain::ContactMemory + .where(account_id: account.id) + .where.not(source_conversation_id: nil) + .distinct + .pluck(:source_conversation_id) + end +end diff --git a/spec/enterprise/jobs/captain/contact_memories/silence_detector_job_spec.rb b/spec/enterprise/jobs/captain/contact_memories/silence_detector_job_spec.rb new file mode 100644 index 000000000..436d360a1 --- /dev/null +++ b/spec/enterprise/jobs/captain/contact_memories/silence_detector_job_spec.rb @@ -0,0 +1,52 @@ +require 'rails_helper' + +RSpec.describe Captain::ContactMemories::SilenceDetectorJob do + let(:account) { create(:account, custom_attributes: { 'captain_contact_memory_extraction_enabled' => true }) } + let(:contact) { create(:contact, account: account) } + + # Widget inboxes auto-generate template messages (pre-chat form, etc.) with current + # timestamps when an incoming message is created. To deterministically test "silence", + # we update all messages in the conversation to an old created_at via update_all + # (bypassing callbacks) after creation. + def age_all_messages(conversation, age) + conversation.messages.update_all(created_at: age) # rubocop:disable Rails/SkipsModelValidations + end + + it 'enqueues extraction for conversations silent > 30 minutes' do + conv = create(:conversation, account: account, contact: contact) + create(:message, conversation: conv, account: account) + age_all_messages(conv, 35.minutes.ago) + + expect { described_class.perform_now } + .to have_enqueued_job(Captain::ContactMemories::ExtractFromConversationJob).with(conv.id) + end + + it 'ignores conversations with recent activity' do + conv = create(:conversation, account: account, contact: contact) + create(:message, conversation: conv, account: account) + age_all_messages(conv, 5.minutes.ago) + + expect { described_class.perform_now } + .not_to have_enqueued_job(Captain::ContactMemories::ExtractFromConversationJob) + end + + it 'ignores conversations already extracted' do + conv = create(:conversation, account: account, contact: contact) + create(:message, conversation: conv, account: account) + age_all_messages(conv, 35.minutes.ago) + create(:captain_contact_memory, account: account, contact: contact, source_conversation_id: conv.id) + + expect { described_class.perform_now } + .not_to have_enqueued_job(Captain::ContactMemories::ExtractFromConversationJob) + end + + it 'ignores accounts with flag off' do + account.update!(custom_attributes: { 'captain_contact_memory_extraction_enabled' => false }) + conv = create(:conversation, account: account, contact: contact) + create(:message, conversation: conv, account: account) + age_all_messages(conv, 35.minutes.ago) + + expect { described_class.perform_now } + .not_to have_enqueued_job(Captain::ContactMemories::ExtractFromConversationJob) + end +end