From e4dba113ce1ea5967dd373fd1b1f53d01ffe6e81 Mon Sep 17 00:00:00 2001 From: Pranav Date: Mon, 14 Jul 2025 16:49:12 -0700 Subject: [PATCH] Agentbot as assignee --- .../conversations/assignments_controller.rb | 26 +++++--- app/listeners/agent_bot_listener.rb | 12 ++++ app/models/agent_bot.rb | 1 + app/models/conversation.rb | 60 +++++++++++++------ app/models/user.rb | 2 +- ..._make_conversation_assignee_polymorphic.rb | 13 ++++ db/schema.rb | 4 +- .../concerns/assignment_handler_shared.rb | 7 ++- 8 files changed, 95 insertions(+), 30 deletions(-) create mode 100644 db/migrate/20250714205206_make_conversation_assignee_polymorphic.rb diff --git a/app/controllers/api/v1/accounts/conversations/assignments_controller.rb b/app/controllers/api/v1/accounts/conversations/assignments_controller.rb index 1fb2095e3..8dda74a98 100644 --- a/app/controllers/api/v1/accounts/conversations/assignments_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/assignments_controller.rb @@ -1,8 +1,8 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Accounts::Conversations::BaseController - # assigns agent/team to a conversation + # assigns agent/team/bot to a conversation def create if params.key?(:assignee_id) - set_agent + set_assignee elsif params.key?(:team_id) set_team else @@ -12,18 +12,26 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Account private - def set_agent - @agent = Current.account.users.find_by(id: params[:assignee_id]) - @conversation.assignee = @agent + def set_assignee + @assignee = case params[:assignee_type] + when 'AgentBot' + Current.account.agent_bots.find_by(id: params[:assignee_id]) + else + Current.account.users.find_by(id: params[:assignee_id]) + end + + @conversation.assignee = @assignee @conversation.save! - render_agent + render_assignee end - def render_agent - if @agent.nil? + def render_assignee + if @assignee.nil? render json: nil + elsif @assignee.is_a?(AgentBot) + render json: @assignee.webhook_data else - render partial: 'api/v1/models/agent', formats: [:json], locals: { resource: @agent } + render partial: 'api/v1/models/agent', formats: [:json], locals: { resource: @assignee } end end diff --git a/app/listeners/agent_bot_listener.rb b/app/listeners/agent_bot_listener.rb index 38c046e34..cc4c68bc1 100644 --- a/app/listeners/agent_bot_listener.rb +++ b/app/listeners/agent_bot_listener.rb @@ -50,6 +50,18 @@ class AgentBotListener < BaseListener process_webhook_bot_event(inbox.agent_bot, payload) end + def assignee_changed(event) + conversation = extract_conversation_and_account(event)[0] + return unless conversation.assigned_to_bot? + + agent_bot = conversation.assigned_bot + return if agent_bot.blank? || agent_bot.outgoing_url.blank? + + event_name = __method__.to_s + payload = conversation.webhook_data.merge(event: event_name) + process_webhook_bot_event(agent_bot, payload) + end + private def connected_agent_bot_exist?(inbox) diff --git a/app/models/agent_bot.rb b/app/models/agent_bot.rb index be63aebc5..b4aebc1ba 100644 --- a/app/models/agent_bot.rb +++ b/app/models/agent_bot.rb @@ -24,6 +24,7 @@ class AgentBot < ApplicationRecord has_many :agent_bot_inboxes, dependent: :destroy_async has_many :inboxes, through: :agent_bot_inboxes has_many :messages, as: :sender, dependent: :nullify + has_many :assigned_conversations, as: :assignee, class_name: 'Conversation', dependent: :nullify belongs_to :account, optional: true enum bot_type: { webhook: 0 } diff --git a/app/models/conversation.rb b/app/models/conversation.rb index d6c5d0e4a..159ec25f7 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -6,6 +6,7 @@ # additional_attributes :jsonb # agent_last_seen_at :datetime # assignee_last_seen_at :datetime +# assignee_type :string # cached_label_list :text # contact_last_seen_at :datetime # custom_attributes :jsonb @@ -31,22 +32,23 @@ # # Indexes # -# conv_acid_inbid_stat_asgnid_idx (account_id,inbox_id,status,assignee_id) -# index_conversations_on_account_id (account_id) -# index_conversations_on_account_id_and_display_id (account_id,display_id) UNIQUE -# index_conversations_on_assignee_id_and_account_id (assignee_id,account_id) -# index_conversations_on_campaign_id (campaign_id) -# index_conversations_on_contact_id (contact_id) -# index_conversations_on_contact_inbox_id (contact_inbox_id) -# index_conversations_on_first_reply_created_at (first_reply_created_at) -# index_conversations_on_id_and_account_id (account_id,id) -# index_conversations_on_inbox_id (inbox_id) -# index_conversations_on_priority (priority) -# index_conversations_on_status_and_account_id (status,account_id) -# index_conversations_on_status_and_priority (status,priority) -# index_conversations_on_team_id (team_id) -# index_conversations_on_uuid (uuid) UNIQUE -# index_conversations_on_waiting_since (waiting_since) +# conv_acid_inbid_stat_asgnid_idx (account_id,inbox_id,status,assignee_id) +# index_conversations_on_account_id (account_id) +# index_conversations_on_account_id_and_display_id (account_id,display_id) UNIQUE +# index_conversations_on_assignee_id_and_account_id (assignee_id,account_id) +# index_conversations_on_assignee_type_and_assignee_id (assignee_type,assignee_id) +# index_conversations_on_campaign_id (campaign_id) +# index_conversations_on_contact_id (contact_id) +# index_conversations_on_contact_inbox_id (contact_inbox_id) +# index_conversations_on_first_reply_created_at (first_reply_created_at) +# index_conversations_on_id_and_account_id (account_id,id) +# index_conversations_on_inbox_id (inbox_id) +# index_conversations_on_priority (priority) +# index_conversations_on_status_and_account_id (status,account_id) +# index_conversations_on_status_and_priority (status,priority) +# index_conversations_on_team_id (team_id) +# index_conversations_on_uuid (uuid) UNIQUE +# index_conversations_on_waiting_since (waiting_since) # class Conversation < ApplicationRecord @@ -59,6 +61,7 @@ class Conversation < ApplicationRecord include SortHandler include PushDataHelper include ConversationMuteHelpers + include Events::Types validates :account_id, presence: true validates :inbox_id, presence: true @@ -68,6 +71,7 @@ class Conversation < ApplicationRecord validates :custom_attributes, jsonb_attributes_length: true validates :uuid, uniqueness: true validate :validate_referer_url + validate :validate_assignee_belongs_to_account enum status: { open: 0, resolved: 1, pending: 2, snoozed: 3 } enum priority: { low: 0, medium: 1, high: 2, urgent: 3 } @@ -96,7 +100,7 @@ class Conversation < ApplicationRecord belongs_to :account belongs_to :inbox - belongs_to :assignee, class_name: 'User', optional: true, inverse_of: :assigned_conversations + belongs_to :assignee, polymorphic: true, optional: true belongs_to :contact belongs_to :contact_inbox belongs_to :team, optional: true @@ -195,6 +199,14 @@ class Conversation < ApplicationRecord dispatcher_dispatch(CONVERSATION_UPDATED, previous_changes) end + def assigned_to_bot? + assignee_type == 'AgentBot' + end + + def assigned_bot + assignee if assigned_to_bot? + end + private def execute_after_update_commit_callbacks @@ -273,7 +285,8 @@ class Conversation < ApplicationRecord CONVERSATION_RESOLVED => -> { saved_change_to_status? && resolved? }, CONVERSATION_STATUS_CHANGED => -> { saved_change_to_status? }, CONVERSATION_READ => -> { saved_change_to_contact_last_seen_at? }, - CONVERSATION_CONTACT_CHANGED => -> { saved_change_to_contact_id? } + CONVERSATION_CONTACT_CHANGED => -> { saved_change_to_contact_id? }, + ASSIGNEE_CHANGED => -> { saved_change_to_assignee_id? } }.each do |event, condition| condition.call && dispatcher_dispatch(event, status_change) end @@ -309,6 +322,17 @@ class Conversation < ApplicationRecord self['additional_attributes']['referer'] = nil unless url_valid?(additional_attributes['referer']) end + def validate_assignee_belongs_to_account + return unless assignee + + case assignee + when User + errors.add(:assignee, 'must belong to the same account') unless assignee.accounts.include?(account) + when AgentBot + errors.add(:assignee, 'must belong to the same account') unless assignee.account_id == account_id || assignee.system_bot? + end + end + # creating db triggers trigger.before(:insert).for_each(:row) do "NEW.display_id := nextval('conv_dpid_seq_' || NEW.account_id);" diff --git a/app/models/user.rb b/app/models/user.rb index d1907362a..78b45951d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -74,7 +74,7 @@ class User < ApplicationRecord has_many :accounts, through: :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, as: :assignee, class_name: 'Conversation', dependent: :nullify alias_attribute :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 diff --git a/db/migrate/20250714205206_make_conversation_assignee_polymorphic.rb b/db/migrate/20250714205206_make_conversation_assignee_polymorphic.rb new file mode 100644 index 000000000..310074561 --- /dev/null +++ b/db/migrate/20250714205206_make_conversation_assignee_polymorphic.rb @@ -0,0 +1,13 @@ +class MakeConversationAssigneePolymorphic < ActiveRecord::Migration[7.1] + def change + add_column :conversations, :assignee_type, :string + add_index :conversations, [:assignee_type, :assignee_id] + + # Update existing records to use User type + reversible do |dir| + dir.up do + execute "UPDATE conversations SET assignee_type = 'User' WHERE assignee_id IS NOT NULL" + end + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 837eb58bd..93726de94 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2025_07_10_145708) do +ActiveRecord::Schema[7.1].define(version: 2025_07_14_205206) do # These extensions should be enabled to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -585,11 +585,13 @@ ActiveRecord::Schema[7.1].define(version: 2025_07_10_145708) do t.bigint "sla_policy_id" t.datetime "waiting_since" t.text "cached_label_list" + t.string "assignee_type" t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true t.index ["account_id", "id"], name: "index_conversations_on_id_and_account_id" t.index ["account_id", "inbox_id", "status", "assignee_id"], name: "conv_acid_inbid_stat_asgnid_idx" t.index ["account_id"], name: "index_conversations_on_account_id" t.index ["assignee_id", "account_id"], name: "index_conversations_on_assignee_id_and_account_id" + t.index ["assignee_type", "assignee_id"], name: "index_conversations_on_assignee_type_and_assignee_id" t.index ["campaign_id"], name: "index_conversations_on_campaign_id" t.index ["contact_id"], name: "index_conversations_on_contact_id" t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id" diff --git a/spec/models/concerns/assignment_handler_shared.rb b/spec/models/concerns/assignment_handler_shared.rb index 8447b8146..0f3d11fc1 100644 --- a/spec/models/concerns/assignment_handler_shared.rb +++ b/spec/models/concerns/assignment_handler_shared.rb @@ -4,10 +4,15 @@ require 'rails_helper' shared_examples_for 'assignment_handler' do describe '#update_team' do - let(:conversation) { create(:conversation, assignee: create(:user)) } + let(:conversation) { create(:conversation) } + let(:assignee) { create(:user, account: conversation.account, role: :agent) } let(:agent) do create(:user, email: 'agent@example.com', account: conversation.account, role: :agent, auto_offline: false) end + + before do + conversation.update!(assignee: assignee) + end let(:team) do create(:team, account: conversation.account, allow_auto_assign: false) end