feat: update seeder to match new reporting event

This commit is contained in:
Shivam Mishra
2026-01-21 18:26:45 +05:30
parent 6325bd667a
commit cba8e83b91
4 changed files with 278 additions and 58 deletions
+86 -3
View File
@@ -13,6 +13,7 @@ class Seeders::Reports::ConversationCreator
@teams = resources[:teams]
@labels = resources[:labels]
@agents = resources[:agents]
@captain_assistant = resources[:captain_assistant]
@priorities = [nil, 'urgent', 'high', 'medium', 'low']
end
@@ -27,8 +28,17 @@ class Seeders::Reports::ConversationCreator
conversation = build_conversation
conversation.save!
# Trigger conversation_created event
trigger_conversation_created_event(conversation)
add_labels_to_conversation(conversation)
create_messages_for_conversation(conversation)
# Check if this is a bot inbox or human-only inbox
if inbox_has_captain?(conversation.inbox)
create_bot_conversation_flow(conversation, created_at)
else
create_human_conversation_flow(conversation)
end
# Determine if should resolve but don't update yet
should_resolve = rand > 0.3
@@ -100,15 +110,88 @@ class Seeders::Reports::ConversationCreator
conversation.update_labels(labels_to_add.map(&:title))
end
def create_messages_for_conversation(conversation)
def create_messages_for_conversation(conversation, include_bot: false)
message_creator = Seeders::Reports::MessageCreator.new(
account: @account,
agents: @agents,
conversation: conversation
conversation: conversation,
captain_assistant: include_bot ? @captain_assistant : nil
)
message_creator.create_messages
end
def trigger_conversation_created_event(conversation)
event_data = { conversation: conversation }
ReportingEventListener.instance.conversation_created(
Events::Base.new('conversation_created', Time.current, event_data)
)
end
def inbox_has_captain?(inbox)
return false unless @captain_assistant && inbox.respond_to?(:captain_assistant)
inbox.captain_assistant.present?
end
def create_bot_conversation_flow(conversation, _created_at)
message_creator = Seeders::Reports::MessageCreator.new(
account: @account,
agents: @agents,
conversation: conversation,
captain_assistant: @captain_assistant
)
# Create initial customer message
message_creator.create_incoming_message
# Add delay for bot response
travel(rand((5.seconds)..(30.seconds)))
# Create bot response
message_creator.create_bot_message
# Randomly decide outcome: 30% bot resolves, 50% handoff to human, 20% abandoned
outcome = rand(100)
if outcome < 30
# Bot resolves - no more messages needed, will be resolved later
nil
elsif outcome < 80
# Bot hands off to human - use travel to move forward from current time
travel(rand((1.minute)..(10.minutes)))
trigger_bot_handoff_event(conversation)
# Continue with human agent messages
create_human_messages_after_handoff(message_creator)
end
# else: 20% abandoned - no further action
end
def create_human_conversation_flow(conversation)
create_messages_for_conversation(conversation)
end
def create_human_messages_after_handoff(message_creator)
# Create a few more human agent messages after handoff
message_count = rand(2..4)
message_count.times do |i|
travel(rand((1.minute)..(30.minutes)))
if i.even?
message_creator.create_incoming_message
else
message_creator.create_human_outgoing_message
end
end
end
def trigger_bot_handoff_event(conversation)
event_data = { conversation: conversation }
ReportingEventListener.instance.conversation_bot_handoff(
Events::Base.new('conversation_bot_handoff', Time.current, event_data)
)
end
def trigger_conversation_resolved_event(conversation)
event_data = { conversation: conversation }
+73 -11
View File
@@ -8,10 +8,11 @@ class Seeders::Reports::MessageCreator
MESSAGES_PER_CONVERSATION = 5
def initialize(account:, agents:, conversation:)
def initialize(account:, agents:, conversation:, captain_assistant: nil)
@account = account
@agents = agents
@conversation = conversation
@captain_assistant = captain_assistant
end
def create_messages
@@ -37,6 +38,44 @@ class Seeders::Reports::MessageCreator
false # No longer first reply after any agent message
end
def create_incoming_message
@conversation.messages.create!(
account: @account,
inbox: @conversation.inbox,
message_type: :incoming,
content: generate_customer_message_content,
sender: @conversation.contact
)
end
def create_bot_message
return unless @captain_assistant
@conversation.messages.create!(
account: @account,
inbox: @conversation.inbox,
message_type: :outgoing,
content: generate_bot_response_content,
sender: @captain_assistant
)
end
def create_human_outgoing_message
sender = @conversation.assignee || @agents.sample
message = @conversation.messages.create!(
account: @account,
inbox: @conversation.inbox,
message_type: :outgoing,
content: generate_message_content,
sender: sender
)
# Trigger reply events for human messages after handoff
trigger_reply_event(message)
message
end
private
def add_realistic_delay(_message_index, is_incoming)
@@ -64,16 +103,6 @@ class Seeders::Reports::MessageCreator
end
end
def create_incoming_message
@conversation.messages.create!(
account: @account,
inbox: @conversation.inbox,
message_type: :incoming,
content: generate_message_content,
sender: @conversation.contact
)
end
def create_outgoing_message
sender = @conversation.assignee || @agents.sample
@@ -90,6 +119,39 @@ class Seeders::Reports::MessageCreator
Faker::Lorem.paragraph(sentence_count: rand(1..5))
end
def generate_customer_message_content
customer_messages = [
'Hi, I need help with my order.',
'Can you assist me with a billing issue?',
'I have a question about my account.',
"I'm having trouble logging in.",
'How do I change my password?',
'When will my order arrive?',
"I'd like to request a refund.",
'Can you help me update my shipping address?',
'I received the wrong item.',
'Is there a way to track my package?',
Faker::Lorem.paragraph(sentence_count: rand(1..3))
]
customer_messages.sample
end
def generate_bot_response_content
bot_responses = [
"Hello! I'm here to help. Let me look into that for you.",
'Thank you for reaching out. I can assist you with this.',
'I understand your concern. Let me check our records.',
'Sure, I can help with that! Could you provide more details?',
"I've found some information that might help.",
'Let me connect you with a specialist who can better assist you.',
'Based on your question, here are some options available to you.',
'I can see your account details. Let me review this for you.',
"Thank you for your patience. I'm processing your request now.",
'Is there anything else I can help you with today?'
]
bot_responses.sample
end
def handle_agent_reply_events(message, is_first_reply)
if is_first_reply
trigger_first_reply_event(message)
+78 -32
View File
@@ -8,7 +8,7 @@
# to generate historical data with realistic timestamps.
#
# Usage:
# ACCOUNT_ID=1 ENABLE_ACCOUNT_SEEDING=true bundle exec rake db:seed:reports_data
# ENABLE_ACCOUNT_SEEDING=true bundle exec rake db:seed:reports_data
#
# This will create:
# - 1000 conversations with realistic message exchanges
@@ -25,29 +25,36 @@ require 'faker'
require_relative 'conversation_creator'
require_relative 'message_creator'
# rubocop:disable Rails/Output
# rubocop:disable Rails/Output, Metrics/ClassLength
class Seeders::Reports::ReportDataSeeder
include ActiveSupport::Testing::TimeHelpers
TOTAL_CONVERSATIONS = 1000
TOTAL_CONTACTS = 100
TOTAL_AGENTS = 20
TOTAL_TEAMS = 5
TOTAL_LABELS = 30
TOTAL_INBOXES = 3
MESSAGES_PER_CONVERSATION = 5
START_DATE = 3.months.ago # rubocop:disable Rails/RelativeDateConstant
END_DATE = Time.current
def initialize(account:)
def initialize(account:, config: {})
raise 'Account Seeding is not allowed.' unless ENV.fetch('ENABLE_ACCOUNT_SEEDING', !Rails.env.production?)
@account = account
initialize_config(config)
@teams = []
@agents = []
@labels = []
@inboxes = []
@contacts = []
@captain_assistant = nil
end
def initialize_config(config)
defaults = { total_conversations: 1000, total_contacts: 100, total_agents: 20,
total_teams: 5, total_labels: 30, days_range: 90 }
config = defaults.merge(config)
@total_conversations = config[:total_conversations]
@total_contacts = config[:total_contacts]
@total_agents = config[:total_agents]
@total_teams = config[:total_teams]
@total_labels = config[:total_labels]
@days_range = config[:days_range]
end
def perform!
@@ -56,6 +63,7 @@ class Seeders::Reports::ReportDataSeeder
# Clear existing data
clear_existing_data
create_captain_assistant
create_teams
create_agents
create_labels
@@ -78,26 +86,38 @@ class Seeders::Reports::ReportDataSeeder
@account.contacts.destroy_all
@account.agents.destroy_all
@account.reporting_events.destroy_all
Captain::Assistant.where(account: @account).destroy_all if defined?(Captain::Assistant)
end
def create_captain_assistant
return unless defined?(Captain::Assistant)
@captain_assistant = Captain::Assistant.create!(
account: @account,
name: 'Support Bot',
description: 'AI assistant for customer support'
)
puts 'Created Captain assistant: Support Bot'
end
def create_teams
TOTAL_TEAMS.times do |i|
@total_teams.times do |i|
team = @account.teams.create!(
name: "#{Faker::Company.industry} Team #{i + 1}"
)
@teams << team
print "\rCreating teams: #{i + 1}/#{TOTAL_TEAMS}"
print "\rCreating teams: #{i + 1}/#{@total_teams}"
end
print "\n"
end
def create_agents
TOTAL_AGENTS.times do |i|
@total_agents.times do |i|
user = create_single_agent(i)
assign_agent_to_teams(user)
@agents << user
print "\rCreating agents: #{i + 1}/#{TOTAL_AGENTS}"
print "\rCreating agents: #{i + 1}/#{@total_agents}"
end
print "\n"
@@ -134,22 +154,33 @@ class Seeders::Reports::ReportDataSeeder
end
def create_labels
TOTAL_LABELS.times do |i|
@total_labels.times do |i|
label = @account.labels.create!(
title: "Label-#{i + 1}-#{Faker::Lorem.word}",
description: Faker::Company.catch_phrase,
color: Faker::Color.hex_color
)
@labels << label
print "\rCreating labels: #{i + 1}/#{TOTAL_LABELS}"
print "\rCreating labels: #{i + 1}/#{@total_labels}"
end
print "\n"
end
def create_inboxes
TOTAL_INBOXES.times do |_i|
inbox = create_single_inbox
# Create inboxes: 2/3 with Captain (bot inboxes) and 1/3 without (human-only)
bot_inbox_count = (TOTAL_INBOXES * 2 / 3.0).ceil
human_inbox_count = TOTAL_INBOXES - bot_inbox_count
bot_inbox_count.times do |i|
inbox = create_web_inbox_with_captain("Support Bot Inbox #{i + 1}")
assign_agents_to_inbox(inbox)
@inboxes << inbox
print "\rCreating inboxes: #{@inboxes.size}/#{TOTAL_INBOXES}"
end
human_inbox_count.times do |i|
inbox = create_web_inbox("Human Support Inbox #{i + 1}")
assign_agents_to_inbox(inbox)
@inboxes << inbox
print "\rCreating inboxes: #{@inboxes.size}/#{TOTAL_INBOXES}"
@@ -158,14 +189,27 @@ class Seeders::Reports::ReportDataSeeder
print "\n"
end
def create_single_inbox
def create_web_inbox_with_captain(name)
inbox = create_web_inbox(name)
if @captain_assistant && defined?(CaptainInbox)
CaptainInbox.create!(
captain_assistant: @captain_assistant,
inbox: inbox
)
end
inbox
end
def create_web_inbox(name)
channel = Channel::WebWidget.create!(
website_url: "https://#{Faker::Internet.domain_name}",
account_id: @account.id
)
@account.inboxes.create!(
name: "#{Faker::Company.name} Website",
name: name,
channel: channel
)
end
@@ -176,9 +220,10 @@ class Seeders::Reports::ReportDataSeeder
@agents
else
# Subsequent inboxes get random selection with some overlap
min_agents = [@agents.size / TOTAL_INBOXES, 10].max
max_agents = [(@agents.size * 0.8).to_i, 50].min
@agents.sample(rand(min_agents..max_agents))
min_agents = [(@agents.size / TOTAL_INBOXES), 1].max
max_agents = [(@agents.size * 0.8).to_i, @agents.size].min
sample_count = rand([min_agents, max_agents].min..[min_agents, max_agents].max)
@agents.sample(sample_count)
end
agents_to_assign.each do |agent|
@@ -187,7 +232,7 @@ class Seeders::Reports::ReportDataSeeder
end
def create_contacts
TOTAL_CONTACTS.times do |i|
@total_contacts.times do |i|
contact = @account.contacts.create!(
name: Faker::Name.name,
email: Faker::Internet.email,
@@ -202,7 +247,7 @@ class Seeders::Reports::ReportDataSeeder
)
@contacts << contact
print "\rCreating contacts: #{i + 1}/#{TOTAL_CONTACTS}"
print "\rCreating contacts: #{i + 1}/#{@total_contacts}"
end
print "\n"
@@ -216,19 +261,20 @@ class Seeders::Reports::ReportDataSeeder
inboxes: @inboxes,
teams: @teams,
labels: @labels,
agents: @agents
agents: @agents,
captain_assistant: @captain_assistant
}
)
TOTAL_CONVERSATIONS.times do |i|
created_at = Faker::Time.between(from: START_DATE, to: END_DATE)
@total_conversations.times do |i|
created_at = Faker::Time.between(from: @days_range.days.ago, to: Time.current)
conversation_creator.create_conversation(created_at: created_at)
completion_percentage = ((i + 1).to_f / TOTAL_CONVERSATIONS * 100).round
print "\rCreating conversations: #{i + 1}/#{TOTAL_CONVERSATIONS} (#{completion_percentage}%)"
completion_percentage = ((i + 1).to_f / @total_conversations * 100).round
print "\rCreating conversations: #{i + 1}/#{@total_conversations} (#{completion_percentage}%)"
end
print "\n"
end
end
# rubocop:enable Rails/Output
# rubocop:enable Rails/Output, Metrics/ClassLength
+41 -12
View File
@@ -1,24 +1,53 @@
# rubocop:disable Rails/Output, Metrics/BlockLength
namespace :db do
namespace :seed do
desc 'Seed test data for reports with conversations, contacts, agents, teams, and realistic reporting events'
task reports_data: :environment do
if ENV['ACCOUNT_ID'].blank?
puts 'Please provide an ACCOUNT_ID environment variable'
puts 'Usage: ACCOUNT_ID=1 ENABLE_ACCOUNT_SEEDING=true bundle exec rake db:seed:reports_data'
exit 1
end
abort 'This task can only be run in development environment.' unless Rails.env.development?
abort 'ENABLE_ACCOUNT_SEEDING must be set to true.' unless ENV['ENABLE_ACCOUNT_SEEDING'] == 'true'
ENV['ENABLE_ACCOUNT_SEEDING'] = 'true' if ENV['ENABLE_ACCOUNT_SEEDING'].blank?
account = prompt_for_account
config = prompt_for_config
abort 'Seeding cancelled.' unless confirm_seeding(account)
account_id = ENV.fetch('ACCOUNT_ID', nil)
account = Account.find(account_id)
puts "Starting reports data seeding for account: #{account.name} (ID: #{account.id})"
seeder = Seeders::Reports::ReportDataSeeder.new(account: account)
seeder = Seeders::Reports::ReportDataSeeder.new(account: account, config: config)
seeder.perform!
puts "Finished seeding reports data for account: #{account.name}"
end
def prompt(message)
print "#{message}: "
$stdin.gets.chomp
end
def prompt_with_default(message, default)
input = prompt("#{message} (default: #{default})")
input.blank? ? default : input.to_i
end
def prompt_for_account
input = prompt('Enter Account ID')
account = Account.find_by(id: input)
abort "Account with ID #{input} not found." if account.nil?
account
end
def prompt_for_config
{
total_conversations: prompt_with_default('Total conversations', 1000),
total_contacts: prompt_with_default('Total contacts', 100),
total_agents: prompt_with_default('Total agents', 20),
total_teams: prompt_with_default('Total teams', 5),
total_labels: prompt_with_default('Total labels', 30),
days_range: prompt_with_default('Days of data to generate', 90)
}
end
def confirm_seeding(account)
puts "\nWARNING: This will DELETE all existing data for account '#{account.name}' (ID: #{account.id})"
prompt('Are you sure you want to proceed? (yes/no)').downcase == 'yes'
end
end
end
# rubocop:enable Rails/Output, Metrics/BlockLength