From 9a405d65baa3f79913971f4aa35e5ff5c98a3f39 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Wed, 11 Dec 2024 01:08:25 +0530 Subject: [PATCH 1/3] fix: RemoveStaleRedisKeys service (#10562) Fixes issues with RemoveStaleRedisKeys service Co-authored-by: Sojan --- .../internal/process_stale_redis_keys_job.rb | 3 ++- .../internal/remove_stale_redis_keys_job.rb | 8 +++++--- .../remove_stale_redis_keys_service.rb | 11 ++++++++--- .../process_stale_redis_keys_job_spec.rb | 15 +++++++++++++++ .../remove_stale_redis_keys_job_spec.rb | 13 +++++++++++++ .../remove_stale_redis_keys_service_spec.rb | 18 ++++++++++++++++++ 6 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 spec/jobs/internal/process_stale_redis_keys_job_spec.rb create mode 100644 spec/jobs/internal/remove_stale_redis_keys_job_spec.rb create mode 100644 spec/services/internal/remove_stale_redis_keys_service_spec.rb diff --git a/app/jobs/internal/process_stale_redis_keys_job.rb b/app/jobs/internal/process_stale_redis_keys_job.rb index 3afb3b434..30c3cffed 100644 --- a/app/jobs/internal/process_stale_redis_keys_job.rb +++ b/app/jobs/internal/process_stale_redis_keys_job.rb @@ -6,6 +6,7 @@ class Internal::ProcessStaleRedisKeysJob < ApplicationJob queue_as :low def perform(account) - Internal::RemoveStaleRedisKeysService.new(account_id: account.id).perform + removed_count = Internal::RemoveStaleRedisKeysService.new(account_id: account.id).perform + Rails.logger.info "Successfully cleaned up Redis keys for account #{account.id} (removed #{removed_count} keys)" end end diff --git a/app/jobs/internal/remove_stale_redis_keys_job.rb b/app/jobs/internal/remove_stale_redis_keys_job.rb index 667fa398b..da3892b3c 100644 --- a/app/jobs/internal/remove_stale_redis_keys_job.rb +++ b/app/jobs/internal/remove_stale_redis_keys_job.rb @@ -6,9 +6,11 @@ class Internal::RemoveStaleRedisKeysJob < ApplicationJob queue_as :scheduled_jobs def perform - Account.find_each do |account| - Rails.logger.info "Enqueuing ProcessStaleRedisKeysJob for account #{account.id}" - Internal::ProcessStaleRedisKeysJob.perform_later(account) + Account.find_in_batches(batch_size: 100) do |accounts| + accounts.each do |account| + Rails.logger.info "Enqueuing ProcessStaleRedisKeysJob for account #{account.id}" + Internal::ProcessStaleRedisKeysJob.perform_later(account) + end end end end diff --git a/app/services/internal/remove_stale_redis_keys_service.rb b/app/services/internal/remove_stale_redis_keys_service.rb index a3e1da207..553cc6c6a 100644 --- a/app/services/internal/remove_stale_redis_keys_service.rb +++ b/app/services/internal/remove_stale_redis_keys_service.rb @@ -1,10 +1,15 @@ class Internal::RemoveStaleRedisKeysService pattr_initialize [:account_id!] - def perform(account_id) - range_start = (Time.zone.now - PRESENCE_DURATION).to_i + def perform + Rails.logger.info "Removing redis stale keys for account #{@account_id}" + range_start = (Time.zone.now - OnlineStatusTracker::PRESENCE_DURATION).to_i # exclusive minimum score is specified by prefixing ( # we are clearing old records because this could clogg up the sorted set - ::Redis::Alfred.zremrangebyscore(presence_key(account_id, 'Contact'), '-inf', "(#{range_start}") + ::Redis::Alfred.zremrangebyscore( + OnlineStatusTracker.presence_key(@account_id, 'Contact'), + '-inf', + "(#{range_start}" + ) end end diff --git a/spec/jobs/internal/process_stale_redis_keys_job_spec.rb b/spec/jobs/internal/process_stale_redis_keys_job_spec.rb new file mode 100644 index 000000000..6189b1b5c --- /dev/null +++ b/spec/jobs/internal/process_stale_redis_keys_job_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +RSpec.describe Internal::ProcessStaleRedisKeysJob do + let(:account) { create(:account) } + + describe '#perform' do + it 'calls the RemoveStaleRedisKeysService with the correct account ID' do + expect(Internal::RemoveStaleRedisKeysService).to receive(:new) + .with(account_id: account.id) + .and_call_original + + described_class.perform_now(account) + end + end +end diff --git a/spec/jobs/internal/remove_stale_redis_keys_job_spec.rb b/spec/jobs/internal/remove_stale_redis_keys_job_spec.rb new file mode 100644 index 000000000..a1b98c9db --- /dev/null +++ b/spec/jobs/internal/remove_stale_redis_keys_job_spec.rb @@ -0,0 +1,13 @@ +require 'rails_helper' + +RSpec.describe Internal::RemoveStaleRedisKeysJob do + let(:account) { create(:account) } + + describe '#perform' do + it 'enqueues ProcessStaleRedisKeysJob for the account' do + expect(Internal::ProcessStaleRedisKeysJob).to receive(:perform_later).with(account) + + described_class.perform_now + end + end +end diff --git a/spec/services/internal/remove_stale_redis_keys_service_spec.rb b/spec/services/internal/remove_stale_redis_keys_service_spec.rb new file mode 100644 index 000000000..75d182aab --- /dev/null +++ b/spec/services/internal/remove_stale_redis_keys_service_spec.rb @@ -0,0 +1,18 @@ +require 'rails_helper' + +RSpec.describe Internal::RemoveStaleRedisKeysService, type: :service do + let(:account_id) { 1 } + let(:service) { described_class.new(account_id: account_id) } + + describe '#perform' do + it 'removes stale Redis keys for the specified account' do + presence_key = OnlineStatusTracker.presence_key(account_id, 'Contact') + + # Mock Redis calls + expect(Redis::Alfred).to receive(:zremrangebyscore) + .with(presence_key, '-inf', anything) + + service.perform + end + end +end From 10a03339801492ab9fa91ed513770c6dbe7be031 Mon Sep 17 00:00:00 2001 From: Pranav Date: Tue, 10 Dec 2024 15:36:48 -0800 Subject: [PATCH 2/3] feat(ee): Add copilot integration (v1) to the conversation sidebar (#10566) --- .../integrations/captain_controller.rb | 29 ++++++- app/javascript/dashboard/api/integrations.js | 4 + .../dashboard/assets/scss/_woot.scss | 26 ++++++ .../components-next/copilot/Copilot.story.vue | 60 ++++++++++++++ .../components-next/copilot/Copilot.vue | 68 ++++++++++++++++ .../copilot/CopilotAgentMessage.vue | 31 ++++++++ .../copilot/CopilotAssistantMessage.vue | 27 +++++++ .../components-next/copilot/CopilotInput.vue | 34 ++++++++ .../copilot/CopilotLoader.story.vue | 12 +++ .../components-next/copilot/CopilotLoader.vue | 22 ++++++ .../dashboard/components/ChatList.vue | 11 +-- .../components/copilot/CopilotContainer.vue | 58 ++++++++++++++ .../widgets/conversation/ConversationBox.vue | 26 ++---- .../conversation/ConversationSidebar.vue | 79 +++++++++++++++++++ .../i18n/locale/en/conversation.json | 4 + .../i18n/locale/en/integrations.json | 8 ++ .../dashboard/conversation/ContactPanel.vue | 4 +- .../conversation/contact/ContactInfo.vue | 2 +- app/models/concerns/llm_formattable.rb | 7 ++ app/models/conversation.rb | 1 + .../conversation_llm_formatter.rb | 32 ++++++++ .../llm_formatter/default_llm_formatter.rb | 9 +++ .../llm_text_formatter_service.rb | 20 +++++ config/routes.rb | 1 + .../integrations/captain_controller_spec.rb | 45 ++++++++++- .../conversation_llm_formatter_spec.rb | 51 ++++++++++++ theme/colors.js | 15 ++++ 27 files changed, 650 insertions(+), 36 deletions(-) create mode 100644 app/javascript/dashboard/components-next/copilot/Copilot.story.vue create mode 100644 app/javascript/dashboard/components-next/copilot/Copilot.vue create mode 100644 app/javascript/dashboard/components-next/copilot/CopilotAgentMessage.vue create mode 100644 app/javascript/dashboard/components-next/copilot/CopilotAssistantMessage.vue create mode 100644 app/javascript/dashboard/components-next/copilot/CopilotInput.vue create mode 100644 app/javascript/dashboard/components-next/copilot/CopilotLoader.story.vue create mode 100644 app/javascript/dashboard/components-next/copilot/CopilotLoader.vue create mode 100644 app/javascript/dashboard/components/copilot/CopilotContainer.vue create mode 100644 app/javascript/dashboard/components/widgets/conversation/ConversationSidebar.vue create mode 100644 app/models/concerns/llm_formattable.rb create mode 100644 app/services/llm_formatter/conversation_llm_formatter.rb create mode 100644 app/services/llm_formatter/default_llm_formatter.rb create mode 100644 app/services/llm_formatter/llm_text_formatter_service.rb create mode 100644 spec/services/llm_formatter/conversation_llm_formatter_spec.rb diff --git a/app/controllers/api/v1/accounts/integrations/captain_controller.rb b/app/controllers/api/v1/accounts/integrations/captain_controller.rb index 15e81b726..6813d14b5 100644 --- a/app/controllers/api/v1/accounts/integrations/captain_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/captain_controller.rb @@ -2,10 +2,22 @@ class Api::V1::Accounts::Integrations::CaptainController < Api::V1::Accounts::Ba before_action :hook def proxy + request_url = build_request_url(request_path) response = HTTParty.send(request_method, request_url, body: permitted_params[:body].to_json, headers: headers) render plain: response.body, status: response.code end + def copilot + request_url = build_request_url(build_request_path("/assistants/#{hook.settings['assistant_id']}/copilot")) + params = { + previous_messages: copilot_params[:previous_messages], + conversation_history: conversation_history, + message: copilot_params[:message] + } + response = HTTParty.send(:post, request_url, body: params.to_json, headers: headers) + render plain: response.body, status: response.code + end + private def headers @@ -17,15 +29,19 @@ class Api::V1::Accounts::Integrations::CaptainController < Api::V1::Accounts::Ba } end + def build_request_path(route) + "api/accounts/#{hook.settings['account_id']}#{route}" + end + def request_path request_route = with_leading_hash_on_route(params[:route]) return 'api/sessions/profile' if request_route == '/sessions/profile' - "api/accounts/#{hook.settings['account_id']}#{request_route}" + build_request_path(request_route) end - def request_url + def build_request_url(request_path) base_url = InstallationConfig.find_by(name: 'CAPTAIN_API_URL').value URI.join(base_url, request_path).to_s end @@ -47,6 +63,15 @@ class Api::V1::Accounts::Integrations::CaptainController < Api::V1::Accounts::Ba request_route.start_with?('/') ? request_route : "/#{request_route}" end + def conversation_history + conversation = Current.account.conversations.find_by!(display_id: copilot_params[:conversation_id]) + conversation.to_llm_text + end + + def copilot_params + params.permit(:previous_messages, :conversation_id, :message) + end + def permitted_params params.permit(:method, :route, body: {}) end diff --git a/app/javascript/dashboard/api/integrations.js b/app/javascript/dashboard/api/integrations.js index 59a3e244a..b78a2ea7e 100644 --- a/app/javascript/dashboard/api/integrations.js +++ b/app/javascript/dashboard/api/integrations.js @@ -36,6 +36,10 @@ class IntegrationsAPI extends ApiClient { requestCaptain(body) { return axios.post(`${this.baseUrl()}/integrations/captain/proxy`, body); } + + requestCaptainCopilot(body) { + return axios.post(`${this.baseUrl()}/integrations/captain/copilot`, body); + } } export default new IntegrationsAPI(); diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss index e3474e616..fdd467f5f 100644 --- a/app/javascript/dashboard/assets/scss/_woot.scss +++ b/app/javascript/dashboard/assets/scss/_woot.scss @@ -72,6 +72,19 @@ --slate-11: 96 100 108; --slate-12: 28 32 36; + --iris-1: 253 253 255; + --iris-2: 248 248 255; + --iris-3: 240 241 254; + --iris-4: 230 231 255; + --iris-5: 218 220 255; + --iris-6: 203 205 255; + --iris-7: 184 186 248; + --iris-8: 155 158 240; + --iris-9: 91 91 214; + --iris-10: 81 81 205; + --iris-11: 87 83 198; + --iris-12: 39 41 98; + --ruby-1: 255 252 253; --ruby-2: 255 247 248; --ruby-3: 254 234 237; @@ -147,6 +160,19 @@ --slate-11: 176 180 186; --slate-12: 237 238 240; + --iris-1: 19 19 30; + --iris-2: 23 22 37; + --iris-3: 32 34 72; + --iris-4: 38 42 101; + --iris-5: 48 51 116; + --iris-6: 61 62 130; + --iris-7: 74 74 149; + --iris-8: 89 88 177; + --iris-9: 91 91 214; + --iris-10: 84 114 228; + --iris-11: 158 177 255; + --iris-12: 224 223 254; + --ruby-1: 25 17 19; --ruby-2: 30 21 23; --ruby-3: 58 20 30; diff --git a/app/javascript/dashboard/components-next/copilot/Copilot.story.vue b/app/javascript/dashboard/components-next/copilot/Copilot.story.vue new file mode 100644 index 000000000..ece5ec31f --- /dev/null +++ b/app/javascript/dashboard/components-next/copilot/Copilot.story.vue @@ -0,0 +1,60 @@ + + + diff --git a/app/javascript/dashboard/components-next/copilot/Copilot.vue b/app/javascript/dashboard/components-next/copilot/Copilot.vue new file mode 100644 index 000000000..3839532f2 --- /dev/null +++ b/app/javascript/dashboard/components-next/copilot/Copilot.vue @@ -0,0 +1,68 @@ + + + diff --git a/app/javascript/dashboard/components-next/copilot/CopilotAgentMessage.vue b/app/javascript/dashboard/components-next/copilot/CopilotAgentMessage.vue new file mode 100644 index 000000000..ef8c2faf4 --- /dev/null +++ b/app/javascript/dashboard/components-next/copilot/CopilotAgentMessage.vue @@ -0,0 +1,31 @@ + + + diff --git a/app/javascript/dashboard/components-next/copilot/CopilotAssistantMessage.vue b/app/javascript/dashboard/components-next/copilot/CopilotAssistantMessage.vue new file mode 100644 index 000000000..a14508150 --- /dev/null +++ b/app/javascript/dashboard/components-next/copilot/CopilotAssistantMessage.vue @@ -0,0 +1,27 @@ + + + diff --git a/app/javascript/dashboard/components-next/copilot/CopilotInput.vue b/app/javascript/dashboard/components-next/copilot/CopilotInput.vue new file mode 100644 index 000000000..bf14945b5 --- /dev/null +++ b/app/javascript/dashboard/components-next/copilot/CopilotInput.vue @@ -0,0 +1,34 @@ + + + diff --git a/app/javascript/dashboard/components-next/copilot/CopilotLoader.story.vue b/app/javascript/dashboard/components-next/copilot/CopilotLoader.story.vue new file mode 100644 index 000000000..3a5de84c7 --- /dev/null +++ b/app/javascript/dashboard/components-next/copilot/CopilotLoader.story.vue @@ -0,0 +1,12 @@ + + + diff --git a/app/javascript/dashboard/components-next/copilot/CopilotLoader.vue b/app/javascript/dashboard/components-next/copilot/CopilotLoader.vue new file mode 100644 index 000000000..723439f33 --- /dev/null +++ b/app/javascript/dashboard/components-next/copilot/CopilotLoader.vue @@ -0,0 +1,22 @@ + + + diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 0108e02df..953d5f2cc 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -785,7 +785,7 @@ watch(conversationFilters, (newVal, oldVal) => { class="flex flex-col flex-shrink-0 border-r conversations-list-wrap rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50" :class="[ { hidden: !showConversationList }, - isOnExpandedLayout ? 'basis-full' : 'flex-basis-clamp', + isOnExpandedLayout ? 'basis-full' : 'w-[360px]', ]" > @@ -916,12 +916,3 @@ watch(conversationFilters, (newVal, oldVal) => { - - diff --git a/app/javascript/dashboard/components/copilot/CopilotContainer.vue b/app/javascript/dashboard/components/copilot/CopilotContainer.vue new file mode 100644 index 000000000..940408ef8 --- /dev/null +++ b/app/javascript/dashboard/components/copilot/CopilotContainer.vue @@ -0,0 +1,58 @@ + + + diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue index 210357a8b..f56b2a581 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue @@ -1,14 +1,14 @@ + + diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index 9a6a64d6d..f9c3b784a 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -219,6 +219,10 @@ "DELETE": "Delete", "CANCEL": "Cancel" } + }, + "SIDEBAR": { + "CONTACT": "Contact", + "COPILOT": "Copilot" } }, "EMAIL_TRANSCRIPT": { diff --git a/app/javascript/dashboard/i18n/locale/en/integrations.json b/app/javascript/dashboard/i18n/locale/en/integrations.json index f6e8cbc3a..e04164902 100644 --- a/app/javascript/dashboard/i18n/locale/en/integrations.json +++ b/app/javascript/dashboard/i18n/locale/en/integrations.json @@ -299,5 +299,13 @@ "ERROR": "There was an error unlinking the issue, please try again" } } + }, + "CAPTAIN": { + "NAME": "Captain", + "COPILOT": { + "SEND_MESSAGE": "Send message...", + "LOADER": "Captain is thinking", + "YOU": "You" + } } } diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue index e998a85a1..8739b4f26 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue @@ -92,9 +92,7 @@ onMounted(() => {