From c6ef97fc004a1d72652fbe3f55ec23ea3c886421 Mon Sep 17 00:00:00 2001 From: Rodrigo Borba Date: Sat, 3 Jan 2026 19:24:09 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Adiciona=20gerenciamento=20de=20chave?= =?UTF-8?q?=20de=20API=20para=20LLM=20e=20funcionalidades=20de=20webhook?= =?UTF-8?q?=20para=20Wuzapi,=20com=20scripts=20de=20teste=20e=20ajustes=20?= =?UTF-8?q?de=20m=C3=A9todo.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v1/accounts/inboxes/wuzapi_controller.rb | 20 ++++ .../channels/wuzapi/WuzapiConfiguration.vue | 92 ++++++++++++++----- config/routes.rb | 2 + enterprise/app/helpers/captain/chat_helper.rb | 22 ++--- .../app/services/llm/base_ai_service.rb | 6 +- lib/wuzapi/client.rb | 13 ++- local_test_ai.rb | 49 ++++++++++ reproduce_key_error.rb | 39 ++++++++ 8 files changed, 206 insertions(+), 37 deletions(-) mode change 100755 => 100644 enterprise/app/helpers/captain/chat_helper.rb create mode 100644 local_test_ai.rb create mode 100644 reproduce_key_error.rb diff --git a/app/controllers/api/v1/accounts/inboxes/wuzapi_controller.rb b/app/controllers/api/v1/accounts/inboxes/wuzapi_controller.rb index f907223..ec7272b 100644 --- a/app/controllers/api/v1/accounts/inboxes/wuzapi_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes/wuzapi_controller.rb @@ -76,6 +76,26 @@ class Api::V1::Accounts::Inboxes::WuzapiController < Api::V1::Accounts::BaseCont render json: { error: e.message }, status: :internal_server_error end + def webhook_info + info = client.get_webhook(user_token) + render json: info + rescue Wuzapi::Client::Error => e + render json: { error: e.message }, status: :unprocessable_entity + rescue StandardError => e + render json: { error: e.message }, status: :internal_server_error + end + + def update_webhook + # Re-calculate correct webhook URL from model + url = @inbox.channel.webhook_url + client.update_webhook(user_token, url) + render json: { success: true, message: 'Webhook updated successfully', webhook_url: url } + rescue Wuzapi::Client::Error => e + render json: { error: e.message }, status: :unprocessable_entity + rescue StandardError => e + render json: { error: e.message }, status: :internal_server_error + end + private def fetch_inbox diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/wuzapi/WuzapiConfiguration.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/wuzapi/WuzapiConfiguration.vue index fc79f3c..8a273f2 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/wuzapi/WuzapiConfiguration.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/wuzapi/WuzapiConfiguration.vue @@ -25,7 +25,7 @@ export default defineComponent({ // Get accountId reliably from global store (preferred) or inbox prop const accountId = computed(() => { - return store.getters['getCurrentAccountId'] || props.inbox.account_id; + return store.getters.getCurrentAccountId || props.inbox.account_id; }); // Helper for API URL @@ -39,7 +39,6 @@ export default defineComponent({ try { const response = await window.axios.get(getApiUrl('')); - console.log('Status Response:', response.data); const data = response.data; // Wuzapi format: { data: { connected: true, jid: "...", details: "..." } } @@ -58,12 +57,10 @@ export default defineComponent({ statusMessage.value = wuzapiData.details || legacyStatus || 'Unknown'; if (isConnected.value) { - console.log('✅ Wuzapi Connected! JID:', wuzapiData.jid); qrCode.value = ''; stopPolling(); } } catch (error) { - console.error('Status Fetch Error:', error); statusMessage.value = error.response?.data?.error || error.message || 'Check failed'; } @@ -71,9 +68,7 @@ export default defineComponent({ const fetchQrCode = async () => { try { - console.log('Fetching QR code...'); const response = await window.axios.get(getApiUrl('/qr')); - console.log('QR Response Data:', response.data); // Backend now normalizes to 'qrcode' in most cases, but we keep robust checks const d = response.data; @@ -86,13 +81,9 @@ export default defineComponent({ (typeof d.data === 'string' ? d.data : null); if (qrcodeData && qrcodeData.length > 20) { - console.log('QR Code found, updating UI...'); qrCode.value = qrcodeData; startPolling(); } else { - console.warn( - 'No QR code in response. Checking status as fallback...' - ); // Fallback: maybe we are already connected? await fetchStatus(); if (!isConnected.value) { @@ -100,16 +91,13 @@ export default defineComponent({ } } } catch (error) { - console.error('QR Fetch Error:', error); statusMessage.value = error.response?.data?.error || 'Failed to load QR'; } }; const handleConnect = async () => { - console.log('Connect button clicked'); if (!accountId.value) { - console.error('Account ID missing'); useAlert('Error: Account ID missing'); return; } @@ -118,13 +106,10 @@ export default defineComponent({ try { // 1. Call Connect const connectUrl = getApiUrl('/connect'); - console.log('Calling connect:', connectUrl); await window.axios.post(connectUrl); - console.log('Connect successful, fetching QR...'); // 2. Fetch QR await fetchQrCode(); } catch (error) { - console.error('Connect failed:', error); useAlert(error.response?.data?.error || 'Connection failed'); } finally { isLoading.value = false; @@ -146,7 +131,15 @@ export default defineComponent({ } }; - const startPolling = () => { +// Function hoisting allows use before definition + function stopPolling() { + if (pollInterval) { + clearInterval(pollInterval); + pollInterval = null; + } + } + + function startPolling() { if (pollInterval) return; // Poll every 5 seconds to check status AND refresh QR code pollInterval = setInterval(async () => { @@ -156,12 +149,37 @@ export default defineComponent({ await fetchQrCode(); } }, 5000); + } + + const isLoadingWebhook = ref(false); + const webhookInfo = ref(null); + + const fetchWebhookInfo = async () => { + isLoadingWebhook.value = true; + try { + const response = await window.axios.get(getApiUrl('/webhook_info')); + webhookInfo.value = response.data; + useAlert('Webhook info fetched successfully'); + } catch (error) { + useAlert(error.response?.data?.error || 'Failed to fetch webhook info'); + } finally { + isLoadingWebhook.value = false; + } }; - const stopPolling = () => { - if (pollInterval) { - clearInterval(pollInterval); - pollInterval = null; + const updateWebhook = async () => { + isLoadingWebhook.value = true; + try { + const response = await window.axios.put(getApiUrl('/update_webhook')); + webhookInfo.value = { + message: response.data.message, + url: response.data.webhook_url + }; + useAlert('Webhook updated successfully'); + } catch (error) { + useAlert(error.response?.data?.error || 'Failed to update webhook'); + } finally { + isLoadingWebhook.value = false; } }; @@ -182,6 +200,10 @@ export default defineComponent({ disconnect, handleConnect, accountId, + isLoadingWebhook, + webhookInfo, + fetchWebhookInfo, + updateWebhook, }; }, }); @@ -198,7 +220,7 @@ export default defineComponent({
- + {{ $t('INBOX_MGMT.EDIT.WUZAPI.CONNECTED') }}

