diff --git a/enterprise/app/helpers/captain/chat_helper.rb b/enterprise/app/helpers/captain/chat_helper.rb index 9bbbd86a5..12458a99c 100644 --- a/enterprise/app/helpers/captain/chat_helper.rb +++ b/enterprise/app/helpers/captain/chat_helper.rb @@ -20,7 +20,7 @@ module Captain::ChatHelper private def build_chat - llm_chat = chat(model: @model, temperature: temperature) + llm_chat = chat(model: model, temperature: temperature) llm_chat = llm_chat.with_params(response_format: { type: 'json_object' }) llm_chat = setup_tools(llm_chat) @@ -74,7 +74,7 @@ module Captain::ChatHelper account_id: resolved_account_id, conversation_id: @conversation_id, feature_name: feature_name, - model: @model, + model: model, messages: chat ? chat.messages.map { |m| { role: m.role.to_s, content: m.content.to_s } } : @messages, temperature: temperature, metadata: { diff --git a/enterprise/app/services/captain/llm/assistant_chat_service.rb b/enterprise/app/services/captain/llm/assistant_chat_service.rb index 5a2976e39..1184e98ce 100644 --- a/enterprise/app/services/captain/llm/assistant_chat_service.rb +++ b/enterprise/app/services/captain/llm/assistant_chat_service.rb @@ -45,4 +45,8 @@ class Captain::Llm::AssistantChatService < Llm::BaseAiService def feature_name 'assistant' end + + def account + @assistant&.account + end end diff --git a/enterprise/app/services/captain/llm/contact_attributes_service.rb b/enterprise/app/services/captain/llm/contact_attributes_service.rb index 803c06f09..1c7625810 100644 --- a/enterprise/app/services/captain/llm/contact_attributes_service.rb +++ b/enterprise/app/services/captain/llm/contact_attributes_service.rb @@ -33,8 +33,8 @@ class Captain::Llm::ContactAttributesService < Llm::BaseAiService def instrumentation_params { span_name: 'llm.captain.contact_attributes', - model: @model, - temperature: @temperature, + model: model, + temperature: temperature, account_id: @conversation.account_id, feature_name: 'contact_attributes', messages: [ diff --git a/enterprise/app/services/captain/llm/contact_notes_service.rb b/enterprise/app/services/captain/llm/contact_notes_service.rb index d37f9fea6..93d9ffddb 100644 --- a/enterprise/app/services/captain/llm/contact_notes_service.rb +++ b/enterprise/app/services/captain/llm/contact_notes_service.rb @@ -34,8 +34,8 @@ class Captain::Llm::ContactNotesService < Llm::BaseAiService def instrumentation_params { span_name: 'llm.captain.contact_notes', - model: @model, - temperature: @temperature, + model: model, + temperature: temperature, account_id: @conversation.account_id, feature_name: 'contact_notes', messages: [ diff --git a/enterprise/app/services/captain/llm/conversation_faq_service.rb b/enterprise/app/services/captain/llm/conversation_faq_service.rb index e9152bc99..b9b4832e7 100644 --- a/enterprise/app/services/captain/llm/conversation_faq_service.rb +++ b/enterprise/app/services/captain/llm/conversation_faq_service.rb @@ -97,8 +97,8 @@ class Captain::Llm::ConversationFaqService < Llm::BaseAiService def instrumentation_params { span_name: 'llm.captain.conversation_faq', - model: @model, - temperature: @temperature, + model: model, + temperature: temperature, account_id: @conversation.account_id, conversation_id: @conversation.id, feature_name: 'conversation_faq', diff --git a/enterprise/app/services/captain/llm/faq_generator_service.rb b/enterprise/app/services/captain/llm/faq_generator_service.rb index b22a631b3..435ebe477 100644 --- a/enterprise/app/services/captain/llm/faq_generator_service.rb +++ b/enterprise/app/services/captain/llm/faq_generator_service.rb @@ -33,8 +33,8 @@ class Captain::Llm::FaqGeneratorService < Llm::BaseAiService def instrumentation_params { span_name: 'llm.captain.faq_generator', - model: @model, - temperature: @temperature, + model: model, + temperature: temperature, feature_name: 'faq_generator', account_id: @account_id, messages: [ diff --git a/enterprise/app/services/captain/onboarding/website_analyzer_service.rb b/enterprise/app/services/captain/onboarding/website_analyzer_service.rb index a5879ba33..543a728ae 100644 --- a/enterprise/app/services/captain/onboarding/website_analyzer_service.rb +++ b/enterprise/app/services/captain/onboarding/website_analyzer_service.rb @@ -72,7 +72,7 @@ class Captain::Onboarding::WebsiteAnalyzerService < Llm::BaseAiService def instrumentation_params { span_name: 'llm.captain.website_analyzer', - model: @model, + model: model, temperature: 0.1, feature_name: 'website_analyzer', messages: [ diff --git a/enterprise/app/services/llm/base_ai_service.rb b/enterprise/app/services/llm/base_ai_service.rb index a5a91cf24..58c81453e 100644 --- a/enterprise/app/services/llm/base_ai_service.rb +++ b/enterprise/app/services/llm/base_ai_service.rb @@ -2,27 +2,41 @@ # Base service for LLM operations using RubyLLM. # New features should inherit from this class. +# +# Subclasses should implement: +# - feature_name: Returns the feature key (e.g., 'assistant', 'copilot') for model lookup +# - account: Returns the Account instance for per-account model configuration +# +# If these methods are not implemented, falls back to InstallationConfig or DEFAULT_MODEL. class Llm::BaseAiService DEFAULT_MODEL = Llm::Config::DEFAULT_MODEL DEFAULT_TEMPERATURE = 1.0 - attr_reader :model, :temperature + attr_reader :temperature def initialize Llm::Config.initialize! - setup_model setup_temperature end - def chat(model: @model, temperature: @temperature) + def model + @model ||= determine_model + end + + def chat(model: self.model, temperature: @temperature) RubyLLM.chat(model: model).with_temperature(temperature) end private - def setup_model + def determine_model + if respond_to?(:feature_name, true) && respond_to?(:account, true) && account.present? + account_model = account.captain_preferences.dig(:models, feature_name.to_s) + return account_model if account_model.present? + end + config_value = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_MODEL')&.value - @model = (config_value.presence || DEFAULT_MODEL) + config_value.presence || DEFAULT_MODEL end def setup_temperature diff --git a/enterprise/lib/enterprise/integrations/openai_processor_service.rb b/enterprise/lib/enterprise/integrations/openai_processor_service.rb index 5a98ad4c4..1a816fb63 100644 --- a/enterprise/lib/enterprise/integrations/openai_processor_service.rb +++ b/enterprise/lib/enterprise/integrations/openai_processor_service.rb @@ -1,7 +1,6 @@ module Enterprise::Integrations::OpenaiProcessorService - ALLOWED_EVENT_NAMES = %w[rephrase summarize reply_suggestion label_suggestion fix_spelling_grammar shorten expand - make_friendly make_formal simplify].freeze - CACHEABLE_EVENTS = %w[label_suggestion].freeze + ALLOWED_EVENT_NAMES = (Integrations::LlmBaseService::ALLOWED_EVENT_NAMES + Integrations::LlmBaseService::LABEL_SUGGESTION_EVENTS).freeze + CACHEABLE_EVENTS = Integrations::LlmBaseService::LABEL_SUGGESTION_EVENTS.freeze def label_suggestion_message payload = label_suggestion_body @@ -49,7 +48,7 @@ module Enterprise::Integrations::OpenaiProcessorService def summarize_body { - model: self.class::GPT_MODEL, + model: model_for_event, messages: [ { role: 'system', content: prompt_from_file('summary', enterprise: true) }, @@ -65,7 +64,7 @@ module Enterprise::Integrations::OpenaiProcessorService return value_from_cache if content.blank? { - model: self.class::GPT_MODEL, + model: model_for_event, messages: [ { role: 'system', diff --git a/lib/integrations/llm_base_service.rb b/lib/integrations/llm_base_service.rb index ca9459fc8..398c9e2f8 100644 --- a/lib/integrations/llm_base_service.rb +++ b/lib/integrations/llm_base_service.rb @@ -7,11 +7,25 @@ class Integrations::LlmBaseService # 120000 * 4 = 480,000 characters (rounding off downwards to 400,000 to be safe) TOKEN_LIMIT = 400_000 GPT_MODEL = Llm::Config::DEFAULT_MODEL - ALLOWED_EVENT_NAMES = %w[rephrase summarize reply_suggestion fix_spelling_grammar shorten expand make_friendly make_formal simplify].freeze CACHEABLE_EVENTS = %w[].freeze + # Maps event names to feature keys in config/llm.yml + EDITOR_EVENTS = %w[rephrase fix_spelling_grammar shorten expand make_friendly make_formal simplify].freeze + ASSISTANT_EVENTS = %w[summarize reply_suggestion].freeze + LABEL_SUGGESTION_EVENTS = %w[label_suggestion].freeze + + ALLOWED_EVENT_NAMES = (EDITOR_EVENTS + ASSISTANT_EVENTS).freeze + pattr_initialize [:hook!, :event!] + def model_for_event + feature = feature_name_for_event + return self.class::GPT_MODEL unless feature && hook&.account + + account_model = hook.account.captain_preferences.dig(:models, feature) + account_model.presence || self.class::GPT_MODEL + end + def perform return nil unless valid_event_name? @@ -25,6 +39,14 @@ class Integrations::LlmBaseService private + def feature_name_for_event + return 'editor' if EDITOR_EVENTS.include?(event_name) + return 'assistant' if ASSISTANT_EVENTS.include?(event_name) + return 'label_suggestion' if LABEL_SUGGESTION_EVENTS.include?(event_name) + + nil + end + def event_name event['name'] end diff --git a/lib/integrations/openai/processor_service.rb b/lib/integrations/openai/processor_service.rb index 2f0180701..b589b4674 100644 --- a/lib/integrations/openai/processor_service.rb +++ b/lib/integrations/openai/processor_service.rb @@ -53,7 +53,7 @@ class Integrations::Openai::ProcessorService < Integrations::LlmBaseService def build_api_call_body(system_content, user_content = event['data']['content']) { - model: GPT_MODEL, + model: model_for_event, messages: [ { role: 'system', content: system_content }, { role: 'user', content: user_content } @@ -115,7 +115,7 @@ class Integrations::Openai::ProcessorService < Integrations::LlmBaseService def summarize_body { - model: GPT_MODEL, + model: model_for_event, messages: [ { role: 'system', content: prompt_from_file('summary', enterprise: false) }, @@ -126,7 +126,7 @@ class Integrations::Openai::ProcessorService < Integrations::LlmBaseService def reply_suggestion_body { - model: GPT_MODEL, + model: model_for_event, messages: [ { role: 'system', content: prompt_from_file('reply', enterprise: false) }