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
|
||||
|
||||
BUNDLED WITH
|
||||
4.0.4
|
||||
2.5.5
|
||||
|
||||
@ -6,19 +6,13 @@ class Messages::MessageBuilder
|
||||
attr_reader :message
|
||||
|
||||
def initialize(user, conversation, params)
|
||||
@params = params
|
||||
@private = params[:private] || false
|
||||
@conversation = conversation
|
||||
@user = user
|
||||
@conversation = conversation
|
||||
@account = conversation.account
|
||||
@message_type = params[:message_type] || 'outgoing'
|
||||
@attachments = params[:attachments]
|
||||
@automation_rule = content_attributes&.dig(:automation_rule_id)
|
||||
@params = params
|
||||
return unless params.instance_of?(ActionController::Parameters)
|
||||
|
||||
# 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)
|
||||
init_message_attributes
|
||||
end
|
||||
|
||||
def perform
|
||||
@ -223,6 +217,16 @@ class Messages::MessageBuilder
|
||||
'agent' => UserDrop.new(sender)
|
||||
})
|
||||
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
|
||||
|
||||
Messages::MessageBuilder.prepend_mod_with('Messages::MessageBuilder')
|
||||
|
||||
@ -1,72 +1,64 @@
|
||||
module Api
|
||||
module V1
|
||||
module Accounts
|
||||
module Captain
|
||||
class AssistantsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_assistant, only: [:show, :update, :destroy, :playground, :test_webhook]
|
||||
class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_assistant, only: [:show, :update, :destroy, :playground, :test_webhook]
|
||||
|
||||
def index
|
||||
@assistants = current_account.captain_assistants.order(created_at: :desc)
|
||||
render json: @assistants
|
||||
end
|
||||
def index
|
||||
@assistants = current_account.captain_assistants.order(created_at: :desc)
|
||||
render json: @assistants
|
||||
end
|
||||
|
||||
def show
|
||||
render json: @assistant
|
||||
end
|
||||
def show
|
||||
render json: @assistant
|
||||
end
|
||||
|
||||
def create
|
||||
@assistant = current_account.captain_assistants.new(assistant_params)
|
||||
if @assistant.save
|
||||
render json: @assistant
|
||||
else
|
||||
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
|
||||
def create
|
||||
@assistant = current_account.captain_assistants.new(assistant_params)
|
||||
if @assistant.save
|
||||
render json: @assistant
|
||||
else
|
||||
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
|
||||
|
||||
@ -11,28 +11,12 @@ class Api::V1::Accounts::Captain::PricingsController < Api::V1::Accounts::BaseCo
|
||||
end
|
||||
|
||||
def create
|
||||
# Handle multiple inboxes (cloning/distributing the price)
|
||||
inbox_ids_param = params[:pricing][:inbox_ids]
|
||||
|
||||
if inbox_ids_param.present? && inbox_ids_param.is_a?(Array)
|
||||
last_pricing = nil
|
||||
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
|
||||
if inbox_ids_param.is_a?(Array) && inbox_ids_param.present?
|
||||
render json: create_for_multiple_inboxes(inbox_ids_param)
|
||||
else
|
||||
# Create Global (inbox_id: nil) if no specific inbox selected
|
||||
@pricing = current_account.captain_pricings.new(pricing_params)
|
||||
@pricing.inbox_id = nil
|
||||
@pricing.save!
|
||||
render json: @pricing
|
||||
render json: create_single_pricing
|
||||
end
|
||||
rescue StandardError => e
|
||||
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)
|
||||
@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
|
||||
@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
|
||||
params.require(:pricing).permit(:captain_brand_id, :day_range, :suite_category, :duration, :price)
|
||||
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
|
||||
|
||||
@ -1,71 +1,63 @@
|
||||
module Api
|
||||
module V1
|
||||
module Accounts
|
||||
module Captain
|
||||
class ScenariosController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_assistant
|
||||
before_action :fetch_scenario, only: [:show, :update, :destroy]
|
||||
class Api::V1::Accounts::Captain::ScenariosController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_assistant
|
||||
before_action :fetch_scenario, only: [:show, :update, :destroy]
|
||||
|
||||
def index
|
||||
@scenarios = @assistant.captain_scenarios.order(created_at: :desc)
|
||||
render json: @scenarios
|
||||
end
|
||||
def index
|
||||
@scenarios = @assistant.captain_scenarios.order(created_at: :desc)
|
||||
render json: @scenarios
|
||||
end
|
||||
|
||||
def show
|
||||
render json: @scenario
|
||||
end
|
||||
def show
|
||||
render json: @scenario
|
||||
end
|
||||
|
||||
def create
|
||||
@scenario = @assistant.captain_scenarios.new(scenario_params)
|
||||
@scenario.account = current_account
|
||||
if @scenario.save
|
||||
render json: @scenario
|
||||
else
|
||||
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
|
||||
def create
|
||||
@scenario = @assistant.captain_scenarios.new(scenario_params)
|
||||
@scenario.account = current_account
|
||||
if @scenario.save
|
||||
render json: @scenario
|
||||
else
|
||||
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
|
||||
|
||||
@ -1,73 +1,65 @@
|
||||
module Api
|
||||
module V1
|
||||
module Accounts
|
||||
module Captain
|
||||
class ToolsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_assistant
|
||||
class Api::V1::Accounts::Captain::ToolsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_assistant
|
||||
|
||||
NATIVE_TOOLS = [
|
||||
{ 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: '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: '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: '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.' }
|
||||
]
|
||||
NATIVE_TOOLS = [
|
||||
{ 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: '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: '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: '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.' }
|
||||
].freeze
|
||||
|
||||
def index
|
||||
tools = NATIVE_TOOLS.map do |tool|
|
||||
config = @assistant.captain_tool_configs.find_by(tool_key: tool[:key])
|
||||
tool.merge(
|
||||
enabled: config&.is_enabled.nil? || config.is_enabled,
|
||||
webhook_url: config&.webhook_url,
|
||||
plug_play_id: config&.plug_play_id,
|
||||
plug_play_token: config&.plug_play_token,
|
||||
fallback_message: config&.fallback_message
|
||||
)
|
||||
end
|
||||
render json: tools
|
||||
end
|
||||
def index
|
||||
tools = NATIVE_TOOLS.map do |tool|
|
||||
config = @assistant.captain_tool_configs.find_by(tool_key: tool[:key])
|
||||
tool.merge(
|
||||
enabled: config&.is_enabled.nil? || config.is_enabled,
|
||||
webhook_url: config&.webhook_url,
|
||||
plug_play_id: config&.plug_play_id,
|
||||
plug_play_token: config&.plug_play_token,
|
||||
fallback_message: config&.fallback_message
|
||||
)
|
||||
end
|
||||
render json: tools
|
||||
end
|
||||
|
||||
def update
|
||||
tool_key = params[:id]
|
||||
config = @assistant.captain_tool_configs.find_or_initialize_by(tool_key: tool_key)
|
||||
def update
|
||||
tool_key = params[:id]
|
||||
config = @assistant.captain_tool_configs.find_or_initialize_by(tool_key: tool_key)
|
||||
|
||||
# Ensure context unique constraint is respected
|
||||
config.account = current_account
|
||||
# Ensure context unique constraint is respected
|
||||
config.account = current_account
|
||||
|
||||
# Map 'enabled' from frontend to 'is_enabled' in DB
|
||||
update_params = tool_params
|
||||
update_params[:is_enabled] = update_params.delete(:enabled) if update_params.key?(:enabled)
|
||||
# Map 'enabled' from frontend to 'is_enabled' in DB
|
||||
update_params = tool_params
|
||||
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
|
||||
render json: config
|
||||
else
|
||||
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
|
||||
if config.save
|
||||
render json: config
|
||||
else
|
||||
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
|
||||
|
||||
@ -1,21 +1,11 @@
|
||||
module Api
|
||||
module V1
|
||||
module Accounts
|
||||
module Captain
|
||||
module Units
|
||||
class ReservationsSyncController < Api::V1::Accounts::BaseController
|
||||
def create
|
||||
unit = Current.account.captain_units.find(params[:unit_id])
|
||||
::Captain::Reservations::SyncService.new(unit).perform
|
||||
head :ok
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_not_found_error('Unit not found')
|
||||
rescue StandardError => e
|
||||
render_error(e.message)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
class Api::V1::Accounts::Captain::Units::ReservationsSyncController < Api::V1::Accounts::BaseController
|
||||
def create
|
||||
unit = Current.account.captain_units.find(params[:unit_id])
|
||||
::Captain::Reservations::SyncService.new(unit).perform
|
||||
head :ok
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_not_found_error('Unit not found')
|
||||
rescue StandardError => e
|
||||
render_error(e.message)
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,65 +1,57 @@
|
||||
module Api
|
||||
module V1
|
||||
module Accounts
|
||||
module Captain
|
||||
class UnitsController < Api::V1::Accounts::BaseController
|
||||
def index
|
||||
@units = Current.account.captain_units
|
||||
end
|
||||
class Api::V1::Accounts::Captain::UnitsController < Api::V1::Accounts::BaseController
|
||||
def index
|
||||
@units = Current.account.captain_units
|
||||
end
|
||||
|
||||
def show
|
||||
@unit = Current.account.captain_units.find(params[:id])
|
||||
end
|
||||
def show
|
||||
@unit = Current.account.captain_units.find(params[:id])
|
||||
end
|
||||
|
||||
def create
|
||||
@unit = Current.account.captain_units.new(unit_params)
|
||||
@unit.captain_brand = Current.account.captain_brands.first # Default brand logic for now
|
||||
def create
|
||||
@unit = Current.account.captain_units.new(unit_params)
|
||||
@unit.captain_brand = Current.account.captain_brands.first # Default brand logic for now
|
||||
|
||||
if @unit.save
|
||||
render 'show', status: :created
|
||||
else
|
||||
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
|
||||
if @unit.save
|
||||
render 'show', status: :created
|
||||
else
|
||||
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
|
||||
|
||||
@ -19,25 +19,7 @@ class Api::V1::Accounts::Conversations::CrmInsightsController < Api::V1::Account
|
||||
def serialize_insight(insight)
|
||||
return nil if insight.blank?
|
||||
|
||||
{
|
||||
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
|
||||
}
|
||||
insight_attributes(insight)
|
||||
end
|
||||
|
||||
def insights_payload
|
||||
@ -68,4 +50,26 @@ class Api::V1::Accounts::Conversations::CrmInsightsController < Api::V1::Account
|
||||
|
||||
meta
|
||||
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
|
||||
|
||||
@ -88,7 +88,7 @@ class User < ApplicationRecord
|
||||
accepts_nested_attributes_for :account_users
|
||||
|
||||
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 :conversation_participants, dependent: :destroy_async
|
||||
has_many :participating_conversations, through: :conversation_participants, source: :conversation
|
||||
|
||||
@ -29,7 +29,7 @@ Rails.application.configure do
|
||||
config.cache_store = :null_store
|
||||
|
||||
# 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.
|
||||
config.action_controller.allow_forgery_protection = false
|
||||
|
||||
@ -1,30 +1,32 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Jasmine::SemanticSearchService do
|
||||
subject { described_class.new(inbox) }
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
let!(:inbox) { create(:inbox, account: account) }
|
||||
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_shared) { create(:jasmine_collection, name: "Shared", visibility: :shared, 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) }
|
||||
|
||||
# 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_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_shared) { create(:jasmine_document, collection: collection_shared, content: "Shared Knowledge", 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) }
|
||||
|
||||
# Mock Embedding Service behavior by creating chunks directly with known vectors
|
||||
# Query Vector: [1.0, 0.0, ...]
|
||||
# Private Match: [0.9, 0.0, ...] -> Distance ~0.1
|
||||
# Shared Match: [0.8, 0.0, ...] -> Distance ~0.2 (Worse match but still good)
|
||||
# Irrelevant: [0.0, 1.0, ...] -> Distance ~1.0
|
||||
|
||||
|
||||
before do
|
||||
# Create chunks manually to bypass job/api dependency
|
||||
create_chunk(doc_private, [0.9] + [0.0]*1535)
|
||||
create_chunk(doc_shared, [0.8] + [0.0]*1535)
|
||||
create_chunk(doc_private, [0.9] + ([0.0]*1535))
|
||||
create_chunk(doc_shared, [0.8] + ([0.0]*1535))
|
||||
end
|
||||
|
||||
def create_chunk(doc, vec)
|
||||
@ -37,43 +39,41 @@ RSpec.describe Jasmine::SemanticSearchService do
|
||||
)
|
||||
end
|
||||
|
||||
subject { described_class.new(inbox) }
|
||||
|
||||
describe '#search' do
|
||||
it 'returns results from enabled collections' do
|
||||
# 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.first.content).to eq("Private Secret")
|
||||
expect(results.first.content).to eq('Private Secret')
|
||||
end
|
||||
|
||||
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),
|
||||
# does the waterfall prioritize Private?
|
||||
# The algorithm gathers candidates from Groups (Priority 10 first).
|
||||
# Then it filters/reranks.
|
||||
# If Priority 10 fills the FINAL_LIMIT, Priority 0 is skipped.
|
||||
|
||||
|
||||
# Let's limit the service to 1 result to test waterfall
|
||||
# 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.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
|
||||
|
||||
it 'filters out results above threshold' do
|
||||
# Create a bad chunk
|
||||
bad_doc = create(:jasmine_document, collection: collection_private, account: account)
|
||||
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]))
|
||||
|
||||
results = subject.search("Query")
|
||||
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)]))
|
||||
|
||||
results = subject.search('Query')
|
||||
expect(results.map(&:content)).not_to include(bad_doc.content)
|
||||
end
|
||||
end
|
||||
|
||||
@ -11,7 +11,6 @@ def test_msg(assistant, msg)
|
||||
puts " (Raciocínio: #{res['reasoning']})"
|
||||
end
|
||||
|
||||
test_msg(assistant, "Oi, qual o valor da pernoite na Alexa?")
|
||||
test_msg(assistant, "Quero reservar para amanhã 22h")
|
||||
test_msg(assistant, "ESTOU COM MUITA RAIVA!")
|
||||
|
||||
test_msg(assistant, 'Oi, qual o valor da pernoite na Alexa?')
|
||||
test_msg(assistant, 'Quero reservar para amanhã 22h')
|
||||
test_msg(assistant, 'ESTOU COM MUITA RAIVA!')
|
||||
|
||||
@ -3,22 +3,22 @@
|
||||
|
||||
assistant = Captain::Assistant.find_by(name: 'Jasmine (Hotel Prime)')
|
||||
unless assistant
|
||||
puts "Assistente Jasmine não encontrada. Execute o seed primeiro."
|
||||
puts 'Assistente Jasmine não encontrada. Execute o seed primeiro.'
|
||||
exit
|
||||
end
|
||||
|
||||
def simulate_chat(assistant, message)
|
||||
puts "\n--- CLIENTE: #{message}"
|
||||
|
||||
|
||||
# Usamos o AgentRunnerService (V2) que é o que suporta handoffs/sub-agentes
|
||||
runner = Captain::Assistant::AgentRunnerService.new(assistant: assistant)
|
||||
|
||||
|
||||
# Simulamos o histórico com apenas a última mensagem do usuário
|
||||
history = [{ role: 'user', content: message }]
|
||||
|
||||
|
||||
response = runner.generate_response(message_history: history)
|
||||
|
||||
puts "--- JASMINE RESPONDE:"
|
||||
|
||||
puts '--- JASMINE RESPONDE:'
|
||||
puts "DEBUG: #{response.inspect}"
|
||||
puts "Pensamento: #{response['reasoning']}"
|
||||
puts "Agente Atual: #{response['agent_name'] || 'Orquestrador'}"
|
||||
@ -27,8 +27,8 @@ def simulate_chat(assistant, message)
|
||||
end
|
||||
|
||||
# Casos de Teste
|
||||
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, "Tem suite livre agora? To chegando em 10 minutos.")
|
||||
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, 'Oi, quanto custa a pernoite na suite Alexa?')
|
||||
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, 'Pode me mandar fotos da suite com hidro?')
|
||||
simulate_chat(assistant, 'ESTOU MUITO IRRITADO COM A DEMORA!') # Teste de sentimento
|
||||
|
||||
Loading…
Reference in New Issue
Block a user