mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 02:32:29 +00:00 
			
		
		
		
	feat: Integration with Captain (alpha) (#9834)
- Integration with captain (alpha) Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
		| @@ -257,3 +257,5 @@ AZURE_APP_SECRET= | ||||
| # Set to true if you want to remove stale contact inboxes | ||||
| # contact_inboxes with no conversation older than 90 days will be removed | ||||
| # REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false | ||||
|  | ||||
| # CAPTAIN_API_URL=http://localhost:3001/api | ||||
|   | ||||
| @@ -10,7 +10,7 @@ class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseC | ||||
|   private | ||||
|  | ||||
|   def fetch_apps | ||||
|     @apps = Integrations::App.all.select(&:active?) | ||||
|     @apps = Integrations::App.all.select { |app| app.active?(Current.account) } | ||||
|   end | ||||
|  | ||||
|   def fetch_app | ||||
|   | ||||
| @@ -9,6 +9,8 @@ class HookJob < ApplicationJob | ||||
|       process_slack_integration(hook, event_name, event_data) | ||||
|     when 'dialogflow' | ||||
|       process_dialogflow_integration(hook, event_name, event_data) | ||||
|     when 'captain' | ||||
|       process_captain_integration(hook, event_name, event_data) | ||||
|     when 'google_translate' | ||||
|       google_translate_integration(hook, event_name, event_data) | ||||
|     end | ||||
| @@ -35,6 +37,12 @@ class HookJob < ApplicationJob | ||||
|     Integrations::Dialogflow::ProcessorService.new(event_name: event_name, hook: hook, event_data: event_data).perform | ||||
|   end | ||||
|  | ||||
|   def process_captain_integration(hook, event_name, event_data) | ||||
|     return unless ['message.created'].include?(event_name) | ||||
|  | ||||
|     Integrations::Captain::ProcessorService.new(event_name: event_data, hook: hook, event_data: event_data).perform | ||||
|   end | ||||
|  | ||||
|   def google_translate_integration(hook, event_name, event_data) | ||||
|     return unless ['message.created'].include?(event_name) | ||||
|  | ||||
|   | ||||
| @@ -129,7 +129,16 @@ class Inbox < ApplicationRecord | ||||
|   end | ||||
|  | ||||
|   def active_bot? | ||||
|     agent_bot_inbox&.active? || hooks.where(app_id: 'dialogflow', status: 'enabled').count.positive? | ||||
|     agent_bot_inbox&.active? || hooks.where(app_id: %w[dialogflow], | ||||
|                                             status: 'enabled').count.positive? || captain_enabled? | ||||
|   end | ||||
|  | ||||
|   def captain_enabled? | ||||
|     captain_hook = account.hooks.where( | ||||
|       app_id: %w[captain], status: 'enabled' | ||||
|     ).first | ||||
|  | ||||
|     captain_hook.present? && captain_hook.settings['inbox_ids'].split(',').include?(id.to_s) | ||||
|   end | ||||
|  | ||||
|   def inbox_type | ||||
|   | ||||
| @@ -34,12 +34,14 @@ class Integrations::App | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def active? | ||||
|   def active?(account) | ||||
|     case params[:id] | ||||
|     when 'slack' | ||||
|       ENV['SLACK_CLIENT_SECRET'].present? | ||||
|     when 'linear' | ||||
|       Current.account.feature_enabled?('linear_integration') | ||||
|       account.feature_enabled?('linear_integration') | ||||
|     when 'captain' | ||||
|       account.feature_enabled?('captain_integration') && ENV['CAPTAIN_API_URL'].present? | ||||
|     else | ||||
|       true | ||||
|     end | ||||
|   | ||||
| @@ -87,3 +87,5 @@ | ||||
|   premium: true | ||||
| - name: linear_integration | ||||
|   enabled: false | ||||
| - name: captain_integration | ||||
|   enabled: false | ||||
|   | ||||
| @@ -8,7 +8,58 @@ | ||||
| # settings_json_schema: the json schema used to validate the settings hash (https://json-schema.org/) | ||||
| # settings_form_schema: the formulate schema used in frontend to render settings form (https://vueformulate.com/) | ||||
| ######################################################## | ||||
|  | ||||
| captain: | ||||
|   id: captain | ||||
|   logo: captain.png | ||||
|   i18n_key: captain | ||||
|   action: /captain | ||||
|   hook_type: account | ||||
|   allow_multiple_hooks: false | ||||
|   settings_json_schema: { | ||||
|     "type": "object", | ||||
|     "properties": { | ||||
|         "access_token": { "type": "string" }, | ||||
|         "account_id": { "type": "string" }, | ||||
|         "account_email": { "type": "string" }, | ||||
|         "assistant_id": { "type": "string" }, | ||||
|         "inbox_ids": { "type": "strings" }, | ||||
|       }, | ||||
|     "required": ["access_token", "account_id", "account_email", "assistant_id"], | ||||
|     "additionalProperties": false, | ||||
|   } | ||||
|   settings_form_schema: [ | ||||
|     { | ||||
|       "label": "Access Token", | ||||
|       "type": "text", | ||||
|       "name": "access_token", | ||||
|       "validation": "required", | ||||
|     }, | ||||
|     { | ||||
|       "label": "Account ID", | ||||
|       "type": "text", | ||||
|       "name": "account_id", | ||||
|       "validation": "required", | ||||
|     }, | ||||
|     { | ||||
|       "label": "Account Email", | ||||
|       "type": "text", | ||||
|       "name": "account_email", | ||||
|       "validation": "required", | ||||
|     }, | ||||
|     { | ||||
|       "label": "Assistant Id", | ||||
|       "type": "text", | ||||
|       "name": "assistant_id", | ||||
|       "validation": "required", | ||||
|     }, | ||||
|     { | ||||
|       "label": "Inbox Ids", | ||||
|       "type": "text", | ||||
|       "name": "inbox_ids", | ||||
|       "validation": "", | ||||
|     }, | ||||
|   ] | ||||
|   visible_properties: [] | ||||
| webhooks: | ||||
|   id: webhook | ||||
|   logo: webhooks.png | ||||
|   | ||||
| @@ -227,6 +227,9 @@ en: | ||||
|     linear: | ||||
|       name: "Linear" | ||||
|       description: "Create issues in Linear directly from your conversation window. Alternatively, link existing Linear issues for a more streamlined and efficient issue tracking process." | ||||
|     captain: | ||||
|       name: "Captain" | ||||
|       description: "Captain is a native AI assistant built for your product and trained on your company's knowledge base. It responds like a human and resolves customer queries effectively. Configure it to your inboxes easily." | ||||
|   public_portal: | ||||
|     search: | ||||
|       search_placeholder: Search for article by title or body... | ||||
|   | ||||
							
								
								
									
										65
									
								
								lib/integrations/captain/processor_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								lib/integrations/captain/processor_service.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| class Integrations::Captain::ProcessorService < Integrations::BotProcessorService | ||||
|   pattr_initialize [:event_name!, :hook!, :event_data!] | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def get_response(_session_id, message_content) | ||||
|     call_captain(message_content) | ||||
|   end | ||||
|  | ||||
|   def process_response(message, response) | ||||
|     if response == 'conversation_handoff' | ||||
|       message.conversation.bot_handoff! | ||||
|     else | ||||
|       create_conversation(message, { content: response }) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def create_conversation(message, content_params) | ||||
|     return if content_params.blank? | ||||
|  | ||||
|     conversation = message.conversation | ||||
|     conversation.messages.create!( | ||||
|       content_params.merge( | ||||
|         { | ||||
|           message_type: :outgoing, | ||||
|           account_id: conversation.account_id, | ||||
|           inbox_id: conversation.inbox_id | ||||
|         } | ||||
|       ) | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   def call_captain(message_content) | ||||
|     url = "#{ENV.fetch('CAPTAIN_API_URL', nil)}/accounts/#{hook.settings['account_id']}/assistants/#{hook.settings['assistant_id']}/chat" | ||||
|  | ||||
|     headers = { | ||||
|       'X-USER-EMAIL' => hook.settings['account_email'], | ||||
|       'X-USER-TOKEN' => hook.settings['access_token'], | ||||
|       'Content-Type' => 'application/json' | ||||
|     } | ||||
|  | ||||
|     body = { | ||||
|       message: message_content, | ||||
|       previous_messages: previous_messages | ||||
|     } | ||||
|  | ||||
|     response = HTTParty.post(url, headers: headers, body: body.to_json) | ||||
|     response.parsed_response['message'] | ||||
|   end | ||||
|  | ||||
|   def previous_messages | ||||
|     previous_messages = [] | ||||
|     conversation.messages.where(message_type: [:outgoing, :incoming]).where(private: false).offset(1).find_each do |message| | ||||
|       next if message.content_type != 'text' | ||||
|  | ||||
|       role = determine_role(message) | ||||
|       previous_messages << { message: message.content, type: role } | ||||
|     end | ||||
|     previous_messages | ||||
|   end | ||||
|  | ||||
|   def determine_role(message) | ||||
|     message.message_type == 'incoming' ? 'User' : 'Bot' | ||||
|   end | ||||
| end | ||||
| @@ -14,13 +14,13 @@ class Integrations::Dialogflow::ProcessorService < Integrations::BotProcessorSer | ||||
|     message.content | ||||
|   end | ||||
|  | ||||
|   def get_response(session_id, message) | ||||
|   def get_response(session_id, message_content) | ||||
|     if hook.settings['credentials'].blank? | ||||
|       Rails.logger.warn "Account: #{hook.try(:account_id)} Hook: #{hook.id} credentials are not present." && return | ||||
|     end | ||||
|  | ||||
|     configure_dialogflow_client_defaults | ||||
|     detect_intent(session_id, message) | ||||
|     detect_intent(session_id, message_content) | ||||
|   rescue Google::Cloud::PermissionDeniedError => e | ||||
|     Rails.logger.warn "DialogFlow Error: (account-#{hook.try(:account_id)}, hook-#{hook.id}) #{e.message}" | ||||
|     hook.prompt_reauthorization! | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								public/dashboard/images/integrations/captain-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/dashboard/images/integrations/captain-dark.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/dashboard/images/integrations/captain.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/dashboard/images/integrations/captain.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.4 KiB | 
| @@ -16,7 +16,7 @@ RSpec.describe 'Integration Apps API', type: :request do | ||||
|       let(:admin) { create(:user, account: account, role: :administrator) } | ||||
|  | ||||
|       it 'returns all active apps without sensitive information if the user is an agent' do | ||||
|         first_app = Integrations::App.all.find(&:active?) | ||||
|         first_app = Integrations::App.all.find { |app| app.active?(account) } | ||||
|         get api_v1_account_integrations_apps_url(account), | ||||
|             headers: agent.create_new_auth_token, | ||||
|             as: :json | ||||
| @@ -41,7 +41,7 @@ RSpec.describe 'Integration Apps API', type: :request do | ||||
|       end | ||||
|  | ||||
|       it 'returns all active apps with sensitive information if user is an admin' do | ||||
|         first_app = Integrations::App.all.find(&:active?) | ||||
|         first_app = Integrations::App.all.find { |app| app.active?(account) } | ||||
|         get api_v1_account_integrations_apps_url(account), | ||||
|             headers: admin.create_new_auth_token, | ||||
|             as: :json | ||||
|   | ||||
| @@ -5,10 +5,6 @@ RSpec.describe Integrations::App do | ||||
|   let(:app) { apps.find(id: app_name) } | ||||
|   let(:account) { create(:account) } | ||||
|  | ||||
|   before do | ||||
|     allow(Current).to receive(:account).and_return(account) | ||||
|   end | ||||
|  | ||||
|   describe '#name' do | ||||
|     let(:app_name) { 'slack' } | ||||
|  | ||||
| @@ -28,6 +24,10 @@ RSpec.describe Integrations::App do | ||||
|   describe '#action' do | ||||
|     let(:app_name) { 'slack' } | ||||
|  | ||||
|     before do | ||||
|       allow(Current).to receive(:account).and_return(account) | ||||
|     end | ||||
|  | ||||
|     context 'when the app is slack' do | ||||
|       it 'returns the action URL with client_id and redirect_uri' do | ||||
|         with_modified_env SLACK_CLIENT_ID: 'dummy_client_id' do | ||||
| @@ -46,7 +46,7 @@ RSpec.describe Integrations::App do | ||||
|     context 'when the app is slack' do | ||||
|       it 'returns true if SLACK_CLIENT_SECRET is present' do | ||||
|         with_modified_env SLACK_CLIENT_SECRET: 'random_secret' do | ||||
|           expect(app.active?).to be true | ||||
|           expect(app.active?(account)).to be true | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| @@ -55,14 +55,14 @@ RSpec.describe Integrations::App do | ||||
|       let(:app_name) { 'linear' } | ||||
|  | ||||
|       it 'returns true if the linear integration feature is disabled' do | ||||
|         expect(app.active?).to be false | ||||
|         expect(app.active?(account)).to be false | ||||
|       end | ||||
|  | ||||
|       it 'returns false if the linear integration feature is enabled' do | ||||
|         account.enable_features('linear_integration') | ||||
|         account.save! | ||||
|  | ||||
|         expect(app.active?).to be true | ||||
|         expect(app.active?(account)).to be true | ||||
|       end | ||||
|     end | ||||
|  | ||||
| @@ -70,7 +70,7 @@ RSpec.describe Integrations::App do | ||||
|       let(:app_name) { 'webhook' } | ||||
|  | ||||
|       it 'returns true' do | ||||
|         expect(app.active?).to be true | ||||
|         expect(app.active?(account)).to be true | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Sojan Jose
					Sojan Jose