Merge branch 'develop' into feat/v5-ui-redesign
This commit is contained in:
@@ -2,10 +2,6 @@ class DeleteObjectJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
BATCH_SIZE = 5_000
|
||||
HEAVY_ASSOCIATIONS = {
|
||||
Account => %i[conversations contacts inboxes reporting_events],
|
||||
Inbox => %i[conversations contact_inboxes reporting_events]
|
||||
}.freeze
|
||||
|
||||
def perform(object, user = nil, ip = nil)
|
||||
# Pre-purge heavy associations for large objects to avoid
|
||||
@@ -19,11 +15,18 @@ class DeleteObjectJob < ApplicationJob
|
||||
|
||||
private
|
||||
|
||||
def heavy_associations
|
||||
{
|
||||
Account => %i[conversations contacts inboxes reporting_events],
|
||||
Inbox => %i[conversations contact_inboxes reporting_events]
|
||||
}.freeze
|
||||
end
|
||||
|
||||
def purge_heavy_associations(object)
|
||||
klass = HEAVY_ASSOCIATIONS.keys.find { |k| object.is_a?(k) }
|
||||
klass = heavy_associations.keys.find { |k| object.is_a?(k) }
|
||||
return unless klass
|
||||
|
||||
HEAVY_ASSOCIATIONS[klass].each do |assoc|
|
||||
heavy_associations[klass].each do |assoc|
|
||||
next unless object.respond_to?(assoc)
|
||||
|
||||
batch_destroy(object.public_send(assoc))
|
||||
|
||||
@@ -17,7 +17,7 @@ class Api::V1::Accounts::SlaPoliciesController < Api::V1::Accounts::EnterpriseAc
|
||||
end
|
||||
|
||||
def destroy
|
||||
@sla_policy.destroy!
|
||||
::DeleteObjectJob.perform_later(@sla_policy, Current.user, request.ip) if @sla_policy.present?
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
module Enterprise::DeleteObjectJob
|
||||
private
|
||||
|
||||
def heavy_associations
|
||||
super.merge(
|
||||
SlaPolicy => %i[applied_slas]
|
||||
).freeze
|
||||
end
|
||||
|
||||
def process_post_deletion_tasks(object, user, ip)
|
||||
create_audit_entry(object, user, ip)
|
||||
end
|
||||
|
||||
def create_audit_entry(object, user, ip)
|
||||
return unless %w[Inbox Conversation].include?(object.class.to_s) && user.present?
|
||||
return unless %w[Inbox Conversation SlaPolicy].include?(object.class.to_s) && user.present?
|
||||
|
||||
Enterprise::AuditLog.create(
|
||||
auditable: object,
|
||||
|
||||
@@ -66,6 +66,31 @@ module Concerns::Toolable
|
||||
[auth_config['username'], auth_config['password']]
|
||||
end
|
||||
|
||||
def build_metadata_headers(state)
|
||||
{}.tap do |headers|
|
||||
add_base_headers(headers, state)
|
||||
add_conversation_headers(headers, state[:conversation]) if state[:conversation]
|
||||
add_contact_headers(headers, state[:contact]) if state[:contact]
|
||||
end
|
||||
end
|
||||
|
||||
def add_base_headers(headers, state)
|
||||
headers['X-Chatwoot-Account-Id'] = state[:account_id].to_s if state[:account_id]
|
||||
headers['X-Chatwoot-Assistant-Id'] = state[:assistant_id].to_s if state[:assistant_id]
|
||||
headers['X-Chatwoot-Tool-Slug'] = slug if slug.present?
|
||||
end
|
||||
|
||||
def add_conversation_headers(headers, conversation)
|
||||
headers['X-Chatwoot-Conversation-Id'] = conversation[:id].to_s if conversation[:id]
|
||||
headers['X-Chatwoot-Conversation-Display-Id'] = conversation[:display_id].to_s if conversation[:display_id]
|
||||
end
|
||||
|
||||
def add_contact_headers(headers, contact)
|
||||
headers['X-Chatwoot-Contact-Id'] = contact[:id].to_s if contact[:id]
|
||||
headers['X-Chatwoot-Contact-Email'] = contact[:email].to_s if contact[:email].present?
|
||||
headers['X-Chatwoot-Contact-Phone'] = contact[:phone_number].to_s if contact[:phone_number].present?
|
||||
end
|
||||
|
||||
def format_response(raw_response_body)
|
||||
return raw_response_body if response_template.blank?
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ class SlaPolicy < ApplicationRecord
|
||||
validates :name, presence: true
|
||||
|
||||
has_many :conversations, dependent: :nullify
|
||||
has_many :applied_slas, dependent: :destroy
|
||||
has_many :applied_slas, dependent: :destroy_async
|
||||
|
||||
def push_event_data
|
||||
{
|
||||
|
||||
@@ -11,11 +11,11 @@ class Captain::Tools::HttpTool < Agents::Tool
|
||||
@custom_tool.enabled?
|
||||
end
|
||||
|
||||
def perform(_tool_context, **params)
|
||||
def perform(tool_context, **params)
|
||||
url = @custom_tool.build_request_url(params)
|
||||
body = @custom_tool.build_request_body(params)
|
||||
|
||||
response = execute_http_request(url, body)
|
||||
response = execute_http_request(url, body, tool_context)
|
||||
@custom_tool.format_response(response.body)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("HttpTool execution error for #{@custom_tool.slug}: #{e.class} - #{e.message}")
|
||||
@@ -39,7 +39,7 @@ class Captain::Tools::HttpTool < Agents::Tool
|
||||
# 1MB of text ≈ 250K tokens, which exceeds most LLM context windows
|
||||
MAX_RESPONSE_SIZE = 1.megabyte
|
||||
|
||||
def execute_http_request(url, body)
|
||||
def execute_http_request(url, body, tool_context)
|
||||
uri = URI.parse(url)
|
||||
|
||||
# Check if resolved IP is private
|
||||
@@ -53,6 +53,7 @@ class Captain::Tools::HttpTool < Agents::Tool
|
||||
|
||||
request = build_http_request(uri, body)
|
||||
apply_authentication(request)
|
||||
apply_metadata_headers(request, tool_context)
|
||||
|
||||
response = http.request(request)
|
||||
|
||||
@@ -102,4 +103,10 @@ class Captain::Tools::HttpTool < Agents::Tool
|
||||
credentials = @custom_tool.build_basic_auth_credentials
|
||||
request.basic_auth(*credentials) if credentials
|
||||
end
|
||||
|
||||
def apply_metadata_headers(request, tool_context)
|
||||
state = tool_context&.state || {}
|
||||
metadata_headers = @custom_tool.build_metadata_headers(state)
|
||||
metadata_headers.each { |key, value| request[key] = value }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# Apply SLA Policy to Conversations
|
||||
#
|
||||
# This task applies an SLA policy to existing conversations that don't have one assigned.
|
||||
# It processes conversations in batches and only affects conversations with sla_policy_id = nil.
|
||||
#
|
||||
# Usage Examples:
|
||||
# # Using arguments (may need escaping in some shells)
|
||||
# bundle exec rake "sla:apply_to_conversations[19,1,500]"
|
||||
#
|
||||
# # Using environment variables (recommended)
|
||||
# SLA_POLICY_ID=19 ACCOUNT_ID=1 BATCH_SIZE=500 bundle exec rake sla:apply_to_conversations
|
||||
#
|
||||
# Parameters:
|
||||
# SLA_POLICY_ID: ID of the SLA policy to apply (required)
|
||||
# ACCOUNT_ID: ID of the account (required)
|
||||
# BATCH_SIZE: Number of conversations to process (default: 1000)
|
||||
#
|
||||
# Notes:
|
||||
# - Only runs in development environment
|
||||
# - Processes conversations in order of newest first (id DESC)
|
||||
# - Safe to run multiple times - skips conversations that already have SLA policies
|
||||
# - Creates AppliedSla records automatically via Rails callbacks
|
||||
# - SlaEvent records are created later by background jobs when violations occur
|
||||
#
|
||||
# rubocop:disable Metrics/BlockLength
|
||||
namespace :sla do
|
||||
desc 'Apply SLA policy to existing conversations'
|
||||
task :apply_to_conversations, [:sla_policy_id, :account_id, :batch_size] => :environment do |_t, args|
|
||||
unless Rails.env.development?
|
||||
puts 'This task can only be run in the development environment.'
|
||||
puts "Current environment: #{Rails.env}"
|
||||
exit(1)
|
||||
end
|
||||
|
||||
sla_policy_id = args[:sla_policy_id] || ENV.fetch('SLA_POLICY_ID', nil)
|
||||
account_id = args[:account_id] || ENV.fetch('ACCOUNT_ID', nil)
|
||||
batch_size = (args[:batch_size] || ENV['BATCH_SIZE'] || 1000).to_i
|
||||
|
||||
if sla_policy_id.blank?
|
||||
puts 'Error: SLA_POLICY_ID is required'
|
||||
puts 'Usage: bundle exec rake sla:apply_to_conversations[sla_policy_id,account_id,batch_size]'
|
||||
puts 'Or: SLA_POLICY_ID=1 ACCOUNT_ID=1 BATCH_SIZE=500 bundle exec rake sla:apply_to_conversations'
|
||||
exit(1)
|
||||
end
|
||||
|
||||
if account_id.blank?
|
||||
puts 'Error: ACCOUNT_ID is required'
|
||||
puts 'Usage: bundle exec rake sla:apply_to_conversations[sla_policy_id,account_id,batch_size]'
|
||||
puts 'Or: SLA_POLICY_ID=1 ACCOUNT_ID=1 BATCH_SIZE=500 bundle exec rake sla:apply_to_conversations'
|
||||
exit(1)
|
||||
end
|
||||
|
||||
account = Account.find_by(id: account_id)
|
||||
unless account
|
||||
puts "Error: Account with ID #{account_id} not found"
|
||||
exit(1)
|
||||
end
|
||||
|
||||
sla_policy = account.sla_policies.find_by(id: sla_policy_id)
|
||||
unless sla_policy
|
||||
puts "Error: SLA Policy with ID #{sla_policy_id} not found for Account #{account_id}"
|
||||
exit(1)
|
||||
end
|
||||
|
||||
conversations = account.conversations.where(sla_policy_id: nil).order(id: :desc).limit(batch_size)
|
||||
total_count = conversations.count
|
||||
|
||||
if total_count.zero?
|
||||
puts 'No conversations found without SLA policy'
|
||||
exit(0)
|
||||
end
|
||||
|
||||
puts "Applying SLA Policy '#{sla_policy.name}' (ID: #{sla_policy_id}) to #{total_count} conversations in Account #{account_id}"
|
||||
puts "Processing in batches of #{batch_size}"
|
||||
puts "Started at: #{Time.current}"
|
||||
|
||||
start_time = Time.current
|
||||
processed_count = 0
|
||||
error_count = 0
|
||||
|
||||
conversations.find_in_batches(batch_size: batch_size) do |batch|
|
||||
batch.each do |conversation|
|
||||
conversation.update!(sla_policy_id: sla_policy_id)
|
||||
processed_count += 1
|
||||
puts "Processed #{processed_count}/#{total_count} conversations" if (processed_count % 100).zero?
|
||||
rescue StandardError => e
|
||||
error_count += 1
|
||||
puts "Error applying SLA to conversation #{conversation.id}: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
elapsed_time = Time.current - start_time
|
||||
puts "\nCompleted!"
|
||||
puts "Successfully processed: #{processed_count} conversations"
|
||||
puts "Errors encountered: #{error_count}" if error_count.positive?
|
||||
puts "Total time: #{elapsed_time.round(2)}s"
|
||||
puts "Average time per conversation: #{(elapsed_time / processed_count).round(3)}s" if processed_count.positive?
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/BlockLength
|
||||
@@ -0,0 +1,176 @@
|
||||
# Generate Bulk Conversations
|
||||
#
|
||||
# This task creates bulk conversations with fake contacts and movie dialogue messages
|
||||
# for testing purposes. Each conversation gets random messages between contacts and agents.
|
||||
#
|
||||
# Usage Examples:
|
||||
# # Using arguments (may need escaping in some shells)
|
||||
# bundle exec rake "conversations:generate_bulk[100,1,1]"
|
||||
#
|
||||
# # Using environment variables (recommended)
|
||||
# COUNT=100 ACCOUNT_ID=1 INBOX_ID=1 bundle exec rake conversations:generate_bulk
|
||||
#
|
||||
# # Generate 50 conversations
|
||||
# COUNT=50 ACCOUNT_ID=1 INBOX_ID=1 bundle exec rake conversations:generate_bulk
|
||||
#
|
||||
# Parameters:
|
||||
# COUNT: Number of conversations to create (default: 10)
|
||||
# ACCOUNT_ID: ID of the account (required)
|
||||
# INBOX_ID: ID of the inbox that belongs to the account (required)
|
||||
#
|
||||
# What it creates:
|
||||
# - Unique contacts with fake names, emails, phone numbers
|
||||
# - Conversations with random status (open/resolved/pending)
|
||||
# - 3-10 messages per conversation with movie quotes
|
||||
# - Alternating incoming/outgoing message flow
|
||||
#
|
||||
# Notes:
|
||||
# - Only runs in development environment
|
||||
# - Creates realistic test data for conversation testing
|
||||
# - Progress shown every 10 conversations
|
||||
# - All contacts get unique email addresses to avoid conflicts
|
||||
#
|
||||
# rubocop:disable Metrics/BlockLength
|
||||
namespace :conversations do
|
||||
desc 'Generate bulk conversations with contacts and movie dialogue messages'
|
||||
task :generate_bulk, [:count, :account_id, :inbox_id] => :environment do |_t, args|
|
||||
unless Rails.env.development?
|
||||
puts 'This task can only be run in the development environment.'
|
||||
puts "Current environment: #{Rails.env}"
|
||||
exit(1)
|
||||
end
|
||||
|
||||
count = (args[:count] || ENV['COUNT'] || 10).to_i
|
||||
account_id = args[:account_id] || ENV.fetch('ACCOUNT_ID', nil)
|
||||
inbox_id = args[:inbox_id] || ENV.fetch('INBOX_ID', nil)
|
||||
|
||||
if account_id.blank?
|
||||
puts 'Error: ACCOUNT_ID is required'
|
||||
puts 'Usage: bundle exec rake conversations:generate_bulk[count,account_id,inbox_id]'
|
||||
puts 'Or: COUNT=100 ACCOUNT_ID=1 INBOX_ID=1 bundle exec rake conversations:generate_bulk'
|
||||
exit(1)
|
||||
end
|
||||
|
||||
if inbox_id.blank?
|
||||
puts 'Error: INBOX_ID is required'
|
||||
puts 'Usage: bundle exec rake conversations:generate_bulk[count,account_id,inbox_id]'
|
||||
puts 'Or: COUNT=100 ACCOUNT_ID=1 INBOX_ID=1 bundle exec rake conversations:generate_bulk'
|
||||
exit(1)
|
||||
end
|
||||
|
||||
account = Account.find_by(id: account_id)
|
||||
inbox = Inbox.find_by(id: inbox_id)
|
||||
|
||||
unless account
|
||||
puts "Error: Account with ID #{account_id} not found"
|
||||
exit(1)
|
||||
end
|
||||
|
||||
unless inbox
|
||||
puts "Error: Inbox with ID #{inbox_id} not found"
|
||||
exit(1)
|
||||
end
|
||||
|
||||
unless inbox.account_id == account.id
|
||||
puts "Error: Inbox #{inbox_id} does not belong to Account #{account_id}"
|
||||
exit(1)
|
||||
end
|
||||
|
||||
puts "Generating #{count} conversations for Account ##{account.id} in Inbox ##{inbox.id}..."
|
||||
puts "Started at: #{Time.current}"
|
||||
|
||||
start_time = Time.current
|
||||
created_count = 0
|
||||
|
||||
count.times do |i|
|
||||
contact = create_contact(account)
|
||||
contact_inbox = create_contact_inbox(contact, inbox)
|
||||
conversation = create_conversation(contact_inbox)
|
||||
add_messages(conversation)
|
||||
|
||||
created_count += 1
|
||||
puts "Created conversation #{i + 1}/#{count} (ID: #{conversation.id})" if ((i + 1) % 10).zero?
|
||||
rescue StandardError => e
|
||||
puts "Error creating conversation #{i + 1}: #{e.message}"
|
||||
puts e.backtrace.first(5).join("\n")
|
||||
end
|
||||
|
||||
elapsed_time = Time.current - start_time
|
||||
puts "\nCompleted!"
|
||||
puts "Successfully created: #{created_count} conversations"
|
||||
puts "Total time: #{elapsed_time.round(2)}s"
|
||||
puts "Average time per conversation: #{(elapsed_time / created_count).round(3)}s" if created_count.positive?
|
||||
end
|
||||
|
||||
def create_contact(account)
|
||||
Contact.create!(
|
||||
account: account,
|
||||
name: Faker::Name.name,
|
||||
email: "#{SecureRandom.uuid}@example.com",
|
||||
phone_number: generate_e164_phone_number,
|
||||
additional_attributes: {
|
||||
source: 'bulk_generator',
|
||||
company: Faker::Company.name,
|
||||
city: Faker::Address.city
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def generate_e164_phone_number
|
||||
country_code = [1, 44, 61, 91, 81].sample
|
||||
subscriber_number = rand(1_000_000..9_999_999_999).to_s
|
||||
subscriber_number = subscriber_number[0...(15 - country_code.to_s.length)]
|
||||
"+#{country_code}#{subscriber_number}"
|
||||
end
|
||||
|
||||
def create_contact_inbox(contact, inbox)
|
||||
ContactInboxBuilder.new(
|
||||
contact: contact,
|
||||
inbox: inbox
|
||||
).perform
|
||||
end
|
||||
|
||||
def create_conversation(contact_inbox)
|
||||
ConversationBuilder.new(
|
||||
params: ActionController::Parameters.new(
|
||||
status: %w[open resolved pending].sample,
|
||||
additional_attributes: {},
|
||||
custom_attributes: {}
|
||||
),
|
||||
contact_inbox: contact_inbox
|
||||
).perform
|
||||
end
|
||||
|
||||
def add_messages(conversation)
|
||||
num_messages = rand(3..10)
|
||||
message_type = %w[incoming outgoing].sample
|
||||
|
||||
num_messages.times do
|
||||
message_type = message_type == 'incoming' ? 'outgoing' : 'incoming'
|
||||
create_message(conversation, message_type)
|
||||
end
|
||||
end
|
||||
|
||||
def create_message(conversation, message_type)
|
||||
sender = if message_type == 'incoming'
|
||||
conversation.contact
|
||||
else
|
||||
conversation.account.users.sample || conversation.account.administrators.first
|
||||
end
|
||||
|
||||
conversation.messages.create!(
|
||||
account: conversation.account,
|
||||
inbox: conversation.inbox,
|
||||
sender: sender,
|
||||
message_type: message_type,
|
||||
content: generate_movie_dialogue,
|
||||
content_type: :text,
|
||||
private: false
|
||||
)
|
||||
end
|
||||
|
||||
def generate_movie_dialogue
|
||||
Faker::Movie.quote
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/BlockLength
|
||||
@@ -161,12 +161,13 @@ RSpec.describe 'Enterprise SLA API', type: :request do
|
||||
let(:sla_policy) { create(:sla_policy, account: account) }
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'deletes the sla_policy' do
|
||||
it 'queues the sla_policy for deletion' do
|
||||
expect(DeleteObjectJob).to receive(:perform_later).with(sla_policy, administrator, kind_of(String))
|
||||
|
||||
delete "/api/v1/accounts/#{account.id}/sla_policies/#{sla_policy.id}",
|
||||
headers: administrator.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(SlaPolicy.count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -237,5 +237,135 @@ RSpec.describe Captain::Tools::HttpTool, type: :model do
|
||||
expect(result).to eq('Created order #ORD-789 for Widget')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with metadata headers' do
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:contact) { conversation.contact }
|
||||
let(:tool_context_with_state) do
|
||||
Struct.new(:state).new({
|
||||
account_id: account.id,
|
||||
assistant_id: assistant.id,
|
||||
conversation: {
|
||||
id: conversation.id,
|
||||
display_id: conversation.display_id
|
||||
},
|
||||
contact: {
|
||||
id: contact.id,
|
||||
email: contact.email,
|
||||
phone_number: contact.phone_number
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
before do
|
||||
custom_tool.update!(
|
||||
endpoint_url: 'https://example.com/api/data',
|
||||
response_template: nil
|
||||
)
|
||||
end
|
||||
|
||||
it 'includes metadata headers in GET request' do
|
||||
stub_request(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'X-Chatwoot-Account-Id' => account.id.to_s,
|
||||
'X-Chatwoot-Assistant-Id' => assistant.id.to_s,
|
||||
'X-Chatwoot-Tool-Slug' => custom_tool.slug,
|
||||
'X-Chatwoot-Conversation-Id' => conversation.id.to_s,
|
||||
'X-Chatwoot-Conversation-Display-Id' => conversation.display_id.to_s,
|
||||
'X-Chatwoot-Contact-Id' => contact.id.to_s,
|
||||
'X-Chatwoot-Contact-Email' => contact.email
|
||||
})
|
||||
.to_return(status: 200, body: '{"success": true}')
|
||||
|
||||
tool.perform(tool_context_with_state)
|
||||
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'X-Chatwoot-Account-Id' => account.id.to_s,
|
||||
'X-Chatwoot-Contact-Email' => contact.email
|
||||
})
|
||||
end
|
||||
|
||||
it 'includes metadata headers in POST request' do
|
||||
custom_tool.update!(http_method: 'POST', request_template: '{"data": "test"}')
|
||||
|
||||
stub_request(:post, 'https://example.com/api/data')
|
||||
.with(
|
||||
body: '{"data": "test"}',
|
||||
headers: {
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Chatwoot-Account-Id' => account.id.to_s,
|
||||
'X-Chatwoot-Tool-Slug' => custom_tool.slug,
|
||||
'X-Chatwoot-Contact-Email' => contact.email
|
||||
}
|
||||
)
|
||||
.to_return(status: 200, body: '{"success": true}')
|
||||
|
||||
tool.perform(tool_context_with_state)
|
||||
|
||||
expect(WebMock).to have_requested(:post, 'https://example.com/api/data')
|
||||
end
|
||||
|
||||
it 'includes metadata headers along with authentication headers' do
|
||||
custom_tool.update!(
|
||||
auth_type: 'bearer',
|
||||
auth_config: { 'token' => 'test_token' }
|
||||
)
|
||||
|
||||
stub_request(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'Authorization' => 'Bearer test_token',
|
||||
'X-Chatwoot-Account-Id' => account.id.to_s,
|
||||
'X-Chatwoot-Contact-Id' => contact.id.to_s
|
||||
})
|
||||
.to_return(status: 200, body: '{"success": true}')
|
||||
|
||||
tool.perform(tool_context_with_state)
|
||||
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'Authorization' => 'Bearer test_token',
|
||||
'X-Chatwoot-Contact-Id' => contact.id.to_s
|
||||
})
|
||||
end
|
||||
|
||||
it 'handles missing contact in tool context' do
|
||||
tool_context_no_contact = Struct.new(:state).new({
|
||||
account_id: account.id,
|
||||
assistant_id: assistant.id,
|
||||
conversation: {
|
||||
id: conversation.id,
|
||||
display_id: conversation.display_id
|
||||
}
|
||||
})
|
||||
|
||||
stub_request(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'X-Chatwoot-Account-Id' => account.id.to_s,
|
||||
'X-Chatwoot-Conversation-Id' => conversation.id.to_s
|
||||
})
|
||||
.to_return(status: 200, body: '{"success": true}')
|
||||
|
||||
tool.perform(tool_context_no_contact)
|
||||
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/api/data')
|
||||
end
|
||||
|
||||
it 'includes contact phone when present' do
|
||||
contact.update!(phone_number: '+1234567890')
|
||||
tool_context_with_state.state[:contact][:phone_number] = '+1234567890'
|
||||
|
||||
stub_request(:get, 'https://example.com/api/data')
|
||||
.with(headers: {
|
||||
'X-Chatwoot-Contact-Phone' => '+1234567890'
|
||||
})
|
||||
.to_return(status: 200, body: '{"success": true}')
|
||||
|
||||
tool.perform(tool_context_with_state)
|
||||
|
||||
expect(WebMock).to have_requested(:get, 'https://example.com/api/data')
|
||||
.with(headers: { 'X-Chatwoot-Contact-Phone' => '+1234567890' })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -327,6 +327,98 @@ RSpec.describe Captain::CustomTool, type: :model do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#build_metadata_headers' do
|
||||
let(:tool) { create(:captain_custom_tool, account: account, slug: 'custom_test_tool') }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:contact) { conversation.contact }
|
||||
|
||||
let(:state) do
|
||||
{
|
||||
account_id: account.id,
|
||||
assistant_id: 123,
|
||||
conversation: {
|
||||
id: conversation.id,
|
||||
display_id: conversation.display_id
|
||||
},
|
||||
contact: {
|
||||
id: contact.id,
|
||||
email: contact.email,
|
||||
phone_number: contact.phone_number
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'includes account and assistant metadata' do
|
||||
headers = tool.build_metadata_headers(state)
|
||||
|
||||
expect(headers['X-Chatwoot-Account-Id']).to eq(account.id.to_s)
|
||||
expect(headers['X-Chatwoot-Assistant-Id']).to eq('123')
|
||||
end
|
||||
|
||||
it 'includes tool slug' do
|
||||
headers = tool.build_metadata_headers(state)
|
||||
|
||||
expect(headers['X-Chatwoot-Tool-Slug']).to eq('custom_test_tool')
|
||||
end
|
||||
|
||||
it 'includes conversation metadata when present' do
|
||||
headers = tool.build_metadata_headers(state)
|
||||
|
||||
expect(headers['X-Chatwoot-Conversation-Id']).to eq(conversation.id.to_s)
|
||||
expect(headers['X-Chatwoot-Conversation-Display-Id']).to eq(conversation.display_id.to_s)
|
||||
end
|
||||
|
||||
it 'includes contact metadata when present' do
|
||||
headers = tool.build_metadata_headers(state)
|
||||
|
||||
expect(headers['X-Chatwoot-Contact-Id']).to eq(contact.id.to_s)
|
||||
expect(headers['X-Chatwoot-Contact-Email']).to eq(contact.email)
|
||||
end
|
||||
|
||||
it 'handles missing conversation gracefully' do
|
||||
state[:conversation] = nil
|
||||
|
||||
headers = tool.build_metadata_headers(state)
|
||||
|
||||
expect(headers['X-Chatwoot-Conversation-Id']).to be_nil
|
||||
expect(headers['X-Chatwoot-Conversation-Display-Id']).to be_nil
|
||||
expect(headers['X-Chatwoot-Account-Id']).to eq(account.id.to_s)
|
||||
end
|
||||
|
||||
it 'handles missing contact gracefully' do
|
||||
state[:contact] = nil
|
||||
|
||||
headers = tool.build_metadata_headers(state)
|
||||
|
||||
expect(headers['X-Chatwoot-Contact-Id']).to be_nil
|
||||
expect(headers['X-Chatwoot-Contact-Email']).to be_nil
|
||||
expect(headers['X-Chatwoot-Account-Id']).to eq(account.id.to_s)
|
||||
end
|
||||
|
||||
it 'handles empty state' do
|
||||
headers = tool.build_metadata_headers({})
|
||||
|
||||
expect(headers).to be_a(Hash)
|
||||
expect(headers['X-Chatwoot-Tool-Slug']).to eq('custom_test_tool')
|
||||
end
|
||||
|
||||
it 'omits contact email header when email is blank' do
|
||||
state[:contact][:email] = ''
|
||||
|
||||
headers = tool.build_metadata_headers(state)
|
||||
|
||||
expect(headers).not_to have_key('X-Chatwoot-Contact-Email')
|
||||
end
|
||||
|
||||
it 'omits contact phone header when phone number is blank' do
|
||||
state[:contact][:phone_number] = ''
|
||||
|
||||
headers = tool.build_metadata_headers(state)
|
||||
|
||||
expect(headers).not_to have_key('X-Chatwoot-Contact-Phone')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_tool_metadata' do
|
||||
it 'returns tool metadata hash with custom flag' do
|
||||
tool = create(:captain_custom_tool, account: account,
|
||||
|
||||
Reference in New Issue
Block a user