Merge branch 'develop' into fix/email_auth_issue

This commit is contained in:
Tanmay Deep Sharma
2026-02-24 20:32:42 +05:30
committed by GitHub
282 changed files with 7944 additions and 6713 deletions
@@ -72,13 +72,17 @@ class AutoAssignment::AssignmentService
end
def assign_conversation(conversation, agent)
Current.executed_by = inbox.assignment_policy || inbox
conversation.update!(assignee: agent)
Current.executed_by = nil
rate_limiter = build_rate_limiter(agent)
rate_limiter.track_assignment(conversation)
dispatch_assignment_event(conversation, agent)
true
ensure
Current.executed_by = nil
end
def dispatch_assignment_event(conversation, agent)
+2 -10
View File
@@ -2,8 +2,6 @@ class AutoAssignment::RateLimiter
pattr_initialize [:inbox!, :agent!]
def within_limit?
return true unless enabled?
current_count < limit
end
@@ -13,24 +11,18 @@ class AutoAssignment::RateLimiter
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
config&.fair_distribution_limit.present? ? config.fair_distribution_limit.to_i : 5
end
def window
config&.fair_distribution_window&.to_i || 24.hours.to_i
config&.fair_distribution_window&.to_i || 5.minutes.to_i
end
def config
@@ -110,6 +110,7 @@ class CsatTemplateManagementService
template_service = Twilio::CsatTemplateService.new(@inbox.channel)
status_result = template_service.get_template_status(content_sid)
return { template_exists: false, error: 'Template not found' } unless status_result.is_a?(Hash)
if status_result[:success]
{
@@ -130,6 +131,7 @@ class CsatTemplateManagementService
def get_whatsapp_template_status(template)
template_name = template['name'] || CsatTemplateNameService.csat_template_name(@inbox.id)
status_result = Whatsapp::CsatTemplateService.new(@inbox.channel).get_template_status(template_name)
return { template_exists: false, error: 'Template not found' } unless status_result.is_a?(Hash)
if status_result[:success]
{
@@ -0,0 +1,86 @@
class CsatTemplateUtilityAnalysisService
include CsatTemplateUtilityRubric
pattr_initialize [:account!, :inbox!, :message!, { button_text: nil, language: 'en' }]
def perform
baseline = rule_based_result
return baseline if baseline[:classification] == 'LIKELY_MARKETING'
llm_result = llm_result_or_nil(baseline)
llm_result || baseline
end
private
def llm_result_or_nil(baseline)
llm_output = Captain::CsatUtilityAnalysisService.new(
account: account,
message: message,
button_text: button_text,
language: language,
baseline: baseline
).perform
return nil if llm_output[:error]
normalize_llm_result(llm_output, baseline: baseline)
rescue StandardError => e
Rails.logger.error("CSAT utility LLM analysis failed for inbox #{inbox.id}: #{e.message}")
nil
end
def normalize_llm_result(result, baseline:)
classification = normalized_classification(result[:classification], baseline: baseline)
optimized_message = result[:optimized_message].presence || baseline[:optimized_message]
optimized_message = baseline[:optimized_message] if baseline[:classification] == 'LIKELY_MARKETING'
{
classification: classification,
optimized_message: optimized_message
}
end
def normalized_classification(value, baseline:)
raw = value.to_s
return 'LIKELY_MARKETING' if baseline[:classification] == 'LIKELY_MARKETING'
raw
end
def rule_based_result
text = sanitized_message
marketing_hits_count = MARKETING_PATTERNS.count { |pattern| pattern.match?(text) }
utility_hits_count = UTILITY_PATTERNS.count { |pattern| pattern.match?(text) }
criteria = evaluate_criteria(text: text, marketing_hits_count: marketing_hits_count)
classification = classify(criteria: criteria, utility_hits_count: utility_hits_count)
build_rule_payload(
classification: classification
)
end
def build_rule_payload(payload)
{
classification: payload[:classification],
optimized_message: optimized_message_for(payload[:classification])
}
end
def sanitized_message
message.to_s.squish
end
def classify(criteria:, utility_hits_count:)
return 'LIKELY_MARKETING' unless criteria[:marketing_prohibition]
return 'LIKELY_MARKETING' unless criteria[:prohibited_content]
return 'LIKELY_UTILITY' if criteria.values.all? && utility_hits_count >= 2
'UNCLEAR'
end
def optimized_message_for(classification)
return sanitized_message if classification == 'LIKELY_UTILITY'
build_input_aware_utility_message
end
end
@@ -0,0 +1,125 @@
# rubocop:disable Metrics/ModuleLength
module CsatTemplateUtilityRubric
LANGUAGE_FALLBACKS = {
'en' => {
support_request: 'support request',
support_ticket: 'support ticket',
support_conversation: 'support conversation',
status_closed: 'closed',
status_resolved: 'resolved',
status_completed: 'completed',
line_status: 'Your %<subject>s has been %<status>s.',
line_help: 'If you still need help, simply reply to this message.',
line_rate: 'To rate this support interaction, please use the button below.'
}
}.freeze
MARKETING_PATTERNS = [
/\b(discounts?|offers?|promos?|promotions?|deals?|sales?|buy|shop|subscribe)\b/i,
/\b(limited\s*time|don't\s*miss|exclusive|special\s*offer)\b/i,
/\b(click\s*(here|below)\s*to\s*(buy|get|shop))\b/i,
/\b(new\s*(plans?|products?|services?))\b/i
].freeze
TRANSACTION_TRIGGER_PATTERNS = [
/\b(closed|closing|resolved|completed)\b/i,
/\b(ticket|request|case|conversation|support)\b/i
].freeze
TRANSACTIONAL_CONTENT_PATTERNS = [
/\b(ticket|request|case|conversation)\b/i,
/\b(reply\s+to\s+this\s+message|if\s+you\s+still\s+need\s+help)\b/i,
/\b(rate|califica|calificar)\b/i
].freeze
PROHIBITED_CONTENT_PATTERNS = [
/\b(contest|sweepstake|lottery|quiz)\b/i,
/\b(password|otp|pin|cvv|credit\s*card)\b/i,
/\b(weapon|drugs|gambling)\b/i
].freeze
STATUS_PATTERNS = {
'closed' => /\b(closed|closing)\b/i,
'resolved' => /\b(resolved|resolve[sd]?)\b/i,
'completed' => /\b(completed|complete[sd]?)\b/i
}.freeze
SUBJECT_PATTERNS = {
'support ticket' => /\b(ticket)\b/i,
'support conversation' => /\b(conversation|chat)\b/i,
'support request' => /\b(request|case|support)\b/i
}.freeze
UTILITY_PATTERNS = [
/\b(support|ticket|request|conversation|case)\b/i,
/\b(closed|resolved|completed)\b/i,
/\b(reply\s+to\s+this\s+message\b)/i,
/\b(if\s+you\s+still\s+need\s+help)\b/i,
/\b(rate\s+this\s+(support|interaction|conversation))\b/i
].freeze
private
def build_input_aware_utility_message
text = translation_pack
subject = detected_subject
status = detected_status
intro = extracted_intro_sentence
parts = []
parts << intro if intro.present?
parts << format(text[:line_status], subject: subject, status: status)
parts << text[:line_help]
parts << text[:line_rate]
parts.join(' ')
end
def detected_status
matched = STATUS_PATTERNS.find { |_key, pattern| pattern.match?(sanitized_message) }
status_key = matched&.first || 'closed'
translation_pack[:"status_#{status_key}"]
end
def detected_subject
matched = SUBJECT_PATTERNS.find { |_key, pattern| pattern.match?(sanitized_message) }
subject_key = matched&.first&.tr(' ', '_') || 'support_request'
translation_pack[subject_key.to_sym]
end
def extracted_intro_sentence
first_sentence = sanitized_message.split(/(?<=[.!?])\s+/).first.to_s
return nil if first_sentence.blank?
return nil if MARKETING_PATTERNS.any? { |pattern| pattern.match?(first_sentence) }
return nil unless first_sentence.match?(/\b(thanks|thank you|hello|hi)\b/i)
normalized = first_sentence.gsub(/\s+/, ' ').strip
normalized.ends_with?('.', '!', '?') ? normalized : "#{normalized}."
end
def translation_pack
LANGUAGE_FALLBACKS.fetch(primary_language_code, LANGUAGE_FALLBACKS['en'])
end
def primary_language_code
language.to_s.downcase.split(/[-_]/).first
end
def evaluate_criteria(text:, marketing_hits_count:)
{
trigger: TRANSACTION_TRIGGER_PATTERNS.any? { |pattern| pattern.match?(text) },
transactional_content: TRANSACTIONAL_CONTENT_PATTERNS.count { |pattern| pattern.match?(text) } >= 2,
marketing_prohibition: marketing_hits_count.zero?,
prohibited_content: PROHIBITED_CONTENT_PATTERNS.none? { |pattern| pattern.match?(text) },
clarity_and_utility: clear_utility_intent?(text)
}
end
def clear_utility_intent?(text)
has_support_context = text.match?(/\b(support|ticket|request|case|conversation)\b/i)
has_actionable_next_step = text.match?(/\b(reply\s+to\s+this\s+message)\b/i) ||
text.match?(/\b(if\s+you\s+still\s+need\s+help)\b/i) ||
text.match?(/\b(rate\s+this\s+(support|interaction|conversation))\b/i)
has_support_context && has_actionable_next_step
end
end
# rubocop:enable Metrics/ModuleLength
@@ -2,7 +2,7 @@ class Whatsapp::CsatTemplateService
DEFAULT_BUTTON_TEXT = 'Please rate us'.freeze
DEFAULT_LANGUAGE = 'en'.freeze
WHATSAPP_API_VERSION = 'v14.0'.freeze
TEMPLATE_CATEGORY = 'MARKETING'.freeze
TEMPLATE_CATEGORY = 'UTILITY'.freeze
TEMPLATE_STATUS_PENDING = 'PENDING'.freeze
def initialize(whatsapp_channel)