refactor: Otimiza a inicialização de atributos em MessageBuilder e simplifica a declaração de módulos em diversos controladores Captain, além de atualizar a configuração de exceções para testes e o Bundler.
This commit is contained in:
parent
c76c8d5c50
commit
b80d35a307
@ -1183,4 +1183,4 @@ RUBY VERSION
|
|||||||
ruby 3.4.4p34
|
ruby 3.4.4p34
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
4.0.4
|
2.5.5
|
||||||
|
|||||||
@ -6,19 +6,13 @@ class Messages::MessageBuilder
|
|||||||
attr_reader :message
|
attr_reader :message
|
||||||
|
|
||||||
def initialize(user, conversation, params)
|
def initialize(user, conversation, params)
|
||||||
@params = params
|
|
||||||
@private = params[:private] || false
|
|
||||||
@conversation = conversation
|
|
||||||
@user = user
|
@user = user
|
||||||
|
@conversation = conversation
|
||||||
@account = conversation.account
|
@account = conversation.account
|
||||||
@message_type = params[:message_type] || 'outgoing'
|
@params = params
|
||||||
@attachments = params[:attachments]
|
|
||||||
@automation_rule = content_attributes&.dig(:automation_rule_id)
|
|
||||||
return unless params.instance_of?(ActionController::Parameters)
|
return unless params.instance_of?(ActionController::Parameters)
|
||||||
|
|
||||||
# Try to find in_reply_to in params (top level) or content_attributes
|
init_message_attributes
|
||||||
@in_reply_to = params[:in_reply_to_id] || params[:in_reply_to] || content_attributes&.dig(:in_reply_to)
|
|
||||||
@items = content_attributes&.dig(:items)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
@ -223,6 +217,16 @@ class Messages::MessageBuilder
|
|||||||
'agent' => UserDrop.new(sender)
|
'agent' => UserDrop.new(sender)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def init_message_attributes
|
||||||
|
@private = @params[:private] || false
|
||||||
|
@message_type = @params[:message_type] || 'outgoing'
|
||||||
|
@attachments = @params[:attachments]
|
||||||
|
@automation_rule = content_attributes&.dig(:automation_rule_id)
|
||||||
|
# Try to find in_reply_to in params (top level) or content_attributes
|
||||||
|
@in_reply_to = @params[:in_reply_to_id] || @params[:in_reply_to] || content_attributes&.dig(:in_reply_to)
|
||||||
|
@items = content_attributes&.dig(:items)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Messages::MessageBuilder.prepend_mod_with('Messages::MessageBuilder')
|
Messages::MessageBuilder.prepend_mod_with('Messages::MessageBuilder')
|
||||||
|
|||||||
@ -1,72 +1,64 @@
|
|||||||
module Api
|
class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::BaseController
|
||||||
module V1
|
before_action :fetch_assistant, only: [:show, :update, :destroy, :playground, :test_webhook]
|
||||||
module Accounts
|
|
||||||
module Captain
|
|
||||||
class AssistantsController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :fetch_assistant, only: [:show, :update, :destroy, :playground, :test_webhook]
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@assistants = current_account.captain_assistants.order(created_at: :desc)
|
@assistants = current_account.captain_assistants.order(created_at: :desc)
|
||||||
render json: @assistants
|
render json: @assistants
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: @assistant
|
render json: @assistant
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@assistant = current_account.captain_assistants.new(assistant_params)
|
@assistant = current_account.captain_assistants.new(assistant_params)
|
||||||
if @assistant.save
|
if @assistant.save
|
||||||
render json: @assistant
|
render json: @assistant
|
||||||
else
|
else
|
||||||
render_error_response(@assistant)
|
render_error_response(@assistant)
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
if @assistant.update(assistant_params)
|
|
||||||
render json: @assistant
|
|
||||||
else
|
|
||||||
render_error_response(@assistant)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
@assistant.destroy
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
def playground
|
|
||||||
# TODO: Implement playground logic
|
|
||||||
render json: { message: 'Playground not implemented yet' }, status: :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_webhook
|
|
||||||
# TODO: Implement webhook test logic
|
|
||||||
render json: { message: 'Webhook test not implemented yet' }, status: :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def fetch_assistant
|
|
||||||
@assistant = current_account.captain_assistants.find(params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def assistant_params
|
|
||||||
params.require(:assistant).permit(
|
|
||||||
:name,
|
|
||||||
:description,
|
|
||||||
:llm_provider,
|
|
||||||
:llm_model,
|
|
||||||
:api_key,
|
|
||||||
config: {},
|
|
||||||
response_guidelines: [],
|
|
||||||
guardrails: [],
|
|
||||||
handoff_webhook_config: {}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @assistant.update(assistant_params)
|
||||||
|
render json: @assistant
|
||||||
|
else
|
||||||
|
render_error_response(@assistant)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@assistant.destroy
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def playground
|
||||||
|
# TODO: Implement playground logic
|
||||||
|
render json: { message: 'Playground not implemented yet' }, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_webhook
|
||||||
|
# TODO: Implement webhook test logic
|
||||||
|
render json: { message: 'Webhook test not implemented yet' }, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_assistant
|
||||||
|
@assistant = current_account.captain_assistants.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def assistant_params
|
||||||
|
params.require(:assistant).permit(
|
||||||
|
:name,
|
||||||
|
:description,
|
||||||
|
:llm_provider,
|
||||||
|
:llm_model,
|
||||||
|
:api_key,
|
||||||
|
config: {},
|
||||||
|
response_guidelines: [],
|
||||||
|
guardrails: [],
|
||||||
|
handoff_webhook_config: {}
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -11,28 +11,12 @@ class Api::V1::Accounts::Captain::PricingsController < Api::V1::Accounts::BaseCo
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
# Handle multiple inboxes (cloning/distributing the price)
|
|
||||||
inbox_ids_param = params[:pricing][:inbox_ids]
|
inbox_ids_param = params[:pricing][:inbox_ids]
|
||||||
|
|
||||||
if inbox_ids_param.present? && inbox_ids_param.is_a?(Array)
|
if inbox_ids_param.is_a?(Array) && inbox_ids_param.present?
|
||||||
last_pricing = nil
|
render json: create_for_multiple_inboxes(inbox_ids_param)
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
inbox_ids_param.each do |iid|
|
|
||||||
# Create for each inbox. Merge overwrites default params.
|
|
||||||
# Convert iid to integer just in case
|
|
||||||
pricing = current_account.captain_pricings.new(pricing_params)
|
|
||||||
pricing.inbox_id = iid.to_i
|
|
||||||
pricing.save!
|
|
||||||
last_pricing = pricing
|
|
||||||
end
|
|
||||||
end
|
|
||||||
render json: last_pricing
|
|
||||||
else
|
else
|
||||||
# Create Global (inbox_id: nil) if no specific inbox selected
|
render json: create_single_pricing
|
||||||
@pricing = current_account.captain_pricings.new(pricing_params)
|
|
||||||
@pricing.inbox_id = nil
|
|
||||||
@pricing.save!
|
|
||||||
render json: @pricing
|
|
||||||
end
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error "Error creating pricing: #{e.message}"
|
Rails.logger.error "Error creating pricing: #{e.message}"
|
||||||
@ -69,7 +53,7 @@ class Api::V1::Accounts::Captain::PricingsController < Api::V1::Accounts::BaseCo
|
|||||||
# Filter by inbox if provided (returns Specific Inbox + Global rules)
|
# Filter by inbox if provided (returns Specific Inbox + Global rules)
|
||||||
@pricings = @pricings.where(inbox_id: [params[:inbox_id], nil]) if params[:inbox_id].present?
|
@pricings = @pricings.where(inbox_id: [params[:inbox_id], nil]) if params[:inbox_id].present?
|
||||||
|
|
||||||
return unless params[:query].present?
|
return if params[:query].blank?
|
||||||
|
|
||||||
# Fuzzy search using ILIKE for case-insensitive matching
|
# Fuzzy search using ILIKE for case-insensitive matching
|
||||||
@pricings = @pricings.left_outer_joins(:captain_brand).where(
|
@pricings = @pricings.left_outer_joins(:captain_brand).where(
|
||||||
@ -81,4 +65,24 @@ class Api::V1::Accounts::Captain::PricingsController < Api::V1::Accounts::BaseCo
|
|||||||
def pricing_params
|
def pricing_params
|
||||||
params.require(:pricing).permit(:captain_brand_id, :day_range, :suite_category, :duration, :price)
|
params.require(:pricing).permit(:captain_brand_id, :day_range, :suite_category, :duration, :price)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_for_multiple_inboxes(inbox_ids)
|
||||||
|
last_pricing = nil
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
inbox_ids.each do |iid|
|
||||||
|
pricing = current_account.captain_pricings.new(pricing_params)
|
||||||
|
pricing.inbox_id = iid.to_i
|
||||||
|
pricing.save!
|
||||||
|
last_pricing = pricing
|
||||||
|
end
|
||||||
|
end
|
||||||
|
last_pricing
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_single_pricing
|
||||||
|
@pricing = current_account.captain_pricings.new(pricing_params)
|
||||||
|
@pricing.inbox_id = nil
|
||||||
|
@pricing.save!
|
||||||
|
@pricing
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,71 +1,63 @@
|
|||||||
module Api
|
class Api::V1::Accounts::Captain::ScenariosController < Api::V1::Accounts::BaseController
|
||||||
module V1
|
before_action :fetch_assistant
|
||||||
module Accounts
|
before_action :fetch_scenario, only: [:show, :update, :destroy]
|
||||||
module Captain
|
|
||||||
class ScenariosController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :fetch_assistant
|
|
||||||
before_action :fetch_scenario, only: [:show, :update, :destroy]
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@scenarios = @assistant.captain_scenarios.order(created_at: :desc)
|
@scenarios = @assistant.captain_scenarios.order(created_at: :desc)
|
||||||
render json: @scenarios
|
render json: @scenarios
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: @scenario
|
render json: @scenario
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@scenario = @assistant.captain_scenarios.new(scenario_params)
|
@scenario = @assistant.captain_scenarios.new(scenario_params)
|
||||||
@scenario.account = current_account
|
@scenario.account = current_account
|
||||||
if @scenario.save
|
if @scenario.save
|
||||||
render json: @scenario
|
render json: @scenario
|
||||||
else
|
else
|
||||||
render_error_response(@scenario)
|
render_error_response(@scenario)
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
if @scenario.update(scenario_params)
|
|
||||||
render json: @scenario
|
|
||||||
else
|
|
||||||
render_error_response(@scenario)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
@scenario.destroy
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
def suggest_triggers
|
|
||||||
# TODO: Implement AI suggestion logic
|
|
||||||
# For now, return a dummy list based on title/instruction if possible, or empty
|
|
||||||
render json: { keywords: 'keyword1, keyword2' }
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def fetch_assistant
|
|
||||||
@assistant = current_account.captain_assistants.find(params[:assistant_id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_scenario
|
|
||||||
@scenario = @assistant.captain_scenarios.find(params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def scenario_params
|
|
||||||
params.permit(
|
|
||||||
:title,
|
|
||||||
:description,
|
|
||||||
:instruction,
|
|
||||||
:trigger_keywords,
|
|
||||||
:enabled,
|
|
||||||
tools: []
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @scenario.update(scenario_params)
|
||||||
|
render json: @scenario
|
||||||
|
else
|
||||||
|
render_error_response(@scenario)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@scenario.destroy
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def suggest_triggers
|
||||||
|
# TODO: Implement AI suggestion logic
|
||||||
|
# For now, return a dummy list based on title/instruction if possible, or empty
|
||||||
|
render json: { keywords: 'keyword1, keyword2' }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_assistant
|
||||||
|
@assistant = current_account.captain_assistants.find(params[:assistant_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_scenario
|
||||||
|
@scenario = @assistant.captain_scenarios.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def scenario_params
|
||||||
|
params.permit(
|
||||||
|
:title,
|
||||||
|
:description,
|
||||||
|
:instruction,
|
||||||
|
:trigger_keywords,
|
||||||
|
:enabled,
|
||||||
|
tools: []
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,73 +1,65 @@
|
|||||||
module Api
|
class Api::V1::Accounts::Captain::ToolsController < Api::V1::Accounts::BaseController
|
||||||
module V1
|
before_action :fetch_assistant
|
||||||
module Accounts
|
|
||||||
module Captain
|
|
||||||
class ToolsController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :fetch_assistant
|
|
||||||
|
|
||||||
NATIVE_TOOLS = [
|
NATIVE_TOOLS = [
|
||||||
{ key: 'react_to_message', name: 'React to Message', description: 'Reage a mensagens do usuário com emojis adequados.' },
|
{ key: 'react_to_message', name: 'React to Message', description: 'Reage a mensagens do usuário com emojis adequados.' },
|
||||||
{ key: 'check_availability', name: 'Check Availability', description: 'Verifica a disponibilidade de quartos e datas.' },
|
{ key: 'check_availability', name: 'Check Availability', description: 'Verifica a disponibilidade de quartos e datas.' },
|
||||||
{ key: 'update_contact', name: 'Update Contact', description: 'Atualiza informações do contato (nome, email, telefone).' },
|
{ key: 'update_contact', name: 'Update Contact', description: 'Atualiza informações do contato (nome, email, telefone).' },
|
||||||
{ key: 'create_reservation_intent', name: 'Create Reservation Intent', description: 'Cria uma intenção de reserva e calcula valores.' },
|
{ key: 'create_reservation_intent', name: 'Create Reservation Intent', description: 'Cria uma intenção de reserva e calcula valores.' },
|
||||||
{ key: 'generate_pix', name: 'Generate Pix', description: 'Gera código Pix Copy & Paste e QR Code.' },
|
{ key: 'generate_pix', name: 'Generate Pix', description: 'Gera código Pix Copy & Paste e QR Code.' },
|
||||||
{ key: 'list_reservations', name: 'List Reservations', description: 'Lista reservas anteriores do cliente.' },
|
{ key: 'list_reservations', name: 'List Reservations', description: 'Lista reservas anteriores do cliente.' },
|
||||||
{ key: 'status_suites', name: 'Status Suites', description: 'Verifica o status atual de ocupação das suítes.' },
|
{ key: 'status_suites', name: 'Status Suites', description: 'Verifica o status atual de ocupação das suítes.' },
|
||||||
{ key: 'suite_watchdog', name: 'Suite Watchdog', description: 'Monitoramento automático de status de suítes.' }
|
{ key: 'suite_watchdog', name: 'Suite Watchdog', description: 'Monitoramento automático de status de suítes.' }
|
||||||
]
|
].freeze
|
||||||
|
|
||||||
def index
|
def index
|
||||||
tools = NATIVE_TOOLS.map do |tool|
|
tools = NATIVE_TOOLS.map do |tool|
|
||||||
config = @assistant.captain_tool_configs.find_by(tool_key: tool[:key])
|
config = @assistant.captain_tool_configs.find_by(tool_key: tool[:key])
|
||||||
tool.merge(
|
tool.merge(
|
||||||
enabled: config&.is_enabled.nil? || config.is_enabled,
|
enabled: config&.is_enabled.nil? || config.is_enabled,
|
||||||
webhook_url: config&.webhook_url,
|
webhook_url: config&.webhook_url,
|
||||||
plug_play_id: config&.plug_play_id,
|
plug_play_id: config&.plug_play_id,
|
||||||
plug_play_token: config&.plug_play_token,
|
plug_play_token: config&.plug_play_token,
|
||||||
fallback_message: config&.fallback_message
|
fallback_message: config&.fallback_message
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
render json: tools
|
render json: tools
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
tool_key = params[:id]
|
tool_key = params[:id]
|
||||||
config = @assistant.captain_tool_configs.find_or_initialize_by(tool_key: tool_key)
|
config = @assistant.captain_tool_configs.find_or_initialize_by(tool_key: tool_key)
|
||||||
|
|
||||||
# Ensure context unique constraint is respected
|
# Ensure context unique constraint is respected
|
||||||
config.account = current_account
|
config.account = current_account
|
||||||
|
|
||||||
# Map 'enabled' from frontend to 'is_enabled' in DB
|
# Map 'enabled' from frontend to 'is_enabled' in DB
|
||||||
update_params = tool_params
|
update_params = tool_params
|
||||||
update_params[:is_enabled] = update_params.delete(:enabled) if update_params.key?(:enabled)
|
update_params[:is_enabled] = update_params.delete(:enabled) if update_params.key?(:enabled)
|
||||||
|
|
||||||
config.assign_attributes(update_params)
|
config.assign_attributes(update_params)
|
||||||
|
|
||||||
if config.save
|
if config.save
|
||||||
render json: config
|
render json: config
|
||||||
else
|
else
|
||||||
render_error_response(config)
|
render_error_response(config)
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def fetch_assistant
|
|
||||||
@assistant = current_account.captain_assistants.find(params[:assistant_id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def tool_params
|
|
||||||
params.require(:tool).permit(
|
|
||||||
:enabled,
|
|
||||||
:is_enabled,
|
|
||||||
:webhook_url,
|
|
||||||
:plug_play_id,
|
|
||||||
:plug_play_token,
|
|
||||||
:fallback_message
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_assistant
|
||||||
|
@assistant = current_account.captain_assistants.find(params[:assistant_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def tool_params
|
||||||
|
params.require(:tool).permit(
|
||||||
|
:enabled,
|
||||||
|
:is_enabled,
|
||||||
|
:webhook_url,
|
||||||
|
:plug_play_id,
|
||||||
|
:plug_play_token,
|
||||||
|
:fallback_message
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,21 +1,11 @@
|
|||||||
module Api
|
class Api::V1::Accounts::Captain::Units::ReservationsSyncController < Api::V1::Accounts::BaseController
|
||||||
module V1
|
def create
|
||||||
module Accounts
|
unit = Current.account.captain_units.find(params[:unit_id])
|
||||||
module Captain
|
::Captain::Reservations::SyncService.new(unit).perform
|
||||||
module Units
|
head :ok
|
||||||
class ReservationsSyncController < Api::V1::Accounts::BaseController
|
rescue ActiveRecord::RecordNotFound
|
||||||
def create
|
render_not_found_error('Unit not found')
|
||||||
unit = Current.account.captain_units.find(params[:unit_id])
|
rescue StandardError => e
|
||||||
::Captain::Reservations::SyncService.new(unit).perform
|
render_error(e.message)
|
||||||
head :ok
|
|
||||||
rescue ActiveRecord::RecordNotFound
|
|
||||||
render_not_found_error('Unit not found')
|
|
||||||
rescue StandardError => e
|
|
||||||
render_error(e.message)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,65 +1,57 @@
|
|||||||
module Api
|
class Api::V1::Accounts::Captain::UnitsController < Api::V1::Accounts::BaseController
|
||||||
module V1
|
def index
|
||||||
module Accounts
|
@units = Current.account.captain_units
|
||||||
module Captain
|
end
|
||||||
class UnitsController < Api::V1::Accounts::BaseController
|
|
||||||
def index
|
|
||||||
@units = Current.account.captain_units
|
|
||||||
end
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@unit = Current.account.captain_units.find(params[:id])
|
@unit = Current.account.captain_units.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@unit = Current.account.captain_units.new(unit_params)
|
@unit = Current.account.captain_units.new(unit_params)
|
||||||
@unit.captain_brand = Current.account.captain_brands.first # Default brand logic for now
|
@unit.captain_brand = Current.account.captain_brands.first # Default brand logic for now
|
||||||
|
|
||||||
if @unit.save
|
if @unit.save
|
||||||
render 'show', status: :created
|
render 'show', status: :created
|
||||||
else
|
else
|
||||||
render_error_response(@unit)
|
render_error_response(@unit)
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
|
||||||
@unit = Current.account.captain_units.find(params[:id])
|
|
||||||
|
|
||||||
if @unit.update(unit_params)
|
|
||||||
render 'show'
|
|
||||||
else
|
|
||||||
render_error_response(@unit)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
@unit = Current.account.captain_units.find(params[:id])
|
|
||||||
@unit.destroy
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def unit_params
|
|
||||||
params.require(:unit).permit(
|
|
||||||
:name,
|
|
||||||
:status,
|
|
||||||
:reservations_sync_enabled,
|
|
||||||
:plug_play_id,
|
|
||||||
:plug_play_token,
|
|
||||||
:webhook_url,
|
|
||||||
:leader_whatsapp,
|
|
||||||
:reservation_source_tag,
|
|
||||||
:inter_client_id,
|
|
||||||
:inter_client_secret,
|
|
||||||
:inter_pix_key,
|
|
||||||
:inter_account_number,
|
|
||||||
visible_suite_categories: [],
|
|
||||||
suite_category_images: {}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@unit = Current.account.captain_units.find(params[:id])
|
||||||
|
|
||||||
|
if @unit.update(unit_params)
|
||||||
|
render 'show'
|
||||||
|
else
|
||||||
|
render_error_response(@unit)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@unit = Current.account.captain_units.find(params[:id])
|
||||||
|
@unit.destroy
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def unit_params
|
||||||
|
params.require(:unit).permit(
|
||||||
|
:name,
|
||||||
|
:status,
|
||||||
|
:reservations_sync_enabled,
|
||||||
|
:plug_play_id,
|
||||||
|
:plug_play_token,
|
||||||
|
:webhook_url,
|
||||||
|
:leader_whatsapp,
|
||||||
|
:reservation_source_tag,
|
||||||
|
:inter_client_id,
|
||||||
|
:inter_client_secret,
|
||||||
|
:inter_pix_key,
|
||||||
|
:inter_account_number,
|
||||||
|
visible_suite_categories: [],
|
||||||
|
suite_category_images: {}
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -19,25 +19,7 @@ class Api::V1::Accounts::Conversations::CrmInsightsController < Api::V1::Account
|
|||||||
def serialize_insight(insight)
|
def serialize_insight(insight)
|
||||||
return nil if insight.blank?
|
return nil if insight.blank?
|
||||||
|
|
||||||
{
|
insight_attributes(insight)
|
||||||
id: insight.id,
|
|
||||||
conversation_id: insight.conversation_id,
|
|
||||||
account_id: insight.account_id,
|
|
||||||
contact_id: insight.contact_id,
|
|
||||||
summary_text: insight.summary_text,
|
|
||||||
structured_data: insight.structured_data,
|
|
||||||
contact_sessions_count: insight.contact_sessions_count,
|
|
||||||
last_contact_at: insight.last_contact_at,
|
|
||||||
updated_at: insight.updated_at,
|
|
||||||
generated_at: insight.generated_at,
|
|
||||||
range_from_message_id: insight.range_from_message_id,
|
|
||||||
range_to_message_id: insight.range_to_message_id,
|
|
||||||
status: insight.status,
|
|
||||||
error_message: insight.error_message,
|
|
||||||
schema_version: insight.schema_version,
|
|
||||||
model: insight.model,
|
|
||||||
confidence: insight.confidence
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def insights_payload
|
def insights_payload
|
||||||
@ -68,4 +50,26 @@ class Api::V1::Accounts::Conversations::CrmInsightsController < Api::V1::Account
|
|||||||
|
|
||||||
meta
|
meta
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def insight_attributes(insight)
|
||||||
|
{
|
||||||
|
id: insight.id,
|
||||||
|
conversation_id: insight.conversation_id,
|
||||||
|
account_id: insight.account_id,
|
||||||
|
contact_id: insight.contact_id,
|
||||||
|
summary_text: insight.summary_text,
|
||||||
|
structured_data: insight.structured_data,
|
||||||
|
contact_sessions_count: insight.contact_sessions_count,
|
||||||
|
last_contact_at: insight.last_contact_at,
|
||||||
|
updated_at: insight.updated_at,
|
||||||
|
generated_at: insight.generated_at,
|
||||||
|
range_from_message_id: insight.range_from_message_id,
|
||||||
|
range_to_message_id: insight.range_to_message_id,
|
||||||
|
status: insight.status,
|
||||||
|
error_message: insight.error_message,
|
||||||
|
schema_version: insight.schema_version,
|
||||||
|
model: insight.model,
|
||||||
|
confidence: insight.confidence
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -88,7 +88,7 @@ class User < ApplicationRecord
|
|||||||
accepts_nested_attributes_for :account_users
|
accepts_nested_attributes_for :account_users
|
||||||
|
|
||||||
has_many :assigned_conversations, foreign_key: 'assignee_id', class_name: 'Conversation', dependent: :nullify, inverse_of: :assignee
|
has_many :assigned_conversations, foreign_key: 'assignee_id', class_name: 'Conversation', dependent: :nullify, inverse_of: :assignee
|
||||||
alias_attribute :conversations, :assigned_conversations
|
alias conversations assigned_conversations
|
||||||
has_many :csat_survey_responses, foreign_key: 'assigned_agent_id', dependent: :nullify, inverse_of: :assigned_agent
|
has_many :csat_survey_responses, foreign_key: 'assigned_agent_id', dependent: :nullify, inverse_of: :assigned_agent
|
||||||
has_many :conversation_participants, dependent: :destroy_async
|
has_many :conversation_participants, dependent: :destroy_async
|
||||||
has_many :participating_conversations, through: :conversation_participants, source: :conversation
|
has_many :participating_conversations, through: :conversation_participants, source: :conversation
|
||||||
|
|||||||
@ -29,7 +29,7 @@ Rails.application.configure do
|
|||||||
config.cache_store = :null_store
|
config.cache_store = :null_store
|
||||||
|
|
||||||
# Raise exceptions instead of rendering exception templates.
|
# Raise exceptions instead of rendering exception templates.
|
||||||
config.action_dispatch.show_exceptions = true
|
config.action_dispatch.show_exceptions = :all
|
||||||
|
|
||||||
# Disable request forgery protection in test environment.
|
# Disable request forgery protection in test environment.
|
||||||
config.action_controller.allow_forgery_protection = false
|
config.action_controller.allow_forgery_protection = false
|
||||||
|
|||||||
@ -1,30 +1,32 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Jasmine::SemanticSearchService do
|
RSpec.describe Jasmine::SemanticSearchService do
|
||||||
|
subject { described_class.new(inbox) }
|
||||||
|
|
||||||
let!(:account) { create(:account) }
|
let!(:account) { create(:account) }
|
||||||
let!(:inbox) { create(:inbox, account: account) }
|
let!(:inbox) { create(:inbox, account: account) }
|
||||||
let!(:config) { create(:jasmine_inbox_config, inbox: inbox, account: account, is_enabled: true) }
|
let!(:config) { create(:jasmine_inbox_config, inbox: inbox, account: account, is_enabled: true) }
|
||||||
|
|
||||||
let!(:collection_private) { create(:jasmine_collection, name: "Private", visibility: :private, owner_inbox: inbox, account: account) }
|
let!(:collection_private) { create(:jasmine_collection, name: 'Private', visibility: :private, owner_inbox: inbox, account: account) }
|
||||||
let!(:collection_shared) { create(:jasmine_collection, name: "Shared", visibility: :shared, account: account) }
|
let!(:collection_shared) { create(:jasmine_collection, name: 'Shared', visibility: :shared, account: account) }
|
||||||
|
|
||||||
# Link collections: Private (High Priority), Shared (Low Priority)
|
# Link collections: Private (High Priority), Shared (Low Priority)
|
||||||
let!(:link_private) { create(:jasmine_inbox_collection, inbox: inbox, collection: collection_private, priority: 10, account: account) }
|
let!(:link_private) { create(:jasmine_inbox_collection, inbox: inbox, collection: collection_private, priority: 10, account: account) }
|
||||||
let!(:link_shared) { create(:jasmine_inbox_collection, inbox: inbox, collection: collection_shared, priority: 0, account: account) }
|
let!(:link_shared) { create(:jasmine_inbox_collection, inbox: inbox, collection: collection_shared, priority: 0, account: account) }
|
||||||
|
|
||||||
let!(:doc_private) { create(:jasmine_document, collection: collection_private, content: "Private Secret", account: account) }
|
let!(:doc_private) { create(:jasmine_document, collection: collection_private, content: 'Private Secret', account: account) }
|
||||||
let!(:doc_shared) { create(:jasmine_document, collection: collection_shared, content: "Shared Knowledge", account: account) }
|
let!(:doc_shared) { create(:jasmine_document, collection: collection_shared, content: 'Shared Knowledge', account: account) }
|
||||||
|
|
||||||
# Mock Embedding Service behavior by creating chunks directly with known vectors
|
# Mock Embedding Service behavior by creating chunks directly with known vectors
|
||||||
# Query Vector: [1.0, 0.0, ...]
|
# Query Vector: [1.0, 0.0, ...]
|
||||||
# Private Match: [0.9, 0.0, ...] -> Distance ~0.1
|
# Private Match: [0.9, 0.0, ...] -> Distance ~0.1
|
||||||
# Shared Match: [0.8, 0.0, ...] -> Distance ~0.2 (Worse match but still good)
|
# Shared Match: [0.8, 0.0, ...] -> Distance ~0.2 (Worse match but still good)
|
||||||
# Irrelevant: [0.0, 1.0, ...] -> Distance ~1.0
|
# Irrelevant: [0.0, 1.0, ...] -> Distance ~1.0
|
||||||
|
|
||||||
before do
|
before do
|
||||||
# Create chunks manually to bypass job/api dependency
|
# Create chunks manually to bypass job/api dependency
|
||||||
create_chunk(doc_private, [0.9] + [0.0]*1535)
|
create_chunk(doc_private, [0.9] + ([0.0]*1535))
|
||||||
create_chunk(doc_shared, [0.8] + [0.0]*1535)
|
create_chunk(doc_shared, [0.8] + ([0.0]*1535))
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_chunk(doc, vec)
|
def create_chunk(doc, vec)
|
||||||
@ -37,43 +39,41 @@ RSpec.describe Jasmine::SemanticSearchService do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
subject { described_class.new(inbox) }
|
|
||||||
|
|
||||||
describe '#search' do
|
describe '#search' do
|
||||||
it 'returns results from enabled collections' do
|
it 'returns results from enabled collections' do
|
||||||
# Mock the embedding generation for the query
|
# Mock the embedding generation for the query
|
||||||
allow(RubyLLM).to receive(:embed).and_return(OpenStruct.new(vectors: [[1.0] + [0.0]*1535]))
|
allow(RubyLLM).to receive(:embed).and_return(OpenStruct.new(vectors: [[1.0] + ([0.0]*1535)]))
|
||||||
|
|
||||||
results = subject.search("Query")
|
results = subject.search('Query')
|
||||||
expect(results.size).to be >= 2
|
expect(results.size).to be >= 2
|
||||||
expect(results.first.content).to eq("Private Secret")
|
expect(results.first.content).to eq('Private Secret')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'respects priority (waterfall)' do
|
it 'respects priority (waterfall)' do
|
||||||
allow(RubyLLM).to receive(:embed).and_return(OpenStruct.new(vectors: [[1.0] + [0.0]*1535]))
|
allow(RubyLLM).to receive(:embed).and_return(OpenStruct.new(vectors: [[1.0] + ([0.0]*1535)]))
|
||||||
|
|
||||||
# Even if shared has a PERFECT match, if Private has a "Good Enough" match (below threshold),
|
# Even if shared has a PERFECT match, if Private has a "Good Enough" match (below threshold),
|
||||||
# does the waterfall prioritize Private?
|
# does the waterfall prioritize Private?
|
||||||
# The algorithm gathers candidates from Groups (Priority 10 first).
|
# The algorithm gathers candidates from Groups (Priority 10 first).
|
||||||
# Then it filters/reranks.
|
# Then it filters/reranks.
|
||||||
# If Priority 10 fills the FINAL_LIMIT, Priority 0 is skipped.
|
# If Priority 10 fills the FINAL_LIMIT, Priority 0 is skipped.
|
||||||
|
|
||||||
# Let's limit the service to 1 result to test waterfall
|
# Let's limit the service to 1 result to test waterfall
|
||||||
# stub_const("Jasmine::SemanticSearchService::FINAL_LIMIT", 1) # Removed as service uses arg
|
# stub_const("Jasmine::SemanticSearchService::FINAL_LIMIT", 1) # Removed as service uses arg
|
||||||
|
|
||||||
results = subject.search("Query", limit: 1)
|
results = subject.search('Query', limit: 1)
|
||||||
expect(results.size).to eq(1)
|
expect(results.size).to eq(1)
|
||||||
expect(results.first.content).to eq("Private Secret") # Should be from High Priority collection
|
expect(results.first.content).to eq('Private Secret') # Should be from High Priority collection
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'filters out results above threshold' do
|
it 'filters out results above threshold' do
|
||||||
# Create a bad chunk
|
# Create a bad chunk
|
||||||
bad_doc = create(:jasmine_document, collection: collection_private, account: account)
|
bad_doc = create(:jasmine_document, collection: collection_private, account: account)
|
||||||
create_chunk(bad_doc, [0.0] + [1.0]*1535) # Orthogonal/Opposite
|
create_chunk(bad_doc, [0.0] + ([1.0] * 1535)) # Orthogonal/Opposite
|
||||||
|
|
||||||
allow(RubyLLM).to receive(:embed).and_return(OpenStruct.new(vectors: [[1.0] + [0.0]*1535]))
|
allow(RubyLLM).to receive(:embed).and_return(OpenStruct.new(vectors: [[1.0] + ([0.0] * 1535)]))
|
||||||
|
|
||||||
results = subject.search("Query")
|
results = subject.search('Query')
|
||||||
expect(results.map(&:content)).not_to include(bad_doc.content)
|
expect(results.map(&:content)).not_to include(bad_doc.content)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -11,7 +11,6 @@ def test_msg(assistant, msg)
|
|||||||
puts " (Raciocínio: #{res['reasoning']})"
|
puts " (Raciocínio: #{res['reasoning']})"
|
||||||
end
|
end
|
||||||
|
|
||||||
test_msg(assistant, "Oi, qual o valor da pernoite na Alexa?")
|
test_msg(assistant, 'Oi, qual o valor da pernoite na Alexa?')
|
||||||
test_msg(assistant, "Quero reservar para amanhã 22h")
|
test_msg(assistant, 'Quero reservar para amanhã 22h')
|
||||||
test_msg(assistant, "ESTOU COM MUITA RAIVA!")
|
test_msg(assistant, 'ESTOU COM MUITA RAIVA!')
|
||||||
|
|
||||||
|
|||||||
@ -3,22 +3,22 @@
|
|||||||
|
|
||||||
assistant = Captain::Assistant.find_by(name: 'Jasmine (Hotel Prime)')
|
assistant = Captain::Assistant.find_by(name: 'Jasmine (Hotel Prime)')
|
||||||
unless assistant
|
unless assistant
|
||||||
puts "Assistente Jasmine não encontrada. Execute o seed primeiro."
|
puts 'Assistente Jasmine não encontrada. Execute o seed primeiro.'
|
||||||
exit
|
exit
|
||||||
end
|
end
|
||||||
|
|
||||||
def simulate_chat(assistant, message)
|
def simulate_chat(assistant, message)
|
||||||
puts "\n--- CLIENTE: #{message}"
|
puts "\n--- CLIENTE: #{message}"
|
||||||
|
|
||||||
# Usamos o AgentRunnerService (V2) que é o que suporta handoffs/sub-agentes
|
# Usamos o AgentRunnerService (V2) que é o que suporta handoffs/sub-agentes
|
||||||
runner = Captain::Assistant::AgentRunnerService.new(assistant: assistant)
|
runner = Captain::Assistant::AgentRunnerService.new(assistant: assistant)
|
||||||
|
|
||||||
# Simulamos o histórico com apenas a última mensagem do usuário
|
# Simulamos o histórico com apenas a última mensagem do usuário
|
||||||
history = [{ role: 'user', content: message }]
|
history = [{ role: 'user', content: message }]
|
||||||
|
|
||||||
response = runner.generate_response(message_history: history)
|
response = runner.generate_response(message_history: history)
|
||||||
|
|
||||||
puts "--- JASMINE RESPONDE:"
|
puts '--- JASMINE RESPONDE:'
|
||||||
puts "DEBUG: #{response.inspect}"
|
puts "DEBUG: #{response.inspect}"
|
||||||
puts "Pensamento: #{response['reasoning']}"
|
puts "Pensamento: #{response['reasoning']}"
|
||||||
puts "Agente Atual: #{response['agent_name'] || 'Orquestrador'}"
|
puts "Agente Atual: #{response['agent_name'] || 'Orquestrador'}"
|
||||||
@ -27,8 +27,8 @@ def simulate_chat(assistant, message)
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Casos de Teste
|
# Casos de Teste
|
||||||
simulate_chat(assistant, "Oi, quanto custa a pernoite na suite Alexa?")
|
simulate_chat(assistant, 'Oi, quanto custa a pernoite na suite Alexa?')
|
||||||
simulate_chat(assistant, "Quero reservar para sabado que vem às 20h.")
|
simulate_chat(assistant, 'Quero reservar para sabado que vem às 20h.')
|
||||||
simulate_chat(assistant, "Tem suite livre agora? To chegando em 10 minutos.")
|
simulate_chat(assistant, 'Tem suite livre agora? To chegando em 10 minutos.')
|
||||||
simulate_chat(assistant, "Pode me mandar fotos da suite com hidro?")
|
simulate_chat(assistant, 'Pode me mandar fotos da suite com hidro?')
|
||||||
simulate_chat(assistant, "ESTOU MUITO IRRITADO COM A DEMORA!") # Teste de sentimento
|
simulate_chat(assistant, 'ESTOU MUITO IRRITADO COM A DEMORA!') # Teste de sentimento
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user