From 91d8a4e2a3568466de2c59d38629e49b8cb18b91 Mon Sep 17 00:00:00 2001 From: Sony Mathew Date: Thu, 25 Jun 2026 17:01:05 +0530 Subject: [PATCH] feat: Route reply box LLM tasks (2/6) (#14840) # Pull Request Template ## Description Routes OSS reply-box and small Captain tasks through feature-specific LLM model resolution. Rewrite, reply suggestion, summary, follow-up, and CSAT utility analysis now resolve through the `editor` feature; label suggestion resolves through `label_suggestion`. Existing credentials, account OpenAI hook behavior, and instrumentation event names remain unchanged. Linear: https://linear.app/chatwoot/issue/CW-7425/test-new-models Depends on #14839 ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? - `bundle exec rspec spec/lib/captain/base_task_service_spec.rb spec/lib/captain/rewrite_service_spec.rb spec/lib/captain/reply_suggestion_service_spec.rb spec/lib/captain/summary_service_spec.rb spec/lib/captain/label_suggestion_service_spec.rb spec/lib/captain/csat_utility_analysis_service_spec.rb spec/lib/captain/follow_up_service_spec.rb` passed with 81 examples, 0 failures. - `bundle exec rubocop lib/captain/base_task_service.rb lib/captain/rewrite_service.rb lib/captain/reply_suggestion_service.rb lib/captain/summary_service.rb lib/captain/label_suggestion_service.rb lib/captain/csat_utility_analysis_service.rb lib/captain/follow_up_service.rb enterprise/lib/enterprise/captain/reply_suggestion_service.rb spec/lib/captain/base_task_service_spec.rb spec/lib/captain/rewrite_service_spec.rb spec/lib/captain/reply_suggestion_service_spec.rb spec/lib/captain/summary_service_spec.rb spec/lib/captain/label_suggestion_service_spec.rb spec/lib/captain/csat_utility_analysis_service_spec.rb spec/lib/captain/follow_up_service_spec.rb` passed with no offenses. - `git diff --check` passed. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- .../enterprise/captain/reply_suggestion_service.rb | 4 ++-- lib/captain/base_task_service.rb | 9 ++++++++- lib/captain/csat_utility_analysis_service.rb | 2 +- lib/captain/follow_up_service.rb | 2 +- lib/captain/label_suggestion_service.rb | 2 +- lib/captain/reply_suggestion_service.rb | 2 +- lib/captain/rewrite_service.rb | 2 +- lib/captain/summary_service.rb | 2 +- spec/lib/captain/base_task_service_spec.rb | 11 +++++++++++ .../lib/captain/csat_utility_analysis_service_spec.rb | 8 ++++++++ spec/lib/captain/follow_up_service_spec.rb | 1 + spec/lib/captain/label_suggestion_service_spec.rb | 1 + spec/lib/captain/reply_suggestion_service_spec.rb | 6 ++++++ spec/lib/captain/rewrite_service_spec.rb | 2 ++ spec/lib/captain/summary_service_spec.rb | 4 ++-- 15 files changed, 47 insertions(+), 11 deletions(-) diff --git a/enterprise/lib/enterprise/captain/reply_suggestion_service.rb b/enterprise/lib/enterprise/captain/reply_suggestion_service.rb index 503dd095a..31f52ff1e 100644 --- a/enterprise/lib/enterprise/captain/reply_suggestion_service.rb +++ b/enterprise/lib/enterprise/captain/reply_suggestion_service.rb @@ -1,8 +1,8 @@ module Enterprise::Captain::ReplySuggestionService - def make_api_call(model:, messages:, tools: []) + def make_api_call(messages:, model: nil, feature: nil, schema: nil, tools: []) return super unless use_search_tool? - super(model: model, messages: messages, tools: [build_search_tool]) + super(messages: messages, model: model, feature: feature, schema: schema, tools: [build_search_tool]) end private diff --git a/lib/captain/base_task_service.rb b/lib/captain/base_task_service.rb index d382204a5..d098b24af 100644 --- a/lib/captain/base_task_service.rb +++ b/lib/captain/base_task_service.rb @@ -37,12 +37,13 @@ class Captain::BaseTaskService "#{endpoint}/v1" end - def make_api_call(model:, messages:, schema: nil, tools: []) + def make_api_call(messages:, model: nil, feature: nil, schema: nil, tools: []) # Community edition prerequisite checks # Enterprise module handles these with more specific error messages (cloud vs self-hosted) return { error: I18n.t('captain.disabled'), error_code: 403 } unless captain_tasks_enabled? return { error: I18n.t('captain.api_key_missing'), error_code: 401 } unless api_key_configured? + model = resolved_model(model: model, feature: feature) instrumentation_params = build_instrumentation_params(model, messages) instrumentation_method = tools.any? ? :instrument_tool_session : :instrument_llm_call @@ -55,6 +56,12 @@ class Captain::BaseTaskService response.merge(follow_up_context: build_follow_up_context(messages, response)) end + def resolved_model(model:, feature:) + return model if feature.blank? + + Llm::FeatureRouter.resolve(feature: feature, account: account)[:model] + end + def execute_ruby_llm_request(model:, messages:, schema: nil, tools: []) credential = llm_credential diff --git a/lib/captain/csat_utility_analysis_service.rb b/lib/captain/csat_utility_analysis_service.rb index 7aab18e6c..a29c52a1c 100644 --- a/lib/captain/csat_utility_analysis_service.rb +++ b/lib/captain/csat_utility_analysis_service.rb @@ -3,7 +3,7 @@ class Captain::CsatUtilityAnalysisService < Captain::BaseTaskService def perform api_response = make_api_call( - model: GPT_MODEL, + feature: 'editor', messages: [ { role: 'system', content: system_prompt }, { role: 'user', content: message } diff --git a/lib/captain/follow_up_service.rb b/lib/captain/follow_up_service.rb index c4c1225be..60b8e63b2 100644 --- a/lib/captain/follow_up_service.rb +++ b/lib/captain/follow_up_service.rb @@ -33,7 +33,7 @@ class Captain::FollowUpService < Captain::BaseTaskService { role: 'user', content: user_message } ] - response = make_api_call(model: GPT_MODEL, messages: messages) + response = make_api_call(feature: 'editor', messages: messages) return response if response[:error] response.merge(follow_up_context: update_follow_up_context(user_message, response[:message])) diff --git a/lib/captain/label_suggestion_service.rb b/lib/captain/label_suggestion_service.rb index a0e030963..6487fdca4 100644 --- a/lib/captain/label_suggestion_service.rb +++ b/lib/captain/label_suggestion_service.rb @@ -12,7 +12,7 @@ class Captain::LabelSuggestionService < Captain::BaseTaskService # Make API call response = make_api_call( - model: GPT_MODEL, # TODO: Use separate model for label suggestion + feature: 'label_suggestion', messages: [ { role: 'system', content: prompt_from_file('label_suggestion') }, { role: 'user', content: content } diff --git a/lib/captain/reply_suggestion_service.rb b/lib/captain/reply_suggestion_service.rb index 039bdcf26..7af014879 100644 --- a/lib/captain/reply_suggestion_service.rb +++ b/lib/captain/reply_suggestion_service.rb @@ -3,7 +3,7 @@ class Captain::ReplySuggestionService < Captain::BaseTaskService def perform make_api_call( - model: GPT_MODEL, + feature: 'editor', messages: [ { role: 'system', content: system_prompt }, { role: 'user', content: formatted_conversation } diff --git a/lib/captain/rewrite_service.rb b/lib/captain/rewrite_service.rb index 6f880e775..0d16613c1 100644 --- a/lib/captain/rewrite_service.rb +++ b/lib/captain/rewrite_service.rb @@ -36,7 +36,7 @@ class Captain::RewriteService < Captain::BaseTaskService def call_llm_with_prompt(system_content, user_content = content) make_api_call( - model: GPT_MODEL, + feature: 'editor', messages: [ { role: 'system', content: system_content }, { role: 'user', content: user_content } diff --git a/lib/captain/summary_service.rb b/lib/captain/summary_service.rb index f06aa42ca..fad60c0f5 100644 --- a/lib/captain/summary_service.rb +++ b/lib/captain/summary_service.rb @@ -3,7 +3,7 @@ class Captain::SummaryService < Captain::BaseTaskService def perform make_api_call( - model: GPT_MODEL, + feature: 'editor', messages: [ { role: 'system', content: system_prompt }, { role: 'user', content: conversation.to_llm_text(include_contact_details: false) } diff --git a/spec/lib/captain/base_task_service_spec.rb b/spec/lib/captain/base_task_service_spec.rb index 34c889967..7b67ab94a 100644 --- a/spec/lib/captain/base_task_service_spec.rb +++ b/spec/lib/captain/base_task_service_spec.rb @@ -167,6 +167,17 @@ RSpec.describe Captain::BaseTaskService do service.send(:make_api_call, model: model, messages: messages) end + it 'uses the resolved feature model for the request and instrumentation' do + account.update!(captain_models: { 'editor' => 'gpt-4.1' }) + + expect(mock_context).to receive(:chat).with(model: 'gpt-4.1').and_return(mock_chat) + expect(service).to receive(:instrument_llm_call).with( + hash_including(model: 'gpt-4.1', feature_name: 'test_event') + ).and_call_original + + service.send(:make_api_call, feature: 'editor', messages: messages) + end + it 'returns formatted response with tokens' do result = service.send(:make_api_call, model: model, messages: messages) diff --git a/spec/lib/captain/csat_utility_analysis_service_spec.rb b/spec/lib/captain/csat_utility_analysis_service_spec.rb index 34e0c9ece..70c07cdb2 100644 --- a/spec/lib/captain/csat_utility_analysis_service_spec.rb +++ b/spec/lib/captain/csat_utility_analysis_service_spec.rb @@ -25,6 +25,14 @@ RSpec.describe Captain::CsatUtilityAnalysisService do expect(result[:optimized_message]).to eq('Utility-safe message') expect(result[:message]).to eq('{"classification":"LIKELY_UTILITY","optimized_message":"Utility-safe message"}') end + + it 'routes through the editor feature' do + expect(service).to receive(:make_api_call).with( + hash_including(feature: 'editor') + ).and_return({ message: '{"classification":"LIKELY_UTILITY"}' }) + + service.perform + end end describe '#api_key' do diff --git a/spec/lib/captain/follow_up_service_spec.rb b/spec/lib/captain/follow_up_service_spec.rb index 9e330efdc..45535d574 100644 --- a/spec/lib/captain/follow_up_service_spec.rb +++ b/spec/lib/captain/follow_up_service_spec.rb @@ -42,6 +42,7 @@ RSpec.describe Captain::FollowUpService do context 'when follow-up context exists' do it 'constructs messages array with full conversation history' do expect(service).to receive(:make_api_call) do |args| + expect(args[:feature]).to eq('editor') messages = args[:messages] expect(messages).to match( diff --git a/spec/lib/captain/label_suggestion_service_spec.rb b/spec/lib/captain/label_suggestion_service_spec.rb index 0c40b103c..c8d9ed6c7 100644 --- a/spec/lib/captain/label_suggestion_service_spec.rb +++ b/spec/lib/captain/label_suggestion_service_spec.rb @@ -58,6 +58,7 @@ RSpec.describe Captain::LabelSuggestionService do it 'builds labels_with_messages format correctly' do expect(service).to receive(:make_api_call) do |args| + expect(args[:feature]).to eq('label_suggestion') user_message = args[:messages].find { |m| m[:role] == 'user' }[:content] expect(user_message).to include('Messages:') diff --git a/spec/lib/captain/reply_suggestion_service_spec.rb b/spec/lib/captain/reply_suggestion_service_spec.rb index a53825ee4..608db43a2 100644 --- a/spec/lib/captain/reply_suggestion_service_spec.rb +++ b/spec/lib/captain/reply_suggestion_service_spec.rb @@ -30,6 +30,12 @@ RSpec.describe Captain::ReplySuggestionService do end describe '#perform' do + it 'routes through the editor feature' do + expect(Llm::FeatureRouter).to receive(:resolve).with(feature: 'editor', account: account).and_call_original + + service.perform + end + it 'returns the suggested reply' do result = service.perform diff --git a/spec/lib/captain/rewrite_service_spec.rb b/spec/lib/captain/rewrite_service_spec.rb index 3c1d7997a..e4ef7efbf 100644 --- a/spec/lib/captain/rewrite_service_spec.rb +++ b/spec/lib/captain/rewrite_service_spec.rb @@ -29,6 +29,7 @@ RSpec.describe Captain::RewriteService do expect(service).to receive(:prompt_from_file).with('fix_spelling_grammar').and_return('Fix errors') expect(service).to receive(:make_api_call) do |args| + expect(args[:feature]).to eq('editor') expect(args[:messages][0][:content]).to eq('Fix errors') expect(args[:messages][1][:content]).to eq(content) { message: 'Fixed' } @@ -122,6 +123,7 @@ RSpec.describe Captain::RewriteService do it 'uses conversation context and draft message with Liquid template' do expect(service).to receive(:make_api_call) do |args| + expect(args[:feature]).to eq('editor') system_content = args[:messages][0][:content] expect(system_content).to include('Context:') diff --git a/spec/lib/captain/summary_service_spec.rb b/spec/lib/captain/summary_service_spec.rb index c5ec50687..def6daefe 100644 --- a/spec/lib/captain/summary_service_spec.rb +++ b/spec/lib/captain/summary_service_spec.rb @@ -21,9 +21,9 @@ RSpec.describe Captain::SummaryService do end describe '#perform' do - it 'passes correct model to API' do + it 'routes through the editor feature' do expect(service).to receive(:make_api_call).with( - hash_including(model: Captain::BaseTaskService::GPT_MODEL) + hash_including(feature: 'editor') ).and_call_original service.perform