diff --git a/lib/seeders/reports/conversation_creator.rb b/lib/seeders/reports/conversation_creator.rb index 1cd11ef33..33729d3f8 100644 --- a/lib/seeders/reports/conversation_creator.rb +++ b/lib/seeders/reports/conversation_creator.rb @@ -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 } diff --git a/lib/seeders/reports/message_creator.rb b/lib/seeders/reports/message_creator.rb index fc10716d9..8aaf44cde 100644 --- a/lib/seeders/reports/message_creator.rb +++ b/lib/seeders/reports/message_creator.rb @@ -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) diff --git a/lib/seeders/reports/report_data_seeder.rb b/lib/seeders/reports/report_data_seeder.rb index 909818b72..1ad44235a 100644 --- a/lib/seeders/reports/report_data_seeder.rb +++ b/lib/seeders/reports/report_data_seeder.rb @@ -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 diff --git a/lib/tasks/seed_reports_data.rake b/lib/tasks/seed_reports_data.rake index 5c80f1e51..b6fbd8af4 100644 --- a/lib/tasks/seed_reports_data.rake +++ b/lib/tasks/seed_reports_data.rake @@ -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