Files
leadchat/app/services/csat_template_utility_analysis_service.rb
Muhsin Keloth 6be95e79f8 feat(csat): Add WhatsApp utility template analyzer with rewrite guidance (#13575)
CSAT templates for WhatsApp are submitted as Utility, but Meta may
reclassify them as Marketing based on content, which can significantly
increase messaging costs.
This PR introduces a Captain-powered CSAT template analyzer for
WhatsApp/Twilio WhatsApp that predicts utility fit, explains likely
risks, and suggests safer rewrites before submission. The flow is manual
(button-triggered), Captain-gated, and applies rewrites only on explicit
user action. It also updates UX copy to clearly set expectations: the
system submits as Utility, Meta makes the final categorization decision.

Fixes
https://linear.app/chatwoot/issue/CW-6424/ai-powered-whatsapp-template-classifier-for-csat-submissions


https://github.com/user-attachments/assets/8fd1d6db-2f91-447c-9771-3de271b16fd9
2026-02-24 15:11:04 +04:00

87 lines
2.5 KiB
Ruby

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