From 477a8eb83a0dff243182aa847aed34e3d7c61486 Mon Sep 17 00:00:00 2001 From: Rodrigo Borba Date: Mon, 5 Jan 2026 11:23:50 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Adiciona=20integra=C3=A7=C3=A3o=20Googl?= =?UTF-8?q?e=20Gemini,=20teste=20de=20modelos=20LLM=20e=20melhorias=20na?= =?UTF-8?q?=20interface=20de=20integra=C3=A7=C3=B5es.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../accounts/integrations/hooks_controller.rb | 13 ++ .../integrations/llm_models_controller.rb | 59 ++++++++ app/javascript/dashboard/api/integrations.js | 7 + .../settings/AssistantBasicSettingsForm.vue | 85 ++++++----- .../conversation/ConversationHeader.vue | 5 +- .../conversation/CrmInsightsSidebar.vue | 8 +- .../conversation/crm/FunnelTimeline.vue | 48 +++--- .../dashboard/constants/llmModels.js | 32 ++++ .../i18n/locale/en/integrations.json | 28 +++- .../i18n/locale/pt_BR/integrations.json | 28 +++- .../conversation/ConversationView.vue | 2 +- .../settings/integrations/Integration.vue | 15 +- .../integrations/IntegrationHooks.vue | 12 ++ .../settings/integrations/IntegrationItem.vue | 14 +- .../settings/integrations/LlmModelTester.vue | 141 ++++++++++++++++++ .../integrations/SingleIntegrationHooks.vue | 13 +- app/models/concerns/featurable.rb | 4 +- config/integration/apps.yml | 31 ++++ config/locales/en.yml | 4 + config/locales/pt.yml | 4 + config/locales/pt_BR.yml | 4 + config/routes.rb | 5 + docker/Dockerfile | 2 +- .../accounts/captain/assistants_controller.rb | 2 +- enterprise/app/helpers/captain/chat_helper.rb | 9 +- enterprise/app/models/captain/assistant.rb | 2 +- .../captain/llm/system_prompts_service.rb | 4 +- .../app/services/llm/model_test_service.rb | 36 +++++ .../models/captain/_assistant.json.jbuilder | 3 + lib/chatwoot_hub.rb | 4 +- lib/llm/config.rb | 23 ++- .../images/integrations/gemini-dark.svg | 5 + .../dashboard/images/integrations/gemini.svg | 5 + 33 files changed, 567 insertions(+), 90 deletions(-) create mode 100644 app/controllers/api/v1/accounts/integrations/llm_models_controller.rb create mode 100644 app/javascript/dashboard/constants/llmModels.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/integrations/LlmModelTester.vue create mode 100644 enterprise/app/services/llm/model_test_service.rb create mode 100644 public/dashboard/images/integrations/gemini-dark.svg create mode 100644 public/dashboard/images/integrations/gemini.svg diff --git a/app/controllers/api/v1/accounts/integrations/hooks_controller.rb b/app/controllers/api/v1/accounts/integrations/hooks_controller.rb index 087a9b7..7ee3e18 100755 --- a/app/controllers/api/v1/accounts/integrations/hooks_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/hooks_controller.rb @@ -4,10 +4,12 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base def create @hook = Current.account.hooks.create!(permitted_params) + sync_llm_integration_settings(@hook) end def update @hook.update!(permitted_params.slice(:status, :settings)) + sync_llm_integration_settings(@hook) end def process_event @@ -42,4 +44,15 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base def permitted_params params.require(:hook).permit(:app_id, :inbox_id, :status, settings: {}) end + + def sync_llm_integration_settings(hook) + return unless %w[openai gemini].include?(hook.app_id) + + api_key = hook.settings['api_key'].to_s.strip + return if api_key.blank? + + config_key = hook.app_id == 'gemini' ? 'CAPTAIN_GEMINI_API_KEY' : 'CAPTAIN_OPEN_AI_API_KEY' + InstallationConfig.find_or_initialize_by(name: config_key).update!(value: api_key) + Llm::Config.reset! + end end diff --git a/app/controllers/api/v1/accounts/integrations/llm_models_controller.rb b/app/controllers/api/v1/accounts/integrations/llm_models_controller.rb new file mode 100644 index 0000000..1245c4e --- /dev/null +++ b/app/controllers/api/v1/accounts/integrations/llm_models_controller.rb @@ -0,0 +1,59 @@ +class Api::V1::Accounts::Integrations::LlmModelsController < Api::V1::Accounts::BaseController + before_action :check_authorization + + def test + provider = params.require(:provider) + model = params.require(:model) + return render json: { error: 'Unsupported provider' }, status: :unprocessable_entity unless %w[openai gemini].include?(provider) + + hook = Current.account.hooks.find_by(app_id: provider) + + return render json: { error: 'Integration not configured' }, status: :unprocessable_entity if hook.blank? + + api_key = hook.settings['api_key'].to_s.strip + return render json: { error: 'API key is missing' }, status: :unprocessable_entity if api_key.blank? + + result = Llm::ModelTestService.new( + provider: provider, + model: model, + api_key: api_key + ).perform + + update_hook_results(hook, model, result) + + if result[:success] + render json: { success: true } + else + render json: { error: result[:error] || 'Model test failed' }, status: :unprocessable_entity + end + end + + private + + def check_authorization + authorize(:hook, :update?) + end + + def update_hook_results(hook, model, result) + settings = hook.settings.to_h.deep_stringify_keys + model_tests = settings['model_tests'] || {} + model_tests[model] = { + 'success' => result[:success], + 'error' => result[:error], + 'tested_at' => Time.current.iso8601 + } + + validated_models = model_tests.filter_map do |name, entry| + name if entry['success'] + end + + settings['model_tests'] = model_tests + settings['validated_models'] = validated_models + hook.update!(settings: settings) + rescue ActiveRecord::RecordInvalid + Rails.logger.error( + "[LLM][ModelTest] Failed to persist model test results hook_id=#{hook.id} errors=#{hook.errors.full_messages.join(', ')}" + ) + hook.update_columns(settings: settings) + end +end diff --git a/app/javascript/dashboard/api/integrations.js b/app/javascript/dashboard/api/integrations.js index d4ffcbc..e839b19 100755 --- a/app/javascript/dashboard/api/integrations.js +++ b/app/javascript/dashboard/api/integrations.js @@ -33,6 +33,13 @@ class IntegrationsAPI extends ApiClient { return axios.delete(`${this.baseUrl()}/integrations/hooks/${hookId}`); } + testLlmModel({ provider, model }) { + return axios.post(`${this.baseUrl()}/integrations/llm_models/test`, { + provider, + model, + }); + } + connectShopify({ shopDomain }) { return axios.post(`${this.baseUrl()}/integrations/shopify/auth`, { shop_domain: shopDomain, diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue index e398448..7961c98 100755 --- a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue +++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue @@ -1,8 +1,10 @@