feat: Adiciona gerenciamento de chave de API para LLM e funcionalidades de webhook para Wuzapi, com scripts de teste e ajustes de método.
This commit is contained in:
parent
ad308c4f11
commit
c6ef97fc00
@ -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
|
||||
|
||||
@ -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({
|
||||
<div v-if="accountId" class="flex flex-col items-center">
|
||||
<div v-if="isConnected" class="flex flex-col items-center">
|
||||
<div class="text-green-600 font-bold mb-4 flex items-center gap-2">
|
||||
<span class="i-woot-checkmark-circle text-2xl"></span>
|
||||
<span class="i-woot-checkmark-circle text-2xl" />
|
||||
{{ $t('INBOX_MGMT.EDIT.WUZAPI.CONNECTED') }}
|
||||
</div>
|
||||
<p class="text-n-slate-11 mb-4">
|
||||
@ -250,6 +272,32 @@ export default defineComponent({
|
||||
<div v-else class="text-red-600 p-4">
|
||||
Error: Account ID not loaded. Please refresh the page.
|
||||
</div>
|
||||
<div class="mt-8 pt-6 border-t border-n-weak w-full">
|
||||
<h4 class="text-md font-medium text-n-slate-12 mb-4">
|
||||
Webhook Configuration
|
||||
</h4>
|
||||
<div class="flex gap-4 mb-4">
|
||||
<NextButton
|
||||
icon="i-woot-refresh"
|
||||
:is-loading="isLoadingWebhook"
|
||||
label="Get Webhook Info"
|
||||
@click="fetchWebhookInfo"
|
||||
/>
|
||||
<NextButton
|
||||
icon="i-woot-upload"
|
||||
:is-loading="isLoadingWebhook"
|
||||
label="Update Webhook Connection"
|
||||
@click="updateWebhook"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="webhookInfo"
|
||||
class="bg-n-alpha-1 p-4 rounded text-sm font-mono overflow-auto"
|
||||
>
|
||||
<pre>{{ JSON.stringify(webhookInfo, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -236,6 +236,8 @@ Rails.application.routes.draw do
|
||||
get :qr
|
||||
post :connect
|
||||
post :disconnect
|
||||
get :webhook_info
|
||||
put :update_webhook
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
22
enterprise/app/helpers/captain/chat_helper.rb
Executable file → Normal file
22
enterprise/app/helpers/captain/chat_helper.rb
Executable file → Normal file
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
49
local_test_ai.rb
Normal file
49
local_test_ai.rb
Normal file
@ -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
|
||||
39
reproduce_key_error.rb
Normal file
39
reproduce_key_error.rb
Normal file
@ -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
|
||||
Loading…
Reference in New Issue
Block a user