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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:')
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user