Compare commits

...
Author SHA1 Message Date
Sojan Jose 2750321830 refactor: tidy connected bot ownership flow 2026-07-22 17:06:52 -07:00
Sojan Jose af6389bdd8 refactor: clarify conversation open paths 2026-07-22 16:40:11 -07:00
Sojan Jose bb37face88 Merge remote-tracking branch 'origin/develop' into codex/connected-agent-bot-owner 2026-07-22 15:35:08 -07:00
Sojan Jose 078c85e067 Merge remote-tracking branch 'origin/codex/connected-agent-bot-owner' into codex/connected-agent-bot-owner 2026-07-22 15:34:58 -07:00
Sojan Jose e19799d473 fix: clear bot owner when humans open conversations 2026-07-22 15:34:52 -07:00
Sojan JoseandGitHub f2e2d24ce6 Merge branch 'develop' into codex/connected-agent-bot-owner 2026-07-22 07:51:29 -07:00
Sojan Jose c1ef2c3039 Merge remote-tracking branch 'origin/develop' into codex/connected-agent-bot-owner 2026-07-20 18:21:59 -07:00
Sojan Jose 26b8650f92 Revert "fix: classify bot-owned conversations as assigned"
This reverts commit 365935c440.
2026-07-20 18:16:01 -07:00
Sojan Jose 365935c440 fix: classify bot-owned conversations as assigned 2026-07-07 19:52:11 -07:00
Sojan Jose d96415c814 fix: clear bot owner on handoff 2026-07-07 19:21:19 -07:00
Sojan JoseandGitHub 4f237add73 Merge branch 'develop' into codex/connected-agent-bot-owner 2026-07-07 19:18:29 -07:00
Sojan Jose 7bde6f23b9 fix: clear human assignee for connected bot owner 2026-07-06 22:52:37 -07:00
Sojan Jose 99d4ad8f99 refactor: trim active bot conversation setup 2026-07-06 22:31:12 -07:00
Sojan JoseandGitHub 68f941df30 Merge branch 'develop' into codex/connected-agent-bot-owner 2026-07-06 22:27:40 -07:00
Sojan Jose 73054c6a94 refactor: clarify connected agent bot conversation setup 2026-07-06 22:25:59 -07:00
Sojan Jose 3ace6f01eb fix(deps): refresh audited lockfile gems 2026-07-06 22:12:53 -07:00
Sojan Jose 5f70fd4454 fix: preserve explicit assignee for bot inbox conversations 2026-06-28 20:46:51 -07:00
Sojan Jose f5a7d78180 feat: assign connected agent bot owner 2026-06-28 19:47:23 -07:00
4 changed files with 49 additions and 16 deletions
@@ -80,7 +80,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def toggle_status def toggle_status
# FIXME: move this logic into a service object # FIXME: move this logic into a service object
if pending_to_open_by_bot? if bot_handoff?
@conversation.bot_handoff! @conversation.bot_handoff!
elsif params[:status].present? elsif params[:status].present?
set_conversation_status set_conversation_status
@@ -88,19 +88,15 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
else else
@status = @conversation.toggle_status @status = @conversation.toggle_status
end end
assign_conversation if should_assign_conversation? handle_human_open if @conversation.open? && Current.user.is_a?(User)
end end
def pending_to_open_by_bot? def bot_handoff?
return false unless Current.user.is_a?(AgentBot) return false unless Current.user.is_a?(AgentBot)
@conversation.status == 'pending' && params[:status] == 'open' @conversation.status == 'pending' && params[:status] == 'open'
end end
def should_assign_conversation?
@conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
end
def toggle_priority def toggle_priority
@conversation.toggle_priority(params[:priority]) @conversation.toggle_priority(params[:priority])
head :ok head :ok
@@ -183,8 +179,9 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until] @conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
end end
def assign_conversation def handle_human_open
@conversation.assignee = current_user @conversation.assignee_agent_bot = nil
@conversation.assignee = Current.user if Current.user.agent?
@conversation.save! @conversation.save!
end end
+11 -4
View File
@@ -173,6 +173,7 @@ class Conversation < ApplicationRecord
def bot_handoff! def bot_handoff!
update(waiting_since: Time.current) if waiting_since.blank? update(waiting_since: Time.current) if waiting_since.blank?
self.assignee_agent_bot = nil
open! open!
dispatcher_dispatch(CONVERSATION_BOT_HANDOFF) dispatcher_dispatch(CONVERSATION_BOT_HANDOFF)
end end
@@ -291,13 +292,19 @@ class Conversation < ApplicationRecord
return handle_campaign_status if campaign.present? return handle_campaign_status if campaign.present?
# TODO: make this an inbox config instead of assuming bot conversations should start as pending set_active_bot_conversation if inbox.active_bot?
self.status = :pending if inbox.active_bot?
end end
def handle_campaign_status def handle_campaign_status
# If campaign has no sender (bot-initiated) and inbox has active bot, let bot handle it set_active_bot_conversation if campaign.sender_id.nil? && inbox.active_bot?
self.status = :pending if campaign.sender_id.nil? && inbox.active_bot? end
def set_active_bot_conversation
# TODO: make this an inbox config instead of assuming bot conversations should start as pending
self.status = :pending
return unless inbox.agent_bot_inbox&.active? && assignee_id.blank?
self.assignee_agent_bot = inbox.agent_bot
end end
def notify_conversation_creation def notify_conversation_creation
@@ -565,15 +565,17 @@ RSpec.describe 'Conversations API', type: :request do
expect(conversation.reload.assignee_id).to eq(agent.id) expect(conversation.reload.assignee_id).to eq(agent.id)
end end
it 'disbale self assign if admin changes the conversation status to open' do it 'does not self assign and clears the agent bot owner if admin changes the conversation status to open' do
conversation.update!(status: 'pending') conversation.update!(status: 'pending', assignee: nil, assignee_agent_bot: agent_bot)
conversation.update!(assignee_id: nil)
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status", post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
headers: administrator.create_new_auth_token, headers: administrator.create_new_auth_token,
as: :json as: :json
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
expect(conversation.reload.status).to eq('open') expect(conversation.reload.status).to eq('open')
expect(conversation.reload.assignee_id).not_to eq(administrator.id) expect(conversation.reload.assignee_id).not_to eq(administrator.id)
expect(conversation.reload.assignee_agent_bot).to be_nil
end end
it 'toggles the conversation status to specific status when parameter is passed' do it 'toggles the conversation status to specific status when parameter is passed' do
+27
View File
@@ -408,6 +408,14 @@ RSpec.describe Conversation do
expect(conversation.reload.status).to eq('open') expect(conversation.reload.status).to eq('open')
end end
it 'clears agent bot ownership' do
conversation.update!(assignee_agent_bot: create(:agent_bot, account: conversation.account))
conversation.bot_handoff!
expect(conversation.reload.assignee_agent_bot).to be_nil
end
it 'dispatches CONVERSATION_BOT_HANDOFF event' do it 'dispatches CONVERSATION_BOT_HANDOFF event' do
expect(Rails.configuration.dispatcher).to receive(:dispatch) expect(Rails.configuration.dispatcher).to receive(:dispatch)
.with(described_class::CONVERSATION_BOT_HANDOFF, anything, hash_including(conversation: conversation)) .with(described_class::CONVERSATION_BOT_HANDOFF, anything, hash_including(conversation: conversation))
@@ -719,6 +727,19 @@ RSpec.describe Conversation do
expect(conversation.status).to eq('pending') expect(conversation.status).to eq('pending')
end end
it 'sets connected agent bot as the conversation owner' do
expect(conversation.assignee_agent_bot).to eq(bot_inbox.agent_bot)
expect(conversation.assignee).to be_nil
end
it 'preserves explicit human assignee' do
agent = create(:user, account: bot_inbox.inbox.account)
conversation = create(:conversation, inbox: bot_inbox.inbox, assignee: agent)
expect(conversation.assignee).to eq(agent)
expect(conversation.assignee_agent_bot).to be_nil
end
context 'with campaigns' do context 'with campaigns' do
let(:user) { create(:user, account: bot_inbox.inbox.account) } let(:user) { create(:user, account: bot_inbox.inbox.account) }
@@ -726,12 +747,14 @@ RSpec.describe Conversation do
campaign = create(:campaign, inbox: bot_inbox.inbox, account: bot_inbox.inbox.account, sender: user) campaign = create(:campaign, inbox: bot_inbox.inbox, account: bot_inbox.inbox.account, sender: user)
conversation = create(:conversation, inbox: bot_inbox.inbox, campaign: campaign) conversation = create(:conversation, inbox: bot_inbox.inbox, campaign: campaign)
expect(conversation.status).to eq('open') expect(conversation.status).to eq('open')
expect(conversation.assignee_agent_bot).to be_nil
end end
it 'returns conversation as pending if campaign has no sender (bot-initiated) and bot is active' do it 'returns conversation as pending if campaign has no sender (bot-initiated) and bot is active' do
campaign = create(:campaign, inbox: bot_inbox.inbox, account: bot_inbox.inbox.account, sender: nil) campaign = create(:campaign, inbox: bot_inbox.inbox, account: bot_inbox.inbox.account, sender: nil)
conversation = create(:conversation, inbox: bot_inbox.inbox, campaign: campaign) conversation = create(:conversation, inbox: bot_inbox.inbox, campaign: campaign)
expect(conversation.status).to eq('pending') expect(conversation.status).to eq('pending')
expect(conversation.assignee_agent_bot).to eq(bot_inbox.agent_bot)
end end
end end
@@ -762,6 +785,10 @@ RSpec.describe Conversation do
it 'returns conversation status as pending' do it 'returns conversation status as pending' do
expect(conversation.status).to eq('pending') expect(conversation.status).to eq('pending')
end end
it 'does not set agent bot ownership' do
expect(conversation.assignee_agent_bot).to be_nil
end
end end
describe '#delete conversation' do describe '#delete conversation' do