diff --git a/app/jobs/clear_ai_cache_job.rb b/app/jobs/clear_ai_cache_job.rb new file mode 100644 index 000000000..bff65fa79 --- /dev/null +++ b/app/jobs/clear_ai_cache_job.rb @@ -0,0 +1,15 @@ +class ClearAiCacheJob < ApplicationJob + queue_as :low_priority + + def perform(family) + Rails.logger.info("Clearing AI cache for family #{family.id}") + + # Clear AI enrichment data for transactions + Transaction.clear_ai_cache(family) + Rails.logger.info("Cleared AI cache for transactions") + + # Clear AI enrichment data for entries + Entry.clear_ai_cache(family) + Rails.logger.info("Cleared AI cache for entries") + end +end diff --git a/app/models/concerns/enrichable.rb b/app/models/concerns/enrichable.rb index f305a4faa..d9e0fc38b 100644 --- a/app/models/concerns/enrichable.rb +++ b/app/models/concerns/enrichable.rb @@ -15,6 +15,8 @@ module Enrichable InvalidAttributeError = Class.new(StandardError) included do + has_many :data_enrichments, as: :enrichable, dependent: :destroy + scope :enrichable, ->(attrs) { attrs = Array(attrs).map(&:to_s) json_condition = attrs.each_with_object({}) { |attr, hash| hash[attr] = true } @@ -22,6 +24,28 @@ module Enrichable } end + class_methods do + def clear_ai_cache(family) + # Get all records that belong to this family + records = if respond_to?(:joins) + case name + when "Transaction" + joins(entry: :account).where(accounts: { family_id: family.id }) + when "Entry" + joins(:account).where(accounts: { family_id: family.id }) + else + none + end + else + none + end + + records.find_each do |record| + record.clear_ai_cache + end + end + end + # Convenience method for a single attribute def enrich_attribute(attr, value, source:, metadata: {}) enrich_attributes({ attr => value }, source:, metadata:) @@ -124,6 +148,21 @@ module Enrichable end end + def clear_ai_cache + ActiveRecord::Base.transaction do + # Find attributes that were locked by AI enrichment + ai_enriched_attrs = data_enrichments.where(source: "ai").pluck(:attribute_name).uniq + + # Remove locks for AI-enriched attributes + ai_enriched_attrs.each do |attr| + unlock_attr!(attr) if locked?(attr) + end + + # Delete AI enrichment records + data_enrichments.where(source: "ai").delete_all + end + end + private def log_enrichment(attribute_name:, attribute_value:, source:, metadata: {}) de = DataEnrichment.find_or_create_by( diff --git a/app/models/setting.rb b/app/models/setting.rb index 857949b5a..9a9facfb8 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -80,6 +80,8 @@ class Setting < RailsSettings::Base class << self alias_method :raw_onboarding_state, :onboarding_state alias_method :raw_onboarding_state=, :onboarding_state= + alias_method :raw_openai_model, :openai_model + alias_method :raw_openai_model=, :openai_model= def onboarding_state value = raw_onboarding_state @@ -94,6 +96,18 @@ class Setting < RailsSettings::Base self.raw_onboarding_state = state end + def openai_model=(value) + old_value = raw_openai_model + self.raw_openai_model = value + + if old_value != value && old_value.present? + Rails.logger.info("OpenAI model changed from #{old_value} to #{value}, clearing AI cache for all families") + Family.find_each do |family| + ClearAiCacheJob.perform_later(family) + end + end + end + # Support dynamic field access via bracket notation # First checks if it's a declared field, then falls back to individual dynamic entries def [](key)