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