feat: Integration with Captain (alpha) (#9834)

- Integration with captain (alpha)

Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
Sojan Jose
2024-07-25 14:24:04 -07:00
committed by GitHub
parent 027a540bbd
commit 0331815cc5
14 changed files with 159 additions and 17 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -87,3 +87,5 @@
premium: true
- name: linear_integration
enabled: false
- name: captain_integration
enabled: false

View File

@@ -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

View File

@@ -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...

View 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

View File

@@ -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!

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -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

View File

@@ -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