iachat/enterprise/app/models/concerns/toolable.rb
2026-05-06 06:24:46 -03:00

135 lines
4.9 KiB
Ruby

module Concerns::Toolable
extend ActiveSupport::Concern
# Isolated namespace for user-defined custom tool classes.
# Keeps them separate from built-in classes in Captain::Tools (e.g., HttpTool, CustomHttpTool).
module CustomTools; end
def tool(assistant, base_class: Captain::Tools::HttpTool, **)
custom_tool_record = self
class_name = custom_tool_record.slug.underscore.camelize
# Always create a fresh class to reflect current metadata
tool_slug = custom_tool_record.slug
tool_class = Class.new(base_class) do
description custom_tool_record.description
# Override name to use the slug directly, avoiding the namespace prefix
# that RubyLLM's default normalization would produce (e.g., "captain--tools--custom_dog_facts").
define_method(:name) { tool_slug }
custom_tool_record.param_schema.each do |param_def|
param param_def['name'].to_sym,
type: param_def['type'],
desc: param_def['description'],
required: param_def.fetch('required', true)
end
end
# Register as a constant so the class gets a proper name (Class#name).
# Anonymous classes return nil for #name, which causes "Invalid 'tools[].function.name':
# empty string" errors from the LLM API. We use CustomTools as the namespace to avoid
# collisions with real classes in Captain::Tools.
CustomTools.send(:remove_const, class_name) if CustomTools.const_defined?(class_name, false)
CustomTools.const_set(class_name, tool_class)
tool_class.new(assistant, self, **)
end
def build_request_url(params)
return endpoint_url if endpoint_url.blank? || endpoint_url.exclude?('{{')
render_template(endpoint_url, params)
end
def build_request_body(params)
return nil if request_template.blank?
render_template(request_template, params)
end
def build_auth_headers
return {} if auth_none?
case auth_type
when 'bearer'
{ 'Authorization' => "Bearer #{auth_config['token']}" }
when 'api_key'
if auth_config['location'] == 'header'
{ auth_config['name'] => auth_config['key'] }
else
{}
end
when 'custom_headers'
# Suporta múltiplos headers configurados pelo usuário via UI.
# Formato esperado em auth_config: { "headers": [{"name": "X-API-ID", "value": "123"}, ...] }
headers = auth_config['headers'] || []
headers.each_with_object({}) do |h, acc|
acc[h['name']] = h['value'] if h['name'].present? && h['value'].present?
end
else
{}
end
end
def build_basic_auth_credentials
return nil unless auth_type == 'basic'
[auth_config['username'], auth_config['password']]
end
def build_metadata_headers(state)
{}.tap do |headers|
add_base_headers(headers, state)
add_conversation_headers(headers, state[:conversation]) if state[:conversation]
add_contact_headers(headers, state[:contact]) if state[:contact]
add_contact_inbox_headers(headers, state[:contact_inbox])
end
end
def add_base_headers(headers, state)
headers['X-Chatwoot-Account-Id'] = state[:account_id].to_s if state[:account_id]
headers['X-Chatwoot-Assistant-Id'] = state[:assistant_id].to_s if state[:assistant_id]
headers['X-Chatwoot-Tool-Slug'] = slug if slug.present?
end
def add_conversation_headers(headers, conversation)
headers['X-Chatwoot-Conversation-Id'] = conversation[:id].to_s if conversation[:id]
headers['X-Chatwoot-Conversation-Display-Id'] = conversation[:display_id].to_s if conversation[:display_id]
end
def add_contact_headers(headers, contact)
headers['X-Chatwoot-Contact-Id'] = contact[:id].to_s if contact[:id]
headers['X-Chatwoot-Contact-Email'] = contact[:email].to_s if contact[:email].present?
headers['X-Chatwoot-Contact-Phone'] = contact[:phone_number].to_s if contact[:phone_number].present?
end
def add_contact_inbox_headers(headers, contact_inbox)
headers['X-Chatwoot-Contact-Inbox-Id'] = contact_inbox[:id].to_s if contact_inbox&.[](:id)
headers['X-Chatwoot-Contact-Inbox-Verified'] = (contact_inbox&.[](:hmac_verified) || false).to_s
end
def format_response(raw_response_body)
return raw_response_body if response_template.blank?
response_data = parse_response_body(raw_response_body)
render_template(response_template, { 'response' => response_data, 'r' => response_data })
end
private
def render_template(template, context)
liquid_template = Liquid::Template.parse(template, error_mode: :strict)
liquid_template.render(context.deep_stringify_keys, registers: {}, strict_variables: true, strict_filters: true)
rescue Liquid::SyntaxError, Liquid::UndefinedVariable, Liquid::UndefinedFilter => e
Rails.logger.error("Liquid template error: #{e.message}")
raise "Template rendering failed: #{e.message}"
end
def parse_response_body(body)
JSON.parse(body)
rescue JSON::ParserError
body
end
end