chore: Update the behavior of Captain resolutions (#10794)

This PR ensures that only conversations from quick conversation channels
are resolved, avoiding resolutions on the email channel (we still need
to improve the UX here). It also updates the FAQ generation logic,
limiting it to conversations that had at least one human interaction.
This commit is contained in:
Pranav
2025-02-03 02:55:08 -08:00
committed by GitHub
parent 5905b5301d
commit c7d259d5fd
5 changed files with 61 additions and 19 deletions

View File

@@ -9,7 +9,13 @@ module Enterprise::Account::ConversationsResolutionSchedulerJob
def resolve_captain_conversations def resolve_captain_conversations
CaptainInbox.all.find_each(batch_size: 100) do |captain_inbox| CaptainInbox.all.find_each(batch_size: 100) do |captain_inbox|
Captain::InboxPendingConversationsResolutionJob.perform_later(captain_inbox.inbox) inbox = captain_inbox.inbox
next if inbox.email?
Captain::InboxPendingConversationsResolutionJob.perform_later(
inbox
)
end end
end end
end end

View File

@@ -8,7 +8,11 @@ class Captain::Llm::ConversationFaqService < Captain::Llm::BaseOpenAiService
@content = conversation.to_llm_text @content = conversation.to_llm_text
end end
# Generates and deduplicates FAQs from conversation content
# Skips processing if there was no human interaction
def generate_and_deduplicate def generate_and_deduplicate
return [] if no_human_interaction?
new_faqs = generate new_faqs = generate
return [] if new_faqs.empty? return [] if new_faqs.empty?
@@ -21,6 +25,10 @@ class Captain::Llm::ConversationFaqService < Captain::Llm::BaseOpenAiService
attr_reader :content, :conversation, :assistant attr_reader :content, :conversation, :assistant
def no_human_interaction?
conversation.first_reply_created_at.nil?
end
def find_and_separate_duplicates(faqs) def find_and_separate_duplicates(faqs)
duplicate_faqs = [] duplicate_faqs = []
unique_faqs = [] unique_faqs = []

View File

@@ -1,29 +1,44 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Account::ConversationsResolutionSchedulerJob, type: :job do RSpec.describe Account::ConversationsResolutionSchedulerJob, type: :job do
let!(:account_with_bot) { create(:account) }
let(:account) { create(:account) } let(:account) { create(:account) }
let(:assistant) { create(:captain_assistant, account: account_with_bot) } let(:assistant) { create(:captain_assistant, account: account) }
let!(:account_without_bot) { create(:account) }
let!(:inbox_with_bot) { create(:inbox, account: account_with_bot) }
let!(:inbox_without_bot) { create(:inbox, account: account_without_bot) }
describe '#perform - captain resolutions' do describe '#perform - captain resolutions' do
before do context 'when handling different inbox types' do
create(:captain_inbox, captain_assistant: assistant, inbox: inbox_with_bot) let!(:regular_inbox) { create(:inbox, account: account) }
let!(:email_inbox) { create(:inbox, :with_email, account: account) }
before do
create(:captain_inbox, captain_assistant: assistant, inbox: regular_inbox)
create(:captain_inbox, captain_assistant: assistant, inbox: email_inbox)
end
it 'enqueues resolution jobs only for non-email inboxes with captain enabled' do
expect do
described_class.perform_now
end.to have_enqueued_job(Captain::InboxPendingConversationsResolutionJob)
.with(regular_inbox)
.exactly(:once)
end
it 'does not enqueue resolution jobs for email inboxes even with captain enabled' do
expect do
described_class.perform_now
end.not_to have_enqueued_job(Captain::InboxPendingConversationsResolutionJob)
.with(email_inbox)
end
end end
it 'enqueues resolution jobs only for inboxes with captain enabled' do context 'when inbox has no captain enabled' do
expect do let!(:inbox_without_captain) { create(:inbox, account: create(:account)) }
described_class.perform_now
end.to have_enqueued_job(Captain::InboxPendingConversationsResolutionJob).with(inbox_with_bot).and have_enqueued_job.exactly(:once)
end
it 'does not enqueue resolution jobs for inboxes without captain enabled' do it 'does not enqueue resolution jobs' do
expect do expect do
described_class.perform_now described_class.perform_now
end.not_to have_enqueued_job(Captain::InboxPendingConversationsResolutionJob).with(inbox_without_bot) end.not_to have_enqueued_job(Captain::InboxPendingConversationsResolutionJob)
.with(inbox_without_captain)
end
end end
end end
end end

View File

@@ -2,7 +2,7 @@ require 'rails_helper'
RSpec.describe Captain::Llm::ConversationFaqService do RSpec.describe Captain::Llm::ConversationFaqService do
let(:captain_assistant) { create(:captain_assistant) } let(:captain_assistant) { create(:captain_assistant) }
let(:conversation) { create(:conversation) } let(:conversation) { create(:conversation, first_reply_created_at: Time.zone.now) }
let(:service) { described_class.new(captain_assistant, conversation) } let(:service) { described_class.new(captain_assistant, conversation) }
let(:client) { instance_double(OpenAI::Client) } let(:client) { instance_double(OpenAI::Client) }
let(:embedding_service) { instance_double(Captain::Llm::EmbeddingService) } let(:embedding_service) { instance_double(Captain::Llm::EmbeddingService) }
@@ -57,6 +57,14 @@ RSpec.describe Captain::Llm::ConversationFaqService do
end end
end end
context 'without human interaction' do
let(:conversation) { create(:conversation) }
it 'returns an empty array without generating FAQs' do
expect(service.generate_and_deduplicate).to eq([])
end
end
context 'when finding duplicates' do context 'when finding duplicates' do
let(:existing_response) do let(:existing_response) do
create(:captain_assistant_response, assistant: captain_assistant, question: 'Similar question', answer: 'Similar answer') create(:captain_assistant_response, assistant: captain_assistant, question: 'Similar question', answer: 'Similar answer')

View File

@@ -9,5 +9,10 @@ FactoryBot.define do
after(:create) do |inbox| after(:create) do |inbox|
inbox.channel.save! inbox.channel.save!
end end
trait :with_email do
channel { FactoryBot.build(:channel_email, account: account) }
name { 'Email Inbox' }
end
end end
end end