Restructure omni services and add Chatwoot research snapshot

This commit is contained in:
Ruslan Bakiev
2026-02-21 11:11:27 +07:00
parent edea7a0034
commit b73babbbf6
7732 changed files with 978203 additions and 32 deletions

View File

@@ -0,0 +1,38 @@
class AutoAssignment::AgentAssignmentService
# Allowed agent ids: array
# This is the list of agents from which an agent can be assigned to this conversation
# examples: Agents with assignment capacity, Agents who are members of a team etc
pattr_initialize [:conversation!, :allowed_agent_ids!]
def find_assignee
round_robin_manage_service.available_agent(allowed_agent_ids: allowed_online_agent_ids)
end
def perform
new_assignee = find_assignee
conversation.update(assignee: new_assignee) if new_assignee
end
private
def online_agent_ids
online_agents = OnlineStatusTracker.get_available_users(conversation.account_id)
online_agents.select { |_key, value| value.eql?('online') }.keys if online_agents.present?
end
def allowed_online_agent_ids
# We want to perform roundrobin only over online agents
# Hence taking an intersection of online agents and allowed member ids
# the online user ids are string, since its from redis, allowed member ids are integer, since its from active record
@allowed_online_agent_ids ||= online_agent_ids & allowed_agent_ids&.map(&:to_s)
end
def round_robin_manage_service
@round_robin_manage_service ||= AutoAssignment::InboxRoundRobinService.new(inbox: conversation.inbox)
end
def round_robin_key
format(::Redis::Alfred::ROUND_ROBIN_AGENTS, inbox_id: conversation.inbox_id)
end
end

View File

@@ -0,0 +1,102 @@
class AutoAssignment::AssignmentService
pattr_initialize [:inbox!]
def perform_bulk_assignment(limit: 100)
return 0 unless inbox.auto_assignment_v2_enabled?
return 0 unless inbox.enable_auto_assignment?
assigned_count = 0
unassigned_conversations(limit).each do |conversation|
assigned_count += 1 if perform_for_conversation(conversation)
end
assigned_count
end
private
def perform_for_conversation(conversation)
return false unless assignable?(conversation)
agent = find_available_agent(conversation)
return false unless agent
assign_conversation(conversation, agent)
end
def assignable?(conversation)
conversation.status == 'open' &&
conversation.assignee_id.nil?
end
def unassigned_conversations(limit)
scope = inbox.conversations.unassigned.open
# Apply conversation priority using assignment policy if available
policy = inbox.assignment_policy
scope = if policy&.longest_waiting?
scope.reorder(last_activity_at: :asc, created_at: :asc)
else
scope.reorder(created_at: :asc)
end
scope.limit(limit)
end
def find_available_agent(conversation = nil)
agents = filter_agents_by_team(inbox.available_agents, conversation)
return nil if agents.nil?
agents = filter_agents_by_rate_limit(agents)
return nil if agents.empty?
round_robin_selector.select_agent(agents)
end
def filter_agents_by_team(agents, conversation)
return agents if conversation&.team_id.blank?
team = conversation.team
return nil if team.blank? || team.allow_auto_assign.blank?
team_member_ids = team.members.ids
agents.where(user_id: team_member_ids)
end
def filter_agents_by_rate_limit(agents)
agents.select do |agent_member|
rate_limiter = build_rate_limiter(agent_member.user)
rate_limiter.within_limit?
end
end
def assign_conversation(conversation, agent)
conversation.update!(assignee: agent)
rate_limiter = build_rate_limiter(agent)
rate_limiter.track_assignment(conversation)
dispatch_assignment_event(conversation, agent)
true
end
def dispatch_assignment_event(conversation, agent)
Rails.configuration.dispatcher.dispatch(
Events::Types::ASSIGNEE_CHANGED,
Time.zone.now,
conversation: conversation,
user: agent
)
end
def build_rate_limiter(agent)
AutoAssignment::RateLimiter.new(inbox: inbox, agent: agent)
end
def round_robin_selector
@round_robin_selector ||= AutoAssignment::RoundRobinSelector.new(inbox: inbox)
end
end
AutoAssignment::AssignmentService.prepend_mod_with('AutoAssignment::AssignmentService')

View File

@@ -0,0 +1,62 @@
class AutoAssignment::InboxRoundRobinService
pattr_initialize [:inbox!]
# called on inbox delete
def clear_queue
::Redis::Alfred.delete(round_robin_key)
end
# called on inbox member create
def add_agent_to_queue(user_id)
::Redis::Alfred.lpush(round_robin_key, user_id)
end
# called on inbox member delete
def remove_agent_from_queue(user_id)
::Redis::Alfred.lrem(round_robin_key, user_id)
end
def reset_queue
clear_queue
add_agent_to_queue(inbox.inbox_members.map(&:user_id))
end
# end of queue management functions
# allowed member ids = [assignable online agents supplied by the assignment service]
# the values of allowed member ids should be in string format
def available_agent(allowed_agent_ids: [])
reset_queue unless validate_queue?
user_id = get_member_from_allowed_agent_ids(allowed_agent_ids)
inbox.inbox_members.find_by(user_id: user_id)&.user if user_id.present?
end
private
def get_member_from_allowed_agent_ids(allowed_agent_ids)
return nil if allowed_agent_ids.blank?
user_id = queue.intersection(allowed_agent_ids).pop
pop_push_to_queue(user_id)
user_id
end
def pop_push_to_queue(user_id)
return if user_id.blank?
remove_agent_from_queue(user_id)
add_agent_to_queue(user_id)
end
def validate_queue?
return true if inbox.inbox_members.map(&:user_id).sort == queue.map(&:to_i).sort
end
def queue
::Redis::Alfred.lrange(round_robin_key)
end
def round_robin_key
format(::Redis::Alfred::ROUND_ROBIN_AGENTS, inbox_id: inbox.id)
end
end

View File

@@ -0,0 +1,47 @@
class AutoAssignment::RateLimiter
pattr_initialize [:inbox!, :agent!]
def within_limit?
return true unless enabled?
current_count < limit
end
def track_assignment(conversation)
assignment_key = build_assignment_key(conversation.id)
Redis::Alfred.set(assignment_key, conversation.id.to_s, ex: window)
end
def current_count
return 0 unless enabled?
pattern = assignment_key_pattern
Redis::Alfred.keys_count(pattern)
end
private
def enabled?
config.present? && limit.positive?
end
def limit
config&.fair_distribution_limit.present? ? config.fair_distribution_limit.to_i : Float::INFINITY
end
def window
config&.fair_distribution_window&.to_i || 24.hours.to_i
end
def config
@config ||= inbox.assignment_policy
end
def assignment_key_pattern
format(Redis::RedisKeys::ASSIGNMENT_KEY_PATTERN, inbox_id: inbox.id, agent_id: agent.id)
end
def build_assignment_key(conversation_id)
format(Redis::RedisKeys::ASSIGNMENT_KEY, inbox_id: inbox.id, agent_id: agent.id, conversation_id: conversation_id)
end
end

View File

@@ -0,0 +1,16 @@
class AutoAssignment::RoundRobinSelector
pattr_initialize [:inbox!]
def select_agent(available_agents)
return nil if available_agents.empty?
agent_user_ids = available_agents.map(&:user_id).map(&:to_s)
round_robin_service.available_agent(allowed_agent_ids: agent_user_ids)
end
private
def round_robin_service
@round_robin_service ||= AutoAssignment::InboxRoundRobinService.new(inbox: inbox)
end
end