diff --git a/app/jobs/regular/hot_topics_gist_batch.rb b/app/jobs/regular/hot_topics_gist_batch.rb index b5ff3a7e..aef230a7 100644 --- a/app/jobs/regular/hot_topics_gist_batch.rb +++ b/app/jobs/regular/hot_topics_gist_batch.rb @@ -2,7 +2,7 @@ module ::Jobs class HotTopicsGistBatch < ::Jobs::Base - def execute(args) + def execute(_args) return if !SiteSetting.discourse_ai_enabled return if !SiteSetting.ai_summarization_enabled return if SiteSetting.ai_summarize_max_hot_topics_gists_per_batch.zero? @@ -15,11 +15,7 @@ module ::Jobs summarizer = DiscourseAi::Summarization.topic_gist(topic) gist = summarizer.existing_summary - if gist.blank? || gist.outdated - summarizer.delete_cached_summaries! - - summarizer.summarize(Discourse.system_user) - end + summarizer.force_summarize(Discourse.system_user) if gist.blank? || gist.outdated end end end diff --git a/app/jobs/regular/update_hot_topic_gist.rb b/app/jobs/regular/update_hot_topic_gist.rb new file mode 100644 index 00000000..f4a2d3b3 --- /dev/null +++ b/app/jobs/regular/update_hot_topic_gist.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module ::Jobs + class UpdateHotTopicGist < ::Jobs::Base + sidekiq_options retry: false + + def execute(args) + return if !SiteSetting.discourse_ai_enabled + return if !SiteSetting.ai_summarization_enabled + return if SiteSetting.ai_summarize_max_hot_topics_gists_per_batch.zero? + + topic = Topic.find_by(id: args[:topic_id]) + return if topic.blank? + + return if !TopicHotScore.where(topic: topic).exists? + + summarizer = DiscourseAi::Summarization.topic_gist(topic) + gist = summarizer.existing_summary + return if gist.blank? + return if !gist.outdated + + summarizer.force_summarize(Discourse.system_user) + end + end +end diff --git a/lib/summarization/entry_point.rb b/lib/summarization/entry_point.rb index 07fc3ef4..f3d00939 100644 --- a/lib/summarization/entry_point.rb +++ b/lib/summarization/entry_point.rb @@ -47,6 +47,19 @@ module DiscourseAi # To make sure hot topic gists are inmediately up to date, we rely on this event # instead of using a scheduled job. plugin.on(:topic_hot_scores_updated) { Jobs.enqueue(:hot_topics_gist_batch) } + + # As this event can be triggered quite often, let's be overly cautious enqueueing + # jobs if the feature is disabled. + plugin.on(:post_created) do |post| + if SiteSetting.discourse_ai_enabled && SiteSetting.ai_summarization_enabled && + SiteSetting.ai_summarize_max_hot_topics_gists_per_batch > 0 && post.topic + hot_score = TopicHotScore.find_by(topic: post.topic) + + if hot_score.exists? && hot_score.updated_at > 1.day.ago + Jobs.enqueue(:update_hot_topic_gist, topic_id: post&.topic_id) + end + end + end end end end diff --git a/lib/summarization/fold_content.rb b/lib/summarization/fold_content.rb index a443e04f..da3880d8 100644 --- a/lib/summarization/fold_content.rb +++ b/lib/summarization/fold_content.rb @@ -66,6 +66,11 @@ module DiscourseAi AiSummary.where(target: strategy.target, summary_type: strategy.type).destroy_all end + def force_summarize(user, &on_partial_blk) + delete_cached_summaries! + summarize(user, &on_partial_blk) + end + private attr_reader :persist_summaries diff --git a/spec/jobs/regular/update_hot_topic_gist_spec.rb b/spec/jobs/regular/update_hot_topic_gist_spec.rb new file mode 100644 index 00000000..b999df67 --- /dev/null +++ b/spec/jobs/regular/update_hot_topic_gist_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +RSpec.describe Jobs::UpdateHotTopicGist do + describe "#execute" do + fab!(:topic_1) { Fabricate(:topic) } + fab!(:post_1) { Fabricate(:post, topic: topic_1, post_number: 1) } + fab!(:post_2) { Fabricate(:post, topic: topic_1, post_number: 2) } + + before do + assign_fake_provider_to(:ai_summarization_model) + SiteSetting.ai_summarization_enabled = true + SiteSetting.ai_summarize_max_hot_topics_gists_per_batch = 100 + end + + context "when the hot topic has a gist" do + before { TopicHotScore.create!(topic_id: topic_1.id, score: 0.1) } + fab!(:ai_gist) do + Fabricate(:topic_ai_gist, target: topic_1, original_content_sha: AiSummary.build_sha("12")) + end + let(:updated_gist) { "They updated me :(" } + + context "when it's up to date" do + it "does nothing" do + DiscourseAi::Completions::Llm.with_prepared_responses([updated_gist]) do + subject.execute(topic_id: topic_1.id) + end + + gist = AiSummary.gist.find_by(target: topic_1) + expect(AiSummary.gist.where(target: topic_1).count).to eq(1) + expect(gist.summarized_text).not_to eq(updated_gist) + end + end + + context "when it's outdated" do + it "regenerates the gist using the latest data" do + Fabricate(:post, topic: topic_1, post_number: 3) + + DiscourseAi::Completions::Llm.with_prepared_responses([updated_gist]) do + subject.execute(topic_id: topic_1.id) + end + + gist = AiSummary.gist.find_by(target: topic_1) + expect(AiSummary.gist.where(target: topic_1).count).to eq(1) + expect(gist.summarized_text).to eq(updated_gist) + expect(gist.original_content_sha).to eq(AiSummary.build_sha("123")) + end + end + end + + context "when the topic doesn't have a hot topic score" do + it "does nothing" do + subject.execute({}) + + gist = AiSummary.gist.find_by(target: topic_1) + expect(gist).to be_nil + end + end + + context "when the topic has a hot topic score but no gist" do + before { TopicHotScore.create!(topic_id: topic_1.id, score: 0.1) } + + it "does nothing" do + subject.execute({}) + + gist = AiSummary.gist.find_by(target: topic_1) + expect(gist).to be_nil + end + end + end +end