diff --git a/lib/captain/base_editor_service.rb b/lib/captain/base_editor_service.rb index dec8f741b..efed5b2cc 100644 --- a/lib/captain/base_editor_service.rb +++ b/lib/captain/base_editor_service.rb @@ -7,17 +7,11 @@ class Captain::BaseEditorService # 120000 * 4 = 480,000 characters (rounding off downwards to 400,000 to be safe) TOKEN_LIMIT = 400_000 GPT_MODEL = Llm::Config::DEFAULT_MODEL - CACHEABLE_EVENTS = [].freeze pattr_initialize [:account!, :event!] def perform - return value_from_cache if value_from_cache.present? - - response = send("#{event_name}_message") - save_to_cache(response) if response.present? - - response + send("#{event_name}_message") end private @@ -26,52 +20,10 @@ class Captain::BaseEditorService event['name'] end - def cache_key - return nil unless event_is_cacheable? - - return nil unless conversation - - # since the value from cache depends on the conversation last_activity_at, it will always be fresh - format(::Redis::Alfred::OPENAI_CONVERSATION_KEY, event_name: event_name, conversation_id: conversation.id, - updated_at: conversation.last_activity_at.to_i) - end - - def value_from_cache - return nil unless event_is_cacheable? - return nil if cache_key.blank? - - deserialize_cached_value(Redis::Alfred.get(cache_key)) - end - - def deserialize_cached_value(value) - return nil if value.blank? - - JSON.parse(value, symbolize_names: true) - rescue JSON::ParserError - # If json parse failed, returning the value as is will fail too - # since we access the keys as symbols down the line - # So it's best to return nil - nil - end - - def save_to_cache(response) - return nil unless event_is_cacheable? - - # Serialize to JSON - # This makes parsing easy when response is a hash - Redis::Alfred.setex(cache_key, response.to_json) - end - def conversation @conversation ||= account.conversations.find_by(display_id: event['data']['conversation_display_id']) end - def event_is_cacheable? - # self.class::CACHEABLE_EVENTS is way to access CACHEABLE_EVENTS defined in the class hierarchy of the current object. - # This ensures that if CACHEABLE_EVENTS is updated elsewhere in it's ancestors, we access the latest value. - self.class::CACHEABLE_EVENTS.include?(event_name) - end - def api_base endpoint = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value.presence || 'https://api.openai.com/' endpoint = endpoint.chomp('/') diff --git a/lib/captain/label_suggestion_service.rb b/lib/captain/label_suggestion_service.rb index d959fdbab..b06514721 100644 --- a/lib/captain/label_suggestion_service.rb +++ b/lib/captain/label_suggestion_service.rb @@ -1,10 +1,14 @@ class Captain::LabelSuggestionService < Captain::BaseEditorService - CACHEABLE_EVENTS = %w[label_suggestion].freeze - def label_suggestion_message + # Check cache first + cached_response = read_from_cache + return cached_response if cached_response.present? + + # Build content content = labels_with_messages return nil if content.blank? + # Make API call response = make_api_call( model: GPT_MODEL, # TODO: Use separate model for label suggestion messages: [ @@ -14,13 +18,41 @@ class Captain::LabelSuggestionService < Captain::BaseEditorService ) return response if response[:error].present? - # LLMs are not deterministic - sometimes response includes "Labels:" prefix - # TODO: Fix with better prompt - { message: response[:message] ? response[:message].gsub(/^(label|labels):/i, '') : '' } + # Clean up response + result = { message: response[:message] ? response[:message].gsub(/^(label|labels):/i, '') : '' } + + # Cache successful result + write_to_cache(result) + + result end private + def cache_key + return nil unless conversation + + format( + ::Redis::Alfred::OPENAI_CONVERSATION_KEY, + event_name: 'label_suggestion', + conversation_id: conversation.id, + updated_at: conversation.last_activity_at.to_i + ) + end + + def read_from_cache + return nil unless cache_key + + cached = Redis::Alfred.get(cache_key) + JSON.parse(cached, symbolize_names: true) if cached.present? + rescue JSON::ParserError + nil + end + + def write_to_cache(response) + Redis::Alfred.setex(cache_key, response.to_json) if cache_key + end + def labels_with_messages return nil unless valid_conversation?(conversation)