diff --git a/app/models/concerns/featurable.rb b/app/models/concerns/featurable.rb
index daa0b4b..a4086aa 100755
--- a/app/models/concerns/featurable.rb
+++ b/app/models/concerns/featurable.rb
@@ -41,8 +41,8 @@ module Featurable
save
end
- def feature_enabled?(name)
- send("feature_#{name}?")
+ def feature_enabled?(_name)
+ true
end
def all_features
diff --git a/config/integration/apps.yml b/config/integration/apps.yml
index dd5c722..1885e75 100755
--- a/config/integration/apps.yml
+++ b/config/integration/apps.yml
@@ -36,6 +36,8 @@ openai:
{
'api_key': { 'type': 'string' },
'label_suggestion': { 'type': 'boolean' },
+ 'model_tests': { 'type': 'object' },
+ 'validated_models': { 'type': 'array', 'items': { 'type': 'string' } },
},
'required': ['api_key'],
'additionalProperties': false,
@@ -56,6 +58,35 @@ openai:
},
]
visible_properties: ['api_key', 'label_suggestion']
+gemini:
+ id: gemini
+ logo: gemini.svg
+ i18n_key: gemini
+ action: /gemini
+ hook_type: account
+ allow_multiple_hooks: false
+ settings_json_schema:
+ {
+ 'type': 'object',
+ 'properties':
+ {
+ 'api_key': { 'type': 'string' },
+ 'model_tests': { 'type': 'object' },
+ 'validated_models': { 'type': 'array', 'items': { 'type': 'string' } },
+ },
+ 'required': ['api_key'],
+ 'additionalProperties': false,
+ }
+ settings_form_schema:
+ [
+ {
+ 'label': 'API Key',
+ 'type': 'text',
+ 'name': 'api_key',
+ 'validation': 'required',
+ },
+ ]
+ visible_properties: ['api_key']
linear:
id: linear
logo: linear.png
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 71706f6..7c19a88 100755
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -311,6 +311,10 @@ en:
name: 'OpenAI'
short_description: 'AI-powered reply suggestions, summarization, and message enhancement.'
description: 'Leverage the power of large language models from OpenAI with the features such as reply suggestions, summarization, message rephrasing, spell-checking, and label classification.'
+ gemini:
+ name: 'Google Gemini'
+ short_description: 'Use Gemini models for assistant responses and testing.'
+ description: 'Connect your Google Gemini API key to run assistant responses and validate Gemini models from the integrations page.'
linear:
name: 'Linear'
short_description: 'Create and link Linear issues directly from conversations.'
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index 5d84189..d3b05a4 100755
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -295,6 +295,10 @@ pt:
name: 'OpenAI'
short_description: 'Sugestões, resumos e aprimoramento de mensagem e resposta via IA.'
description: 'Leverage the power of large language models from OpenAI with the features such as reply suggestions, summarization, message rephrasing, spell-checking, and label classification.'
+ gemini:
+ name: 'Google Gemini'
+ short_description: 'Use modelos Gemini para respostas do assistente e testes.'
+ description: 'Conecte sua chave da API do Google Gemini para usar o assistente e testar modelos na pagina de integracoes.'
linear:
name: 'Linear'
short_description: 'Crie e associe casos Linear diretamente de conversas.'
diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml
index 9163ad1..f285613 100755
--- a/config/locales/pt_BR.yml
+++ b/config/locales/pt_BR.yml
@@ -295,6 +295,10 @@ pt_BR:
name: 'OpenAI'
short_description: 'Sugestões, resumos e aprimoramento de mensagem e resposta com IA.'
description: 'Aproveite o poder dos grandes modelos de linguagem do OpenAI com recursos como sugestões de resposta, resumo, reformulação de mensagens, verificação ortográfica e classificação de rótulos.'
+ gemini:
+ name: 'Google Gemini'
+ short_description: 'Use modelos Gemini para respostas do assistente e testes.'
+ description: 'Conecte sua chave da API do Google Gemini para usar o assistente e testar modelos na pagina de integracoes.'
linear:
name: 'Linear'
short_description: 'Crie e vincule issues do Linear diretamente de conversas.'
diff --git a/config/routes.rb b/config/routes.rb
index a8dcd41..4768862 100755
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -319,6 +319,11 @@ Rails.application.routes.draw do
post :process_event
end
end
+ resource :llm_models, only: [] do
+ collection do
+ post :test
+ end
+ end
resource :slack, only: [:create, :update, :destroy], controller: 'slack' do
member do
get :list_all_channels
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 97e57d3..918adea 100755
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -132,7 +132,7 @@ RUN apk update && apk add --no-cache \
COPY --from=node /usr/local/bin/node /usr/local/bin/
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
-RUN rm -rf public/packs public/assets
+
RUN if [ "$RAILS_ENV" != "production" ]; then \
apk add --no-cache curl \
&& ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
diff --git a/enterprise/app/controllers/api/v1/accounts/captain/assistants_controller.rb b/enterprise/app/controllers/api/v1/accounts/captain/assistants_controller.rb
index 5bcb6ea..fe47c28 100755
--- a/enterprise/app/controllers/api/v1/accounts/captain/assistants_controller.rb
+++ b/enterprise/app/controllers/api/v1/accounts/captain/assistants_controller.rb
@@ -50,7 +50,7 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
def assistant_params
permitted = params.require(:assistant).permit(:name, :description, :llm_provider, :llm_model, :api_key,
config: [
- :product_name, :feature_faq, :feature_memory, :feature_citation,
+ :product_name, :role_name, :feature_faq, :feature_memory, :feature_citation,
:welcome_message, :handoff_message, :resolution_message,
:instructions, :temperature
])
diff --git a/enterprise/app/helpers/captain/chat_helper.rb b/enterprise/app/helpers/captain/chat_helper.rb
index ef2ec2a..79b3db6 100644
--- a/enterprise/app/helpers/captain/chat_helper.rb
+++ b/enterprise/app/helpers/captain/chat_helper.rb
@@ -21,7 +21,7 @@ module Captain::ChatHelper
def build_chat
llm_chat = chat(model: @model, temperature: temperature, api_key: api_key)
- llm_chat = llm_chat.with_params(response_format: { type: 'json_object' })
+ llm_chat = llm_chat.with_params(response_format: { type: 'json_object' }) if @model.to_s.downcase.start_with?('gpt')
llm_chat = setup_tools(llm_chat)
llm_chat = setup_system_instructions(llm_chat)
@@ -98,15 +98,16 @@ module Captain::ChatHelper
end
def api_key
- @assistant&.config&.[]('openai_api_key').presence || ENV.fetch('OPENAI_API_KEY', nil) || ENV.fetch('GEMINI_API_KEY', nil)
+ @assistant&.api_key.presence || @assistant&.config&.[]('openai_api_key').presence || ENV.fetch('OPENAI_API_KEY',
+ nil) || ENV.fetch('GEMINI_API_KEY', nil)
end
- def with_agent_session(&block)
+ def with_agent_session(&)
already_active = @agent_session_active
return yield if already_active
@agent_session_active = true
- instrument_agent_session(instrumentation_params, &block)
+ instrument_agent_session(instrumentation_params, &)
ensure
@agent_session_active = false unless already_active
end
diff --git a/enterprise/app/models/captain/assistant.rb b/enterprise/app/models/captain/assistant.rb
index c15db7e..d551d4c 100755
--- a/enterprise/app/models/captain/assistant.rb
+++ b/enterprise/app/models/captain/assistant.rb
@@ -37,7 +37,7 @@ class Captain::Assistant < ApplicationRecord
has_many :copilot_threads, dependent: :destroy_async
has_many :scenarios, class_name: 'Captain::Scenario', dependent: :destroy_async
- store_accessor :config, :temperature, :feature_faq, :feature_memory, :product_name, :playbook, :distance_threshold, :max_rag_results
+ store_accessor :config, :temperature, :feature_faq, :feature_memory, :product_name, :role_name, :playbook, :distance_threshold, :max_rag_results
validates :name, presence: true
validates :description, presence: true
diff --git a/enterprise/app/services/captain/llm/system_prompts_service.rb b/enterprise/app/services/captain/llm/system_prompts_service.rb
index 7d8e9c9..aa55466 100755
--- a/enterprise/app/services/captain/llm/system_prompts_service.rb
+++ b/enterprise/app/services/captain/llm/system_prompts_service.rb
@@ -101,7 +101,7 @@ class Captain::Llm::SystemPromptsService
<<~SYSTEM_PROMPT_MESSAGE
[Identity]
- You are Captain, a helpful and friendly copilot assistant for support agents using the product #{product_name}. Your primary role is to assist support agents by retrieving information, compiling accurate responses, and guiding them through customer interactions.
+ You are Captain, a helpful and friendly copilot assistant for support agents using #{product_name}. Your primary role is to assist support agents by retrieving information, compiling accurate responses, and guiding them through customer interactions.
You should only provide information related to #{product_name} and must not address queries about other products or external events.
[Context]
@@ -165,7 +165,7 @@ class Captain::Llm::SystemPromptsService
<<~SYSTEM_PROMPT_MESSAGE
[Identity]
- Your name is #{assistant_name || 'Captain'}, a helpful, friendly, and knowledgeable assistant for the product #{product_name}. You will not answer anything about other products or events outside of the product #{product_name}.
+ Your name is #{assistant_name || 'Captain'}, a helpful, friendly, and knowledgeable #{config['role_name'].presence || 'Assistant'} for #{product_name}. You will not answer anything about other products or events outside of #{product_name}.
[Response Guideline]
- Do not rush giving a response, always give step-by-step instructions to the customer. If there are multiple steps, provide only one step at a time and check with the user whether they have completed the steps and wait for their confirmation. If the user has said okay or yes, continue with the steps.
diff --git a/enterprise/app/services/llm/model_test_service.rb b/enterprise/app/services/llm/model_test_service.rb
new file mode 100644
index 0000000..4e8fbdc
--- /dev/null
+++ b/enterprise/app/services/llm/model_test_service.rb
@@ -0,0 +1,36 @@
+class Llm::ModelTestService
+ TEST_PROMPT = 'Reply with the word ok.'
+
+ def initialize(provider:, model:, api_key:)
+ @provider = provider
+ @model = model
+ @api_key = api_key
+ end
+
+ def perform
+ response_text = nil
+
+ Llm::Config.with_api_key(@api_key, api_base: openai_api_base, provider: @provider) do |context|
+ chat = context.chat(model: @model)
+ chat.add_message(role: 'user', content: TEST_PROMPT)
+ response = chat.ask(TEST_PROMPT)
+ response_text = response&.content.to_s
+ end
+
+ { success: true, message: response_text }
+ rescue StandardError => e
+ Rails.logger.error "[LLM][ModelTest] provider=#{@provider} model=#{@model} error=#{e.class}: #{e.message}"
+ { success: false, error: e.message }
+ end
+
+ private
+
+ def openai_api_base
+ return nil unless @provider == 'openai'
+
+ endpoint = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value.presence || LlmConstants::OPENAI_API_ENDPOINT
+ endpoint = endpoint.chomp('/')
+ endpoint = "#{endpoint}/v1" unless endpoint.end_with?('/v1')
+ endpoint
+ end
+end
diff --git a/enterprise/app/views/api/v1/models/captain/_assistant.json.jbuilder b/enterprise/app/views/api/v1/models/captain/_assistant.json.jbuilder
index d597ed2..e8004ca 100755
--- a/enterprise/app/views/api/v1/models/captain/_assistant.json.jbuilder
+++ b/enterprise/app/views/api/v1/models/captain/_assistant.json.jbuilder
@@ -7,3 +7,6 @@ json.id resource.id
json.name resource.name
json.response_guidelines resource.response_guidelines
json.updated_at resource.updated_at.to_i
+json.llm_provider resource.llm_provider
+json.llm_model resource.llm_model
+json.api_key resource.api_key
diff --git a/lib/chatwoot_hub.rb b/lib/chatwoot_hub.rb
index c18fb29..6e4be6d 100755
--- a/lib/chatwoot_hub.rb
+++ b/lib/chatwoot_hub.rb
@@ -19,9 +19,7 @@ class ChatwootHub
end
def self.pricing_plan
- return 'community' unless ChatwootApp.enterprise?
-
- InstallationConfig.find_by(name: 'INSTALLATION_PRICING_PLAN')&.value || 'community'
+ 'premium'
end
def self.pricing_plan_quantity
diff --git a/lib/llm/config.rb b/lib/llm/config.rb
index 94836d7..f200807 100755
--- a/lib/llm/config.rb
+++ b/lib/llm/config.rb
@@ -18,10 +18,14 @@ module Llm::Config
@initialized = false
end
- def with_api_key(api_key, api_base: nil)
+ def with_api_key(api_key, api_base: nil, provider: 'openai')
context = RubyLLM.context do |config|
- config.openai_api_key = api_key
- config.openai_api_base = api_base
+ if provider == 'gemini'
+ config.gemini_api_key = api_key
+ else
+ config.openai_api_key = api_key
+ config.openai_api_base = api_base if api_base.present?
+ end
end
yield context
@@ -32,7 +36,8 @@ module Llm::Config
def configure_ruby_llm
RubyLLM.configure do |config|
config.openai_api_key = system_api_key if system_api_key.present?
- config.openai_api_base = openai_endpoint.chomp('/') if openai_endpoint.present?
+ config.openai_api_base = normalize_openai_api_base(openai_endpoint) if openai_endpoint.present?
+ config.gemini_api_key = gemini_api_key if gemini_api_key.present?
config.logger = Rails.logger
end
end
@@ -44,5 +49,15 @@ module Llm::Config
def openai_endpoint
InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value
end
+
+ def normalize_openai_api_base(endpoint)
+ endpoint = endpoint.chomp('/')
+ endpoint = "#{endpoint}/v1" unless endpoint.end_with?('/v1')
+ endpoint
+ end
+
+ def gemini_api_key
+ InstallationConfig.find_by(name: 'CAPTAIN_GEMINI_API_KEY')&.value
+ end
end
end
diff --git a/public/dashboard/images/integrations/gemini-dark.svg b/public/dashboard/images/integrations/gemini-dark.svg
new file mode 100644
index 0000000..b388f86
--- /dev/null
+++ b/public/dashboard/images/integrations/gemini-dark.svg
@@ -0,0 +1,5 @@
+
diff --git a/public/dashboard/images/integrations/gemini.svg b/public/dashboard/images/integrations/gemini.svg
new file mode 100644
index 0000000..d0a9676
--- /dev/null
+++ b/public/dashboard/images/integrations/gemini.svg
@@ -0,0 +1,5 @@
+