Compare commits

...
Author SHA1 Message Date
Pranav e4dba113ce Agentbot as assignee 2025-07-14 16:49:12 -07:00
8 changed files with 95 additions and 30 deletions
@@ -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
+12
View File
@@ -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)
+1
View File
@@ -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 }
+42 -18
View File
@@ -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);"
+1 -1
View File
@@ -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
@@ -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
+3 -1
View File
@@ -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"
@@ -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