chore: Use Round Robin service for team assignment (#4237)

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Sojan Jose
2022-03-28 14:38:07 +05:30
committed by GitHub
parent e8bc30e3c6
commit 823c0ab6a7
6 changed files with 86 additions and 22 deletions

View File

@@ -110,7 +110,7 @@ jobs:
- run: - run:
name: Run backend tests name: Run backend tests
command: | command: |
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10 bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10 --format documentation
~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json ~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
- persist_to_workspace: - persist_to_workspace:
root: ~/tmp root: ~/tmp

View File

@@ -12,18 +12,19 @@ module AssignmentHandler
def ensure_assignee_is_from_team def ensure_assignee_is_from_team
return unless team_id_changed? return unless team_id_changed?
ensure_current_assignee_team validate_current_assignee_team
self.assignee_id ||= find_team_assignee_id_for_inbox if team&.allow_auto_assign.present? self.assignee ||= find_assignee_from_team
end end
def ensure_current_assignee_team def validate_current_assignee_team
self.assignee_id = nil if team&.members&.exclude?(assignee) self.assignee_id = nil if team&.members&.exclude?(assignee)
end end
def find_team_assignee_id_for_inbox def find_assignee_from_team
members = inbox.members.ids & team.members.ids return if team&.allow_auto_assign.blank?
# TODO: User round robin to determine the next agent instead of using sample
members.sample team_members = inbox.members.ids & team.members.ids
::RoundRobin::AssignmentService.new(conversation: self, allowed_member_ids: team_members).find_assignee
end end
def notify_assignment_change def notify_assignment_change

View File

@@ -1,9 +1,13 @@
class RoundRobin::AssignmentService class RoundRobin::AssignmentService
pattr_initialize [:conversation] pattr_initialize [:conversation, { allowed_member_ids: [] }]
def find_assignee
round_robin_manage_service.available_agent(priority_list: online_agents)
end
def perform def perform
# online agents will get priority # online agents will get priority
new_assignee = round_robin_manage_service.available_agent(priority_list: online_agents) new_assignee = find_assignee
conversation.update(assignee: new_assignee) if new_assignee conversation.update(assignee: new_assignee) if new_assignee
end end
@@ -15,7 +19,7 @@ class RoundRobin::AssignmentService
end end
def round_robin_manage_service def round_robin_manage_service
@round_robin_manage_service ||= RoundRobin::ManageService.new(inbox: conversation.inbox) @round_robin_manage_service ||= RoundRobin::ManageService.new(inbox: conversation.inbox, allowed_member_ids: allowed_member_ids)
end end
def round_robin_key def round_robin_key

View File

@@ -1,5 +1,7 @@
# If allowed_member_ids are supplied round robin service will only fetch a member from member id
# This is used in case of team assignment
class RoundRobin::ManageService class RoundRobin::ManageService
pattr_initialize [:inbox!] pattr_initialize [:inbox!, { allowed_member_ids: [] }]
# called on inbox delete # called on inbox delete
def clear_queue def clear_queue
@@ -18,9 +20,9 @@ class RoundRobin::ManageService
def available_agent(priority_list: []) def available_agent(priority_list: [])
reset_queue unless validate_queue? reset_queue unless validate_queue?
user_id = get_agent_via_priority_list(priority_list) user_id = get_member_via_priority_list(priority_list)
# incase priority list was empty or inbox members weren't present # incase priority list was empty or inbox members weren't present
user_id ||= ::Redis::Alfred.rpoplpush(round_robin_key, round_robin_key) user_id ||= fetch_user_id
inbox.inbox_members.find_by(user_id: user_id)&.user if user_id.present? inbox.inbox_members.find_by(user_id: user_id)&.user if user_id.present?
end end
@@ -31,17 +33,36 @@ class RoundRobin::ManageService
private private
def get_agent_via_priority_list(priority_list) def fetch_user_id
if allowed_member_ids_in_str.present?
user_id = queue.intersection(allowed_member_ids_in_str).pop
pop_push_to_queue(user_id)
user_id
else
::Redis::Alfred.rpoplpush(round_robin_key, round_robin_key)
end
end
# priority list is usually the members who are online passed from assignmebt service
def get_member_via_priority_list(priority_list)
return if priority_list.blank?
# when allowed member ids is passed we will be looking to get members from that list alone
priority_list = priority_list.intersection(allowed_member_ids_in_str) if allowed_member_ids_in_str.present?
return if priority_list.blank? return if priority_list.blank?
user_id = queue.intersection(priority_list.map(&:to_s)).pop user_id = queue.intersection(priority_list.map(&:to_s)).pop
if user_id.present? pop_push_to_queue(user_id)
remove_agent_from_queue(user_id)
add_agent_to_queue(user_id)
end
user_id user_id
end 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? def validate_queue?
return true if inbox.inbox_members.map(&:user_id).sort == queue.map(&:to_i).sort return true if inbox.inbox_members.map(&:user_id).sort == queue.map(&:to_i).sort
end end
@@ -53,4 +74,9 @@ class RoundRobin::ManageService
def round_robin_key def round_robin_key
format(::Redis::Alfred::ROUND_ROBIN_AGENTS, inbox_id: inbox.id) format(::Redis::Alfred::ROUND_ROBIN_AGENTS, inbox_id: inbox.id)
end end
def allowed_member_ids_in_str
# NOTE: the values which are returned from redis for priority list are string
@allowed_member_ids_in_str ||= allowed_member_ids.map(&:to_s)
end
end end

View File

@@ -35,6 +35,9 @@ RSpec.describe 'Conversation Assignment API', type: :request do
end end
it 'assigns a team to the conversation' do it 'assigns a team to the conversation' do
team_member = create(:user, account: account, role: :agent)
create(:inbox_member, inbox: conversation.inbox, user: team_member)
create(:team_member, team: team, user: team_member)
params = { team_id: team.id } params = { team_id: team.id }
post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id), post api_v1_account_conversation_assignments_url(account_id: account.id, conversation_id: conversation.display_id),
@@ -44,6 +47,8 @@ RSpec.describe 'Conversation Assignment API', type: :request do
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
expect(conversation.reload.team).to eq(team) expect(conversation.reload.team).to eq(team)
# assignee will be from team
expect(conversation.reload.assignee).to eq(team_member)
end end
end end

