FEATURE: Fast-track gist regeneration when a hot topic gets a new post (#860)

* FEATURE: Fast-track gist regeneration when a hot topic gets a new post

* DEV: Introduce an upsert-like summarize

* FIX: Only enqueue fast-track gist for hot hot hot topics

---------

Co-authored-by: Rafael Silva <xfalcox@gmail.com>
This commit is contained in:
Roman Rizzi 2024-10-25 12:38:49 -03:00 committed by GitHub
parent 33da27e231
commit a2b1ea3c63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 115 additions and 6 deletions

View File

@ -2,7 +2,7 @@
module ::Jobs module ::Jobs
class HotTopicsGistBatch < ::Jobs::Base class HotTopicsGistBatch < ::Jobs::Base
def execute(args) def execute(_args)
return if !SiteSetting.discourse_ai_enabled return if !SiteSetting.discourse_ai_enabled
return if !SiteSetting.ai_summarization_enabled return if !SiteSetting.ai_summarization_enabled
return if SiteSetting.ai_summarize_max_hot_topics_gists_per_batch.zero? return if SiteSetting.ai_summarize_max_hot_topics_gists_per_batch.zero?
@ -15,11 +15,7 @@ module ::Jobs
summarizer = DiscourseAi::Summarization.topic_gist(topic) summarizer = DiscourseAi::Summarization.topic_gist(topic)
gist = summarizer.existing_summary gist = summarizer.existing_summary
if gist.blank? || gist.outdated summarizer.force_summarize(Discourse.system_user) if gist.blank? || gist.outdated
summarizer.delete_cached_summaries!
summarizer.summarize(Discourse.system_user)
end
end end
end end
end end

View File

@ -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

View File

@ -47,6 +47,19 @@ module DiscourseAi
# To make sure hot topic gists are inmediately up to date, we rely on this event # To make sure hot topic gists are inmediately up to date, we rely on this event
# instead of using a scheduled job. # instead of using a scheduled job.
plugin.on(:topic_hot_scores_updated) { Jobs.enqueue(:hot_topics_gist_batch) } 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 end
end end

View File

@ -66,6 +66,11 @@ module DiscourseAi
AiSummary.where(target: strategy.target, summary_type: strategy.type).destroy_all AiSummary.where(target: strategy.target, summary_type: strategy.type).destroy_all
end end
def force_summarize(user, &on_partial_blk)
delete_cached_summaries!
summarize(user, &on_partial_blk)
end
private private
attr_reader :persist_summaries attr_reader :persist_summaries

View File

@ -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