diff --git a/Gemfile.lock b/Gemfile.lock index d607e95..6b54493 100755 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1183,4 +1183,4 @@ RUBY VERSION ruby 3.4.4p34 BUNDLED WITH - 4.0.4 + 2.5.5 diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index 47e914b..87c3a7b 100755 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -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') diff --git a/app/controllers/api/v1/accounts/captain/assistants_controller.rb b/app/controllers/api/v1/accounts/captain/assistants_controller.rb index 8237a44..898c083 100644 --- a/app/controllers/api/v1/accounts/captain/assistants_controller.rb +++ b/app/controllers/api/v1/accounts/captain/assistants_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/captain/pricings_controller.rb b/app/controllers/api/v1/accounts/captain/pricings_controller.rb index 456b349..276b636 100644 --- a/app/controllers/api/v1/accounts/captain/pricings_controller.rb +++ b/app/controllers/api/v1/accounts/captain/pricings_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/captain/scenarios_controller.rb b/app/controllers/api/v1/accounts/captain/scenarios_controller.rb index 9b9137c..77d7a40 100644 --- a/app/controllers/api/v1/accounts/captain/scenarios_controller.rb +++ b/app/controllers/api/v1/accounts/captain/scenarios_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/captain/tools_controller.rb b/app/controllers/api/v1/accounts/captain/tools_controller.rb index b5ad0d7..12e8994 100644 --- a/app/controllers/api/v1/accounts/captain/tools_controller.rb +++ b/app/controllers/api/v1/accounts/captain/tools_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/captain/units/reservations_sync_controller.rb b/app/controllers/api/v1/accounts/captain/units/reservations_sync_controller.rb index ba8225c..e0a9359 100644 --- a/app/controllers/api/v1/accounts/captain/units/reservations_sync_controller.rb +++ b/app/controllers/api/v1/accounts/captain/units/reservations_sync_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/captain/units_controller.rb b/app/controllers/api/v1/accounts/captain/units_controller.rb index 39f0f5d..406ad21 100644 --- a/app/controllers/api/v1/accounts/captain/units_controller.rb +++ b/app/controllers/api/v1/accounts/captain/units_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/conversations/crm_insights_controller.rb b/app/controllers/api/v1/accounts/conversations/crm_insights_controller.rb index 79ac7f2..ec1c96e 100644 --- a/app/controllers/api/v1/accounts/conversations/crm_insights_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/crm_insights_controller.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 4923d0a..802ba1b 100755 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/config/environments/test.rb b/config/environments/test.rb index 4d9e77c..d656538 100755 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -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 diff --git a/spec/services/jasmine/semantic_search_service_spec.rb b/spec/services/jasmine/semantic_search_service_spec.rb index 4c0a67f..71a9f0a 100644 --- a/spec/services/jasmine/semantic_search_service_spec.rb +++ b/spec/services/jasmine/semantic_search_service_spec.rb @@ -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 diff --git a/test_jasmine_final.rb b/test_jasmine_final.rb index f02cbd8..8b7c169 100644 --- a/test_jasmine_final.rb +++ b/test_jasmine_final.rb @@ -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!') diff --git a/test_jasmine_routing.rb b/test_jasmine_routing.rb index 5504d6d..6fdd9d6 100644 --- a/test_jasmine_routing.rb +++ b/test_jasmine_routing.rb @@ -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