@@ -250,6 +272,32 @@ export default defineComponent({

Error: Account ID not loaded. Please refresh the page.
+
+

+ Webhook Configuration +

+
+ + +
+ +
+
{{ JSON.stringify(webhookInfo, null, 2) }}
+
+
diff --git a/config/routes.rb b/config/routes.rb index 874016b..66170a4 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -236,6 +236,8 @@ Rails.application.routes.draw do get :qr post :connect post :disconnect + get :webhook_info + put :update_webhook end end diff --git a/enterprise/app/helpers/captain/chat_helper.rb b/enterprise/app/helpers/captain/chat_helper.rb old mode 100755 new mode 100644 index 9bbbd86..ef2ec2a --- a/enterprise/app/helpers/captain/chat_helper.rb +++ b/enterprise/app/helpers/captain/chat_helper.rb @@ -20,7 +20,7 @@ module Captain::ChatHelper private def build_chat - llm_chat = chat(model: @model, temperature: temperature) + llm_chat = chat(model: @model, temperature: temperature, api_key: api_key) llm_chat = llm_chat.with_params(response_format: { type: 'json_object' }) llm_chat = setup_tools(llm_chat) @@ -37,7 +37,9 @@ module Captain::ChatHelper def setup_system_instructions(chat) system_messages = @messages.select { |m| m[:role] == 'system' || m[:role] == :system } - combined_instructions = system_messages.pluck(:content).join("\n\n") + combined_instructions = system_messages.pluck(:content).join(" + +") chat.with_instructions(combined_instructions) end @@ -95,24 +97,20 @@ module Captain::ChatHelper @account&.id || @assistant&.account_id end - # Ensures all LLM calls and tool executions within an agentic loop - # are grouped under a single trace/session in Langfuse. - # - # Without this guard, each recursive call to request_chat_completion - # (triggered by tool calls) would create a separate trace instead of - # nesting within the existing session span. - def with_agent_session(&) + def api_key + @assistant&.config&.[]('openai_api_key').presence || ENV.fetch('OPENAI_API_KEY', nil) || ENV.fetch('GEMINI_API_KEY', nil) + end + + def with_agent_session(&block) already_active = @agent_session_active return yield if already_active @agent_session_active = true - instrument_agent_session(instrumentation_params, &) + instrument_agent_session(instrumentation_params, &block) ensure @agent_session_active = false unless already_active end - # Must be implemented by including class to identify the feature for instrumentation. - # Used for Langfuse tagging and span naming. def feature_name raise NotImplementedError, "#{self.class.name} must implement #feature_name" end diff --git a/enterprise/app/services/llm/base_ai_service.rb b/enterprise/app/services/llm/base_ai_service.rb index fa044a1..6541ef4 100755 --- a/enterprise/app/services/llm/base_ai_service.rb +++ b/enterprise/app/services/llm/base_ai_service.rb @@ -14,8 +14,10 @@ class Llm::BaseAiService setup_temperature end - def chat(model: @model, temperature: @temperature) - RubyLLM.chat(model: model).with_temperature(temperature) + def chat(model: @model, temperature: @temperature, api_key: nil) + client = RubyLLM.chat(model: model) + client = client.with_api_key(api_key) if api_key.present? + client.with_temperature(temperature) end private diff --git a/lib/wuzapi/client.rb b/lib/wuzapi/client.rb index 6985e95..e0144e4 100644 --- a/lib/wuzapi/client.rb +++ b/lib/wuzapi/client.rb @@ -53,7 +53,7 @@ module Wuzapi end def session_disconnect(user_token) - request(:get, '/session/disconnect', nil, user_auth_headers(user_token)) + request(:post, '/session/disconnect', nil, user_auth_headers(user_token)) end def session_logout(user_token) @@ -66,6 +66,15 @@ module Wuzapi request(:post, '/webhook', payload, user_auth_headers(user_token)) end + def update_webhook(user_token, webhook_url) + payload = { 'WebhookURL' => webhook_url, 'Events' => ['All'] } + request(:put, '/webhook', payload, user_auth_headers(user_token)) + end + + def get_webhook(user_token) + request(:get, '/webhook', nil, user_auth_headers(user_token)) + end + private def normalize_url(url) @@ -94,6 +103,8 @@ module Wuzapi Net::HTTP::Get.new(uri.request_uri) when :post Net::HTTP::Post.new(uri.request_uri) + when :put + Net::HTTP::Put.new(uri.request_uri) when :delete Net::HTTP::Delete.new(uri.request_uri) end diff --git a/local_test_ai.rb b/local_test_ai.rb new file mode 100644 index 0000000..bd43741 --- /dev/null +++ b/local_test_ai.rb @@ -0,0 +1,49 @@ +# local_test_ai.rb + +# 1. Check Env Vars +puts "\n--- [DIAGNÓSTICO IA / JASMINE] ---" +puts 'Checking Environment Variables...' +openai_key = ENV.fetch('OPENAI_API_KEY', nil) +gemini_key = ENV.fetch('GEMINI_API_KEY', nil) + +if openai_key.present? + puts "✅ OPENAI_API_KEY found: #{openai_key[0..5]}...#{openai_key[-4..-1]}" +else + puts '❌ OPENAI_API_KEY NOT found' +end + +if gemini_key.present? + puts "✅ GEMINI_API_KEY found: #{gemini_key[0..5]}...#{gemini_key[-4..-1]}" +else + puts '⚠️ GEMINI_API_KEY NOT found (Optional if using OpenAI)' +end + +# 2. Check RubyLLM Config +puts "\nChecking RubyLLM Configuration..." +begin + # Force re-configure just to be sure we are using the env vars + RubyLLM.configure do |config| + config.openai_api_key = openai_key if openai_key.present? + config.gemini_api_key = gemini_key if gemini_key.present? + end + puts '✅ RubyLLM Configured' +rescue StandardError => e + puts "❌ RubyLLM Configuration Error: #{e.message}" +end + +# 3. Test Call +puts "\nTesting Simple LLM Call (Hello World)..." +begin + # Try to use GPT-4o-mini or fallback to gpt-3.5-turbo or gemini + model = openai_key.present? ? 'gpt-4o-mini' : 'gemini-pro' + puts "Using model: #{model}" + + client = RubyLLM.chat(model: model) + response = client.ask("Responda apenas com: 'IA Funcionando!'") + + puts "\n>>> RESPOSTA DA IA: #{response.content}" + puts '✅ CONEXÃO BEM SUCEDIDA!' +rescue StandardError => e + puts "\n❌ ERRO NA CHAMADA DA IA: #{e.message}" + puts e.backtrace.first(5) +end diff --git a/reproduce_key_error.rb b/reproduce_key_error.rb new file mode 100644 index 0000000..f54df33 --- /dev/null +++ b/reproduce_key_error.rb @@ -0,0 +1,39 @@ +# reproduce_key_error.rb +begin + puts "Testing RubyLLM.chat(api_key: 'test') arguments..." + + # Attempt 1: Check signature if possible (inspection) + # puts RubyLLM.method(:chat).parameters + + # Attempt 2: Try to call with explicit key (even if dummy) + # If it raises ArgumentError, it's not supported. + # If it raises ConfigurationError (missing key) despite passing one, it's ignored. + + puts "1. Calling RubyLLM.chat(model: 'gpt-3.5-turbo', api_key: 'sk-test-123')" + begin + client = RubyLLM.chat(model: 'gpt-3.5-turbo', api_key: 'sk-test-123') + puts '✅ RubyLLM.chat accepted api_key argument!' + puts "Client class: #{client.class}" + rescue ArgumentError => e + puts "❌ RubyLLM.chat raised ArgumentError: #{e.message}" + rescue StandardError => e + puts "⚠️ RubyLLM.chat raised #{e.class}: #{e.message}" + end + + puts "\n2. Calling RubyLLM.chat(model: 'gpt-3.5-turbo').with_api_key('sk-test-123') (Guessing method)" + begin + client = RubyLLM.chat(model: 'gpt-3.5-turbo') + if client.respond_to?(:with_api_key) + client.with_api_key('sk-test-123') + puts '✅ Client supports .with_api_key' + else + puts '❌ Client usually does NOT support .with_api_key (respond_to? false)' + end + rescue StandardError => e + puts "⚠️ Error in attempt 2: #{e.message}" + end + +rescue StandardError => e + puts "FATAL: #{e.message}" + puts e.backtrace +end