From c7d259d5fd383e6f7c7ab66dde46247cb5de598a Mon Sep 17 00:00:00 2001 From: Pranav Date: Mon, 3 Feb 2025 02:55:08 -0800 Subject: [PATCH 1/3] 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. --- .../conversations_resolution_scheduler_job.rb | 8 ++- .../captain/llm/conversation_faq_service.rb | 8 +++ ...ersations_resolution_scheduler_job_spec.rb | 49 ++++++++++++------- .../llm/conversation_faq_service_spec.rb | 10 +++- spec/factories/inboxes.rb | 5 ++ 5 files changed, 61 insertions(+), 19 deletions(-) diff --git a/enterprise/app/jobs/enterprise/account/conversations_resolution_scheduler_job.rb b/enterprise/app/jobs/enterprise/account/conversations_resolution_scheduler_job.rb index 2dc6fd67b..599dee96a 100644 --- a/enterprise/app/jobs/enterprise/account/conversations_resolution_scheduler_job.rb +++ b/enterprise/app/jobs/enterprise/account/conversations_resolution_scheduler_job.rb @@ -9,7 +9,13 @@ module Enterprise::Account::ConversationsResolutionSchedulerJob def resolve_captain_conversations 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 diff --git a/enterprise/app/services/captain/llm/conversation_faq_service.rb b/enterprise/app/services/captain/llm/conversation_faq_service.rb index 47cdd6cf4..ea33493f2 100644 --- a/enterprise/app/services/captain/llm/conversation_faq_service.rb +++ b/enterprise/app/services/captain/llm/conversation_faq_service.rb @@ -8,7 +8,11 @@ class Captain::Llm::ConversationFaqService < Captain::Llm::BaseOpenAiService @content = conversation.to_llm_text end + # Generates and deduplicates FAQs from conversation content + # Skips processing if there was no human interaction def generate_and_deduplicate + return [] if no_human_interaction? + new_faqs = generate return [] if new_faqs.empty? @@ -21,6 +25,10 @@ class Captain::Llm::ConversationFaqService < Captain::Llm::BaseOpenAiService attr_reader :content, :conversation, :assistant + def no_human_interaction? + conversation.first_reply_created_at.nil? + end + def find_and_separate_duplicates(faqs) duplicate_faqs = [] unique_faqs = [] diff --git a/spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb b/spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb index 03c799962..b67877412 100644 --- a/spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb +++ b/spec/enterprise/jobs/enterprise/account/conversations_resolution_scheduler_job_spec.rb @@ -1,29 +1,44 @@ require 'rails_helper' RSpec.describe Account::ConversationsResolutionSchedulerJob, type: :job do - let!(:account_with_bot) { create(:account) } let(:account) { create(:account) } - let(:assistant) { create(:captain_assistant, account: account_with_bot) } - - 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) } + let(:assistant) { create(:captain_assistant, account: account) } describe '#perform - captain resolutions' do - before do - create(:captain_inbox, captain_assistant: assistant, inbox: inbox_with_bot) + context 'when handling different inbox types' do + 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 - it 'enqueues resolution jobs only for inboxes with captain enabled' do - expect do - described_class.perform_now - end.to have_enqueued_job(Captain::InboxPendingConversationsResolutionJob).with(inbox_with_bot).and have_enqueued_job.exactly(:once) - end + context 'when inbox has no captain enabled' do + let!(:inbox_without_captain) { create(:inbox, account: create(:account)) } - it 'does not enqueue resolution jobs for inboxes without captain enabled' do - expect do - described_class.perform_now - end.not_to have_enqueued_job(Captain::InboxPendingConversationsResolutionJob).with(inbox_without_bot) + it 'does not enqueue resolution jobs' do + expect do + described_class.perform_now + end.not_to have_enqueued_job(Captain::InboxPendingConversationsResolutionJob) + .with(inbox_without_captain) + end end end end diff --git a/spec/enterprise/services/captain/llm/conversation_faq_service_spec.rb b/spec/enterprise/services/captain/llm/conversation_faq_service_spec.rb index 6c729e428..fe0276b49 100644 --- a/spec/enterprise/services/captain/llm/conversation_faq_service_spec.rb +++ b/spec/enterprise/services/captain/llm/conversation_faq_service_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' RSpec.describe Captain::Llm::ConversationFaqService do 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(:client) { instance_double(OpenAI::Client) } let(:embedding_service) { instance_double(Captain::Llm::EmbeddingService) } @@ -57,6 +57,14 @@ RSpec.describe Captain::Llm::ConversationFaqService do 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 let(:existing_response) do create(:captain_assistant_response, assistant: captain_assistant, question: 'Similar question', answer: 'Similar answer') diff --git a/spec/factories/inboxes.rb b/spec/factories/inboxes.rb index cdd23f3c3..e8ca50200 100644 --- a/spec/factories/inboxes.rb +++ b/spec/factories/inboxes.rb @@ -9,5 +9,10 @@ FactoryBot.define do after(:create) do |inbox| inbox.channel.save! end + + trait :with_email do + channel { FactoryBot.build(:channel_email, account: account) } + name { 'Email Inbox' } + end end end From 3fb77fe806b8bfba25d5e137e1e2d5db8697d611 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 3 Feb 2025 16:54:13 +0530 Subject: [PATCH 2/3] chore: Resolve flaky spec for Contact country sorting (#10810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We have been encountering errors in the community pipeline for the contacts sort by country spec. Upon investigation, it was discovered that the spec assumes the country code is used for sorting. However, the sorting actually relies on the country attribute. The payload from a previous spec run indicates that none of the contact objects include the country attribute. This fix addresses the issue by aligning the spec with the actual implementation logic. Here’s an example payload from the previous spec run for reference: Screenshot 2025-01-31 at 6 17 44 PM --- .../api/v1/accounts/contacts_controller_spec.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb index 97eddc171..60530ad22 100644 --- a/spec/controllers/api/v1/accounts/contacts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/contacts_controller_spec.rb @@ -91,16 +91,15 @@ RSpec.describe 'Contacts API', type: :request do end it 'returns all contacts with country name desc order with null values at last' do + contact_from_albania = create(:contact, :with_email, account: account, additional_attributes: { country_code: 'AL', country: 'Albania' }) get "/api/v1/accounts/#{account.id}/contacts?include_contact_inboxes=false&sort=country", headers: admin.create_new_auth_token, as: :json expect(response).to have_http_status(:success) response_body = response.parsed_body - # TODO: this spec has been flaky for a while, so adding a debug statement to see the response - Rails.logger.info(response_body) - expect(response_body['payload'].first['email']).to eq(contact.email) - expect(response_body['payload'].first['id']).to eq(contact.id) + expect(response_body['payload'].first['email']).to eq(contact_from_albania.email) + expect(response_body['payload'].first['id']).to eq(contact_from_albania.id) expect(response_body['payload'].last['email']).to eq(contact_4.email) end From bd94e5062d08bbcd6019b34b9ffb3cca60acb220 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Mon, 3 Feb 2025 19:34:50 +0530 Subject: [PATCH 3/3] chore: Search improvements (#10801) - Adds pagination support for search. - Use composition API on all search related component. - Minor UI improvements. - Adds missing specs Loom video https://www.loom.com/share/5b01afa5c9204e7d97ff81b215621dde?sid=82ca6d22-ca8c-4d5e-8740-ba06ca4051ba --- app/javascript/dashboard/api/search.js | 9 +- .../dashboard/i18n/locale/en/search.json | 3 + .../search/components/MessageContent.vue | 110 ++-- .../modules/search/components/ReadMore.vue | 21 +- .../search/components/SearchHeader.vue | 95 ++- .../modules/search/components/SearchInput.vue | 35 -- .../components/SearchResultContactItem.vue | 35 +- .../components/SearchResultContactsList.vue | 49 +- .../SearchResultConversationItem.vue | 127 ++-- .../SearchResultConversationsList.vue | 51 +- .../components/SearchResultMessagesList.vue | 62 +- .../search/components/SearchResultSection.vue | 6 +- .../modules/search/components/SearchTabs.vue | 66 +- .../modules/search/components/SearchView.vue | 581 ++++++++++-------- .../store/modules/conversationSearch.js | 30 +- .../specs/conversationSearch/actions.spec.js | 115 ++++ .../specs/conversationSearch/getters.spec.js | 43 +- .../conversationSearch/mutations.spec.js | 97 ++- .../dashboard/store/mutation-types.js | 1 + app/services/search_service.rb | 8 +- 20 files changed, 898 insertions(+), 646 deletions(-) delete mode 100644 app/javascript/dashboard/modules/search/components/SearchInput.vue diff --git a/app/javascript/dashboard/api/search.js b/app/javascript/dashboard/api/search.js index 7dc98dcf2..7abb584c0 100644 --- a/app/javascript/dashboard/api/search.js +++ b/app/javascript/dashboard/api/search.js @@ -14,26 +14,29 @@ class SearchAPI extends ApiClient { }); } - contacts({ q }) { + contacts({ q, page = 1 }) { return axios.get(`${this.url}/contacts`, { params: { q, + page: page, }, }); } - conversations({ q }) { + conversations({ q, page = 1 }) { return axios.get(`${this.url}/conversations`, { params: { q, + page: page, }, }); } - messages({ q }) { + messages({ q, page = 1 }) { return axios.get(`${this.url}/messages`, { params: { q, + page: page, }, }); } diff --git a/app/javascript/dashboard/i18n/locale/en/search.json b/app/javascript/dashboard/i18n/locale/en/search.json index d10c9c4fc..7a0fc4f82 100644 --- a/app/javascript/dashboard/i18n/locale/en/search.json +++ b/app/javascript/dashboard/i18n/locale/en/search.json @@ -11,7 +11,10 @@ "CONVERSATIONS": "Conversations", "MESSAGES": "Messages" }, + "VIEW_MORE": "View more", + "LOAD_MORE": "Load more", "SEARCHING_DATA": "Searching", + "LOADING_DATA": "Loading", "EMPTY_STATE": "No {item} found for query '{query}'", "EMPTY_STATE_FULL": "No results found for query '{query}'", "PLACEHOLDER_KEYBINDING": "/ to focus", diff --git a/app/javascript/dashboard/modules/search/components/MessageContent.vue b/app/javascript/dashboard/modules/search/components/MessageContent.vue index 4ea4714c2..fef35ac89 100644 --- a/app/javascript/dashboard/modules/search/components/MessageContent.vue +++ b/app/javascript/dashboard/modules/search/components/MessageContent.vue @@ -1,73 +1,59 @@ -