feat(captain): add inactivity timer backend (2/5) (#15303)
Captain V2 assistants can now persist a configurable inactivity timer and choose whether inactivity resolution sends the saved closing message or resolves silently. This PR contains only the API, persistence, runtime behavior, and backend specs. ## Closes [AI-163](https://linear.app/chatwoot/issue/AI-163) ## Depends on Stack 2 of 5. Based on the assistant-policy foundation in #15299. The frontend follows in #15308. ## What changed - Added per-assistant inactivity duration and resolution-message settings with safe defaults. - Restricted the Part 2 settings API to Captain V2 while keeping the Part 1 policy mode available without V2. - Updated inactivity handling to use the assistant timer and skip the public resolution message when disabled. - Serialized the effective timer and message settings for the frontend. - Added model, request, and job coverage, including the explicit Captain V2 boundary. ## How to test 1. Enable Captain V2 and update `auto_resolve_after` and `send_inactivity_resolution_message` through the assistant API. 2. Run the inactivity job and confirm it uses the assistant timer. 3. Disable the resolution message and confirm the conversation resolves silently. 4. Disable Captain V2 and confirm timer/message updates are ignored while `auto_resolve_mode` remains updateable. --------- Co-authored-by: Sony Mathew <sony@chatwoot.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com>
This commit is contained in:
@@ -255,6 +255,61 @@ RSpec.describe 'Api::V1::Accounts::Captain::Assistants', type: :request do
|
||||
expect(assistant.reload.config).to include('product_name' => 'Chatwoot', 'auto_resolve_mode' => 'disabled')
|
||||
end
|
||||
|
||||
it 'keeps inactivity timer settings behind Captain V2' do
|
||||
account.disable_features!('captain_integration_v2')
|
||||
assistant.update!(
|
||||
config: {
|
||||
'auto_resolve_mode' => 'evaluated',
|
||||
'auto_resolve_after' => 60,
|
||||
'send_inactivity_resolution_message' => true
|
||||
}
|
||||
)
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}",
|
||||
params: {
|
||||
assistant: {
|
||||
config: {
|
||||
auto_resolve_mode: 'disabled',
|
||||
auto_resolve_after: 90,
|
||||
send_inactivity_resolution_message: false
|
||||
}
|
||||
}
|
||||
},
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(assistant.reload.config).to include(
|
||||
'auto_resolve_mode' => 'disabled',
|
||||
'auto_resolve_after' => 60,
|
||||
'send_inactivity_resolution_message' => true
|
||||
)
|
||||
end
|
||||
|
||||
it 'updates inactive conversation settings for Captain v2' do
|
||||
account.enable_features!('captain_integration_v2')
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}",
|
||||
params: {
|
||||
assistant: {
|
||||
config: {
|
||||
auto_resolve_after: 61,
|
||||
send_inactivity_resolution_message: false,
|
||||
resolution_message: 'Saved closing message'
|
||||
}
|
||||
}
|
||||
},
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(json_response[:config]).to include(
|
||||
auto_resolve_after: 60,
|
||||
send_inactivity_resolution_message: false,
|
||||
resolution_message: 'Saved closing message'
|
||||
)
|
||||
end
|
||||
|
||||
it 'persists the nested audience condition tree' do
|
||||
create(:custom_attribute_definition, account: account, attribute_model: :contact_attribute,
|
||||
attribute_display_type: :text, attribute_key: 'plan_tier')
|
||||
|
||||
@@ -78,6 +78,25 @@ RSpec.describe Captain::InboxPendingConversationsResolutionJob, type: :job do
|
||||
|
||||
expect(Captain::ConversationCompletionService).not_to have_received(:new)
|
||||
end
|
||||
|
||||
it 'uses the assistant inactivity timer' do
|
||||
captain_assistant.account.enable_features!('captain_integration_v2')
|
||||
captain_assistant.update!(config: captain_assistant.config.merge('auto_resolve_after' => 180))
|
||||
|
||||
described_class.perform_now(inbox)
|
||||
|
||||
expect(resolvable_pending_conversation.reload.status).to eq('pending')
|
||||
end
|
||||
|
||||
it 'resolves silently when the resolution message is disabled' do
|
||||
captain_assistant.account.enable_features!('captain_integration_v2')
|
||||
captain_assistant.update!(config: captain_assistant.config.merge('send_inactivity_resolution_message' => false))
|
||||
|
||||
described_class.perform_now(inbox)
|
||||
|
||||
expect(resolvable_pending_conversation.reload.status).to eq('resolved')
|
||||
expect(resolvable_pending_conversation.messages.outgoing).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when captain_tasks is enabled' do
|
||||
|
||||
@@ -6,6 +6,47 @@ RSpec.describe Captain::Assistant, type: :model do
|
||||
let(:contact) { create(:contact, account: account, additional_attributes: { 'country_code' => 'US' }) }
|
||||
let(:conversation) { create(:conversation, account: account, contact: contact) }
|
||||
|
||||
describe 'inactive conversation settings' do
|
||||
it 'uses safe defaults when settings are unavailable' do
|
||||
assistant.account.enable_features('captain_integration_v2')
|
||||
assistant.auto_resolve_after = nil
|
||||
|
||||
expect(assistant.inactivity_threshold_minutes).to eq(60)
|
||||
|
||||
assistant.auto_resolve_after = 5
|
||||
assistant.send_inactivity_resolution_message = false
|
||||
assistant.account.disable_features('captain_integration_v2')
|
||||
|
||||
expect(assistant.inactivity_threshold_minutes).to eq(60)
|
||||
expect(assistant.send_inactivity_resolution_message?).to be(true)
|
||||
end
|
||||
|
||||
it 'validates the inactivity timer range' do
|
||||
assistant.auto_resolve_after = 4
|
||||
|
||||
expect(assistant).not_to be_valid
|
||||
expect(assistant.errors[:auto_resolve_after]).to be_present
|
||||
|
||||
assistant.auto_resolve_after = 61.5
|
||||
|
||||
expect(assistant).not_to be_valid
|
||||
expect(assistant.errors[:auto_resolve_after]).to be_present
|
||||
end
|
||||
|
||||
it 'rounds the inactivity timer to the nearest five minutes' do
|
||||
assistant.auto_resolve_after = 61
|
||||
|
||||
assistant.validate
|
||||
|
||||
expect(assistant.auto_resolve_after).to eq(60)
|
||||
|
||||
assistant.auto_resolve_after = 63
|
||||
assistant.validate
|
||||
|
||||
expect(assistant.auto_resolve_after).to eq(65)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#responds_to_audience?' do
|
||||
it 'returns true when no audience is configured' do
|
||||
expect(assistant.responds_to_audience?(contact, conversation)).to be(true)
|
||||
|
||||
Reference in New Issue
Block a user