Merge branch 'develop' into feat/rollup/1-model-write-path
This commit is contained in:
@@ -19,7 +19,7 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||
contact = @contact
|
||||
end
|
||||
|
||||
@contact_inbox.update(hmac_verified: true) if should_verify_hmac? && valid_hmac?
|
||||
@contact_inbox.update(hmac_verified: true) if should_verify_hmac?
|
||||
|
||||
identify_contact(contact)
|
||||
end
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ const onPortalCreate = ({ slug: portalSlug, locale }) => {
|
||||
<EmptyStateLayout
|
||||
:title="$t('HELP_CENTER.TITLE')"
|
||||
:subtitle="$t('HELP_CENTER.NEW_PAGE.DESCRIPTION')"
|
||||
class="bg-n-surface-1"
|
||||
>
|
||||
<template #empty-state-item>
|
||||
<div class="grid grid-cols-2 gap-4 p-px">
|
||||
|
||||
@@ -41,7 +41,7 @@ class Account < ApplicationRecord
|
||||
'audio_transcriptions': { 'type': %w[boolean null] },
|
||||
'auto_resolve_label': { 'type': %w[string null] },
|
||||
'keep_pending_on_bot_failure': { 'type': %w[boolean null] },
|
||||
'captain_disable_auto_resolve': { 'type': %w[boolean null] },
|
||||
'captain_auto_resolve_mode': { 'type': %w[string null], 'enum': ['evaluated', 'legacy', 'disabled', nil] },
|
||||
'conversation_required_attributes': {
|
||||
'type': %w[array null],
|
||||
'items': { 'type': 'string' }
|
||||
@@ -93,7 +93,8 @@ class Account < ApplicationRecord
|
||||
store_accessor :settings, :captain_models, :captain_features
|
||||
store_accessor :settings, :reporting_timezone
|
||||
store_accessor :settings, :keep_pending_on_bot_failure
|
||||
store_accessor :settings, :captain_disable_auto_resolve
|
||||
store_accessor :settings, :captain_auto_resolve_mode
|
||||
include AccountCaptainAutoResolve
|
||||
|
||||
has_many :account_users, dependent: :destroy_async
|
||||
has_many :agent_bot_inboxes, dependent: :destroy_async
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
module AccountCaptainAutoResolve
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
VALID_CAPTAIN_AUTO_RESOLVE_MODES = %w[evaluated legacy disabled].freeze
|
||||
|
||||
included do
|
||||
VALID_CAPTAIN_AUTO_RESOLVE_MODES.each do |mode|
|
||||
define_method("captain_auto_resolve_#{mode}?") do
|
||||
captain_auto_resolve_mode == mode
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def captain_auto_resolve_mode
|
||||
mode = settings&.[]('captain_auto_resolve_mode')
|
||||
return mode if VALID_CAPTAIN_AUTO_RESOLVE_MODES.include?(mode)
|
||||
return 'disabled' if settings&.[]('captain_disable_auto_resolve') == true
|
||||
|
||||
feature_enabled?('captain_tasks') ? 'evaluated' : 'legacy'
|
||||
end
|
||||
end
|
||||
@@ -104,11 +104,11 @@ wistia:
|
||||
</div>
|
||||
|
||||
bunny:
|
||||
regex: 'https?://iframe\.mediadelivery\.net/play/(?<library_id>\d+)/(?<video_id>[^&/?]+)'
|
||||
regex: 'https?://(?:iframe|player)\.mediadelivery\.net/(?:play|embed)/(?<library_id>\d+)/(?<video_id>[^&/?]+)'
|
||||
template: |
|
||||
<div style="position: relative; padding-top: 56.25%;">
|
||||
<iframe
|
||||
src="https://iframe.mediadelivery.net/embed/%{library_id}/%{video_id}?autoplay=false&loop=false&muted=false&preload=true&responsive=true"
|
||||
src="https://player.mediadelivery.net/embed/%{library_id}/%{video_id}?autoplay=false&loop=false&muted=false&preload=true&responsive=true"
|
||||
title="Bunny video player"
|
||||
loading="lazy"
|
||||
style="border: 0; position: absolute; top: 0; height: 100%; width: 100%;"
|
||||
|
||||
@@ -5,9 +5,9 @@ class Captain::InboxPendingConversationsResolutionJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
def perform(inbox)
|
||||
return if inbox.account.captain_disable_auto_resolve
|
||||
return if inbox.account.captain_auto_resolve_disabled?
|
||||
|
||||
if inbox.account.feature_enabled?('captain_tasks')
|
||||
if evaluate_conversation_completion?(inbox.account)
|
||||
perform_with_evaluation(inbox)
|
||||
else
|
||||
perform_time_based(inbox)
|
||||
@@ -18,6 +18,10 @@ class Captain::InboxPendingConversationsResolutionJob < ApplicationJob
|
||||
|
||||
private
|
||||
|
||||
def evaluate_conversation_completion?(account)
|
||||
account.feature_enabled?('captain_tasks') && account.captain_auto_resolve_evaluated?
|
||||
end
|
||||
|
||||
def perform_time_based(inbox)
|
||||
Current.executed_by = inbox.captain_assistant
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ module Enterprise::Account::ConversationsResolutionSchedulerJob
|
||||
inbox = captain_inbox.inbox
|
||||
|
||||
next if inbox.email?
|
||||
next if inbox.account.captain_disable_auto_resolve
|
||||
next if inbox.account.captain_auto_resolve_disabled?
|
||||
|
||||
Captain::InboxPendingConversationsResolutionJob.perform_later(
|
||||
inbox
|
||||
|
||||
@@ -6,7 +6,7 @@ class Captain::Tools::ResolveConversationTool < Captain::Tools::BasePublicTool
|
||||
conversation = find_conversation(tool_context.state)
|
||||
return 'Conversation not found' unless conversation
|
||||
return "Conversation ##{conversation.display_id} is already resolved" if conversation.resolved?
|
||||
return 'Auto-resolve is disabled for this account' if conversation.account.captain_disable_auto_resolve
|
||||
return 'Auto-resolve is disabled for this account' if conversation.account.captain_auto_resolve_disabled?
|
||||
|
||||
log_tool_usage('resolve_conversation', { conversation_id: conversation.id, reason: reason })
|
||||
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@
|
||||
"json-logic-js": "^2.0.5",
|
||||
"lettersanitizer": "^1.0.6",
|
||||
"libphonenumber-js": "^1.11.9",
|
||||
"markdown-it": "^13.0.2",
|
||||
"markdown-it": "^14.1.1",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"md5": "^2.3.0",
|
||||
"mitt": "^3.0.1",
|
||||
|
||||
Generated
+6
-31
@@ -160,8 +160,8 @@ importers:
|
||||
specifier: ^1.11.9
|
||||
version: 1.11.9
|
||||
markdown-it:
|
||||
specifier: ^13.0.2
|
||||
version: 13.0.2
|
||||
specifier: ^14.1.1
|
||||
version: 14.1.1
|
||||
markdown-it-link-attributes:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
@@ -2206,10 +2206,6 @@ packages:
|
||||
entities@2.1.0:
|
||||
resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==}
|
||||
|
||||
entities@3.0.1:
|
||||
resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
@@ -3087,9 +3083,6 @@ packages:
|
||||
linkify-it@3.0.3:
|
||||
resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==}
|
||||
|
||||
linkify-it@4.0.1:
|
||||
resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==}
|
||||
|
||||
linkify-it@5.0.0:
|
||||
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||
|
||||
@@ -3210,12 +3203,8 @@ packages:
|
||||
resolution: {integrity: sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==}
|
||||
hasBin: true
|
||||
|
||||
markdown-it@13.0.2:
|
||||
resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==}
|
||||
hasBin: true
|
||||
|
||||
markdown-it@14.1.0:
|
||||
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
||||
markdown-it@14.1.1:
|
||||
resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==}
|
||||
hasBin: true
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
@@ -6841,8 +6830,6 @@ snapshots:
|
||||
|
||||
entities@2.1.0: {}
|
||||
|
||||
entities@3.0.1: {}
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
entities@6.0.1: {}
|
||||
@@ -7950,10 +7937,6 @@ snapshots:
|
||||
dependencies:
|
||||
uc.micro: 1.0.6
|
||||
|
||||
linkify-it@4.0.1:
|
||||
dependencies:
|
||||
uc.micro: 1.0.6
|
||||
|
||||
linkify-it@5.0.0:
|
||||
dependencies:
|
||||
uc.micro: 2.1.0
|
||||
@@ -8091,15 +8074,7 @@ snapshots:
|
||||
mdurl: 1.0.1
|
||||
uc.micro: 1.0.6
|
||||
|
||||
markdown-it@13.0.2:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
entities: 3.0.1
|
||||
linkify-it: 4.0.1
|
||||
mdurl: 1.0.1
|
||||
uc.micro: 1.0.6
|
||||
|
||||
markdown-it@14.1.0:
|
||||
markdown-it@14.1.1:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
entities: 4.5.0
|
||||
@@ -8738,7 +8713,7 @@ snapshots:
|
||||
|
||||
prosemirror-markdown@1.13.0:
|
||||
dependencies:
|
||||
markdown-it: 14.1.0
|
||||
markdown-it: 14.1.1
|
||||
prosemirror-model: 1.22.3
|
||||
|
||||
prosemirror-menu@1.2.4:
|
||||
|
||||
@@ -63,7 +63,11 @@ describe 'Markdown Embeds Configuration' do
|
||||
'bunny' => [
|
||||
{ url: 'https://iframe.mediadelivery.net/play/431789/1f105841-cad9-46fe-a70e-b7623c60797c',
|
||||
expected: { 'library_id' => '431789', 'video_id' => '1f105841-cad9-46fe-a70e-b7623c60797c' } },
|
||||
{ url: 'https://iframe.mediadelivery.net/play/12345/abcdef-ghijkl', expected: { 'library_id' => '12345', 'video_id' => 'abcdef-ghijkl' } }
|
||||
{ url: 'https://iframe.mediadelivery.net/play/12345/abcdef-ghijkl', expected: { 'library_id' => '12345', 'video_id' => 'abcdef-ghijkl' } },
|
||||
{ url: 'https://player.mediadelivery.net/play/431789/1f105841-cad9-46fe-a70e-b7623c60797c',
|
||||
expected: { 'library_id' => '431789', 'video_id' => '1f105841-cad9-46fe-a70e-b7623c60797c' } },
|
||||
{ url: 'https://iframe.mediadelivery.net/embed/256380/d9d9ab1f-fc9f-4488-9c26-4ffc653c0024',
|
||||
expected: { 'library_id' => '256380', 'video_id' => 'd9d9ab1f-fc9f-4488-9c26-4ffc653c0024' } }
|
||||
],
|
||||
'codepen' => [
|
||||
{ url: 'https://codepen.io/username/pen/abcdef', expected: { 'user' => 'username', 'pen_id' => 'abcdef' } },
|
||||
|
||||
@@ -84,6 +84,16 @@ RSpec.describe Captain::InboxPendingConversationsResolutionJob, type: :job do
|
||||
expect(resolvable_pending_conversation.reload.status).to eq('pending')
|
||||
expect(resolvable_pending_conversation.messages.outgoing).to be_empty
|
||||
end
|
||||
|
||||
it 'falls back to legacy time-based resolve when legacy auto-resolve is forced' do
|
||||
inbox.account.update!(captain_auto_resolve_mode: 'legacy')
|
||||
allow(Captain::ConversationCompletionService).to receive(:new)
|
||||
|
||||
described_class.perform_now(inbox)
|
||||
|
||||
expect(Captain::ConversationCompletionService).not_to have_received(:new)
|
||||
expect(resolvable_pending_conversation.reload.status).to eq('resolved')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when LLM evaluation returns complete' do
|
||||
@@ -322,7 +332,7 @@ RSpec.describe Captain::InboxPendingConversationsResolutionJob, type: :job do
|
||||
end
|
||||
|
||||
it 'does not resolve conversations when auto-resolve is disabled at execution time' do
|
||||
inbox.account.update!(captain_disable_auto_resolve: true)
|
||||
inbox.account.update!(captain_auto_resolve_mode: 'disabled')
|
||||
|
||||
expect do
|
||||
described_class.perform_now(inbox)
|
||||
@@ -331,4 +341,14 @@ RSpec.describe Captain::InboxPendingConversationsResolutionJob, type: :job do
|
||||
expect(resolvable_pending_conversation.reload.status).to eq('pending')
|
||||
expect(resolvable_pending_conversation.messages.outgoing).to be_empty
|
||||
end
|
||||
|
||||
it 'falls back to disabled mode from legacy settings key' do
|
||||
inbox.account.update!(settings: inbox.account.settings.merge('captain_disable_auto_resolve' => true))
|
||||
|
||||
expect do
|
||||
described_class.perform_now(inbox)
|
||||
end.not_to(change { resolvable_pending_conversation.reload.status })
|
||||
|
||||
expect(resolvable_pending_conversation.reload.status).to eq('pending')
|
||||
end
|
||||
end
|
||||
|
||||
+18
-2
@@ -30,12 +30,28 @@ RSpec.describe Account::ConversationsResolutionSchedulerJob, type: :job do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account has captain_disable_auto_resolve enabled' do
|
||||
context 'when account has captain auto resolve disabled' do
|
||||
let!(:regular_inbox) { create(:inbox, account: account) }
|
||||
|
||||
before do
|
||||
create(:captain_inbox, captain_assistant: assistant, inbox: regular_inbox)
|
||||
account.update!(captain_disable_auto_resolve: true)
|
||||
account.update!(captain_auto_resolve_mode: 'disabled')
|
||||
end
|
||||
|
||||
it 'does not enqueue resolution jobs' do
|
||||
expect do
|
||||
described_class.perform_now
|
||||
end.not_to have_enqueued_job(Captain::InboxPendingConversationsResolutionJob)
|
||||
.with(regular_inbox)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account uses legacy disabled settings key' do
|
||||
let!(:regular_inbox) { create(:inbox, account: account) }
|
||||
|
||||
before do
|
||||
create(:captain_inbox, captain_assistant: assistant, inbox: regular_inbox)
|
||||
account.update!(settings: account.settings.merge('captain_disable_auto_resolve' => true))
|
||||
end
|
||||
|
||||
it 'does not enqueue resolution jobs' do
|
||||
|
||||
@@ -41,7 +41,18 @@ RSpec.describe Captain::Tools::ResolveConversationTool do
|
||||
end
|
||||
|
||||
describe 'when auto-resolve is disabled for the account' do
|
||||
before { account.update!(captain_disable_auto_resolve: true) }
|
||||
before { account.update!(captain_auto_resolve_mode: 'disabled') }
|
||||
|
||||
it 'does not resolve and returns a disabled message' do
|
||||
result = tool.perform(tool_context, reason: 'Possible spam')
|
||||
|
||||
expect(result).to eq('Auto-resolve is disabled for this account')
|
||||
expect(conversation.reload).not_to be_resolved
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when auto-resolve is disabled via legacy settings key' do
|
||||
before { account.update!(settings: account.settings.merge('captain_disable_auto_resolve' => true)) }
|
||||
|
||||
it 'does not resolve and returns a disabled message' do
|
||||
result = tool.perform(tool_context, reason: 'Possible spam')
|
||||
|
||||
@@ -184,12 +184,12 @@ describe CustomMarkdownRenderer do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when link is a Bunny.net URL' do
|
||||
context 'when link is a Bunny.net iframe URL' do
|
||||
let(:bunny_url) { 'https://iframe.mediadelivery.net/play/431789/1f105841-cad9-46fe-a70e-b7623c60797c' }
|
||||
|
||||
it 'renders an iframe with Bunny embed code' do
|
||||
output = render_markdown_link(bunny_url)
|
||||
expect(output).to include('src="https://iframe.mediadelivery.net/embed/431789/1f105841-cad9-46fe-a70e-b7623c60797c?autoplay=false&loop=false&muted=false&preload=true&responsive=true"')
|
||||
expect(output).to include('src="https://player.mediadelivery.net/embed/431789/1f105841-cad9-46fe-a70e-b7623c60797c?autoplay=false&loop=false&muted=false&preload=true&responsive=true"')
|
||||
expect(output).to include('allowfullscreen')
|
||||
expect(output).to include('allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"')
|
||||
end
|
||||
@@ -200,5 +200,17 @@ describe CustomMarkdownRenderer do
|
||||
expect(output).to include('position: absolute; top: 0; height: 100%; width: 100%;')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when link is a Bunny.net player URL' do
|
||||
let(:bunny_url) { 'https://player.mediadelivery.net/play/431789/1f105841-cad9-46fe-a70e-b7623c60797c' }
|
||||
|
||||
it 'renders an iframe with Bunny embed code' do
|
||||
output = render_markdown_link(bunny_url)
|
||||
expect(output).to include('embed/431789/1f105841-cad9-46fe-a70e-b7623c60797c')
|
||||
expect(output).to include('autoplay=false&loop=false&muted=false&preload=true&responsive=true')
|
||||
expect(output).to include('allowfullscreen')
|
||||
expect(output).to include('allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -198,6 +198,44 @@ RSpec.describe Account do
|
||||
expect(account.settings['auto_resolve_message']).to eq(message)
|
||||
end
|
||||
|
||||
it 'defaults captain_auto_resolve_mode to legacy when captain_tasks is disabled' do
|
||||
allow(account).to receive(:feature_enabled?).with('captain_tasks').and_return(false)
|
||||
|
||||
expect(account.captain_auto_resolve_mode).to eq('legacy')
|
||||
expect(account).to be_captain_auto_resolve_legacy
|
||||
end
|
||||
|
||||
it 'defaults captain_auto_resolve_mode to evaluated when captain_tasks is enabled' do
|
||||
allow(account).to receive(:feature_enabled?).with('captain_tasks').and_return(true)
|
||||
|
||||
expect(account.captain_auto_resolve_mode).to eq('evaluated')
|
||||
expect(account).to be_captain_auto_resolve_evaluated
|
||||
end
|
||||
|
||||
it 'correctly gets and sets captain_auto_resolve_mode' do
|
||||
account.captain_auto_resolve_mode = 'legacy'
|
||||
|
||||
expect(account.captain_auto_resolve_mode).to eq('legacy')
|
||||
expect(account.settings['captain_auto_resolve_mode']).to eq('legacy')
|
||||
expect(account).to be_captain_auto_resolve_legacy
|
||||
end
|
||||
|
||||
it 'allows clearing captain_auto_resolve_mode to fall back to feature defaults' do
|
||||
allow(account).to receive(:feature_enabled?).with('captain_tasks').and_return(false)
|
||||
account.captain_auto_resolve_mode = nil
|
||||
|
||||
expect(account).to be_valid
|
||||
expect(account.captain_auto_resolve_mode).to eq('legacy')
|
||||
expect(account.settings['captain_auto_resolve_mode']).to be_nil
|
||||
end
|
||||
|
||||
it 'falls back to disabled mode from legacy settings key' do
|
||||
account.settings = { 'captain_disable_auto_resolve' => true }
|
||||
|
||||
expect(account.captain_auto_resolve_mode).to eq('disabled')
|
||||
expect(account).to be_captain_auto_resolve_disabled
|
||||
end
|
||||
|
||||
it 'handles nil values correctly' do
|
||||
account.auto_resolve_after = nil
|
||||
account.auto_resolve_message = nil
|
||||
|
||||
Reference in New Issue
Block a user