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:
Rodrigo Borba 2026-01-25 08:39:23 -03:00
parent c76c8d5c50
commit b80d35a307
14 changed files with 332 additions and 363 deletions

View File

@ -1183,4 +1183,4 @@ RUBY VERSION
ruby 3.4.4p34 ruby 3.4.4p34
BUNDLED WITH BUNDLED WITH
4.0.4 2.5.5

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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!')

View File

@ -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