View File

@@ -1,7 +1,7 @@
require 'rails_helper' require 'rails_helper'
describe RoundRobin::ManageService do describe RoundRobin::ManageService do
subject(:round_robin_manage_service) { ::RoundRobin::ManageService.new(inbox: inbox) } subject(:round_robin_manage_service) { described_class.new(inbox: inbox) }
let!(:account) { create(:account) } let!(:account) { create(:account) }
let!(:inbox) { create(:inbox, account: account) } let!(:inbox) { create(:inbox, account: account) }
@@ -18,8 +18,9 @@ describe RoundRobin::ManageService do
it 'gets intersection of priority list and agent queue. get and move agent to the end of the list' do it 'gets intersection of priority list and agent queue. get and move agent to the end of the list' do
expected_queue = [inbox_members[2].user_id, inbox_members[4].user_id, inbox_members[3].user_id, inbox_members[1].user_id, expected_queue = [inbox_members[2].user_id, inbox_members[4].user_id, inbox_members[3].user_id, inbox_members[1].user_id,
inbox_members[0].user_id].map(&:to_s) inbox_members[0].user_id].map(&:to_s)
expect(round_robin_manage_service.available_agent(priority_list: [inbox_members[3].user_id, # prority list will be ids in string, since thats what redis supplies to us
inbox_members[2].user_id])).to eq inbox_members[2].user expect(round_robin_manage_service.available_agent(priority_list: [inbox_members[3].user_id.to_s,
inbox_members[2].user_id.to_s])).to eq inbox_members[2].user
expect(round_robin_manage_service.send(:queue)).to eq(expected_queue) expect(round_robin_manage_service.send(:queue)).to eq(expected_queue)
end end
@@ -39,5 +40,32 @@ describe RoundRobin::ManageService do
# the service have refreshed the redis queue before performing # the service have refreshed the redis queue before performing
expect(round_robin_manage_service.send(:queue).sort.map(&:to_i)).to eq(inbox_members.map(&:user_id).sort) expect(round_robin_manage_service.send(:queue).sort.map(&:to_i)).to eq(inbox_members.map(&:user_id).sort)
end end
context 'when allowed_member_ids is passed' do
it 'will get the first allowed member and move it to the end of the queue' do
expected_queue = [inbox_members[3].user_id, inbox_members[2].user_id, inbox_members[4].user_id, inbox_members[1].user_id,
inbox_members[0].user_id].map(&:to_s)
expect(described_class.new(inbox: inbox,
allowed_member_ids: [inbox_members[3].user_id,
inbox_members[2].user_id]).available_agent).to eq inbox_members[2].user
expect(described_class.new(inbox: inbox,
allowed_member_ids: [inbox_members[3].user_id,
inbox_members[2].user_id]).available_agent).to eq inbox_members[3].user
expect(round_robin_manage_service.send(:queue)).to eq(expected_queue)
end
it 'will get union of priority list and allowed_member_ids and move it to the end of the queue' do
expected_queue = [inbox_members[3].user_id, inbox_members[4].user_id, inbox_members[2].user_id, inbox_members[1].user_id,
inbox_members[0].user_id].map(&:to_s)
# prority list will be ids in string, since thats what redis supplies to us
expect(described_class.new(inbox: inbox,
allowed_member_ids: [inbox_members[3].user_id,
inbox_members[2].user_id])
.available_agent(
priority_list: [inbox_members[3].user_id.to_s]
)).to eq inbox_members[3].user
expect(round_robin_manage_service.send(:queue)).to eq(expected_queue)
end
end
end end
end end