Skip to content
This repository was archived by the owner on Jul 22, 2025. It is now read-only.

Commit f1113ac

Browse files
committed
Adjust hot topics gist strategy and add a job to generate gists
1 parent 3f6cbd7 commit f1113ac

File tree

6 files changed

+162
-21
lines changed

6 files changed

+162
-21
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
module ::Jobs
4+
class HotTopicsGistBatch < ::Jobs::Base
5+
def execute(args)
6+
return if !SiteSetting.discourse_ai_enabled
7+
return if !SiteSetting.ai_summarization_enabled
8+
return if !SiteSetting.ai_summarize_hot_topics_list
9+
10+
Topic
11+
.joins("JOIN topic_hot_scores on topics.id = topic_hot_scores.topic_id")
12+
.order("topic_hot_scores.score DESC")
13+
.limit(100)
14+
.each do |topic|
15+
summarizer = DiscourseAi::Summarization.topic_gist(topic)
16+
gist = summarizer.existing_summary
17+
18+
summarizer.delete_cached_summaries! if gist && gist.outdated
19+
20+
summarizer.summarize(Discourse.system_user)
21+
end
22+
end
23+
end
24+
end

lib/summarization.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def self.topic_gist(topic)
1717
if SiteSetting.ai_summarization_model.present? && SiteSetting.ai_summarization_enabled
1818
DiscourseAi::Summarization::FoldContent.new(
1919
DiscourseAi::Completions::Llm.proxy(SiteSetting.ai_summarization_model),
20-
DiscourseAi::Summarization::Strategies::TopicGist.new(topic),
20+
DiscourseAi::Summarization::Strategies::HotTopicGists.new(topic),
2121
)
2222
else
2323
nil

lib/summarization/entry_point.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ def inject_into(plugin)
3636
SiteSetting.ai_summarization_enabled && SiteSetting.ai_summarize_hot_topics_list
3737
end,
3838
) { object.ai_summaries.to_a.first&.summarized_text }
39+
40+
# To make sure hot topic gists are inmediately up to date, we rely on this event
41+
# instead of using a scheduled job.
42+
plugin.on(:topic_hot_scores_updated) { Jobs.enqueue(:summarize_hot_topics_batch) }
3943
end
4044
end
4145
end

lib/summarization/strategies/topic_gist.rb renamed to lib/summarization/strategies/hot_topic_gists.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
module DiscourseAi
44
module Summarization
55
module Strategies
6-
class TopicGist < Base
6+
class HotTopicGists < Base
77
def type
88
AiSummary.summary_types[:gist]
99
end
@@ -13,20 +13,21 @@ def targets_data
1313

1414
op_post_number = 1
1515

16-
last_twenty_posts =
16+
hot_topics_recent_cutoff = Time.zone.now - SiteSetting.hot_topics_recent_days.days
17+
18+
recent_hot_posts =
1719
Post
1820
.where(topic_id: target.id)
1921
.where("post_type = ?", Post.types[:regular])
2022
.where("NOT hidden")
21-
.order("post_number DESC")
22-
.limit(20)
23+
.where("created_at >= ?", hot_topics_recent_cutoff)
2324
.pluck(:post_number)
2425

2526
posts_data =
2627
Post
2728
.where(topic_id: target.id)
2829
.joins(:user)
29-
.where("post_number IN (?)", last_twenty_posts << op_post_number)
30+
.where("post_number IN (?)", recent_hot_posts << op_post_number)
3031
.order(:post_number)
3132
.pluck(:post_number, :raw, :username)
3233

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe Jobs::HotTopicsGistBatch do
4+
fab!(:topic_1) { Fabricate(:topic) }
5+
fab!(:post_1) { Fabricate(:post, topic: topic_1, post_number: 1) }
6+
fab!(:post_2) { Fabricate(:post, topic: topic_1, post_number: 2) }
7+
8+
before do
9+
assign_fake_provider_to(:ai_summarization_model)
10+
SiteSetting.ai_summarization_enabled = true
11+
SiteSetting.ai_summarize_hot_topics_list = true
12+
end
13+
14+
describe "#execute" do
15+
context "When there is a topic with a hot score" do
16+
before { TopicHotScore.create!(topic_id: topic_1.id, score: 0.1) }
17+
18+
it "does nothing if the plugin is disabled" do
19+
SiteSetting.discourse_ai_enabled = false
20+
21+
subject.execute({})
22+
23+
gist = AiSummary.gist.find_by(target: topic_1)
24+
expect(gist).to be_nil
25+
end
26+
27+
it "does nothing if the summarization module is disabled" do
28+
SiteSetting.ai_summarization_enabled = false
29+
30+
subject.execute({})
31+
32+
gist = AiSummary.gist.find_by(target: topic_1)
33+
expect(gist).to be_nil
34+
end
35+
36+
it "does nothing if hot topics summarization is disabled" do
37+
SiteSetting.ai_summarize_hot_topics_list = false
38+
39+
subject.execute({})
40+
41+
gist = AiSummary.gist.find_by(target: topic_1)
42+
expect(gist).to be_nil
43+
end
44+
45+
it "creates a gist" do
46+
gist_result = "I'm a gist"
47+
48+
DiscourseAi::Completions::Llm.with_prepared_responses([gist_result]) { subject.execute({}) }
49+
50+
gist = AiSummary.gist.find_by(target: topic_1)
51+
expect(gist.summarized_text).to eq("I'm a gist")
52+
end
53+
54+
context "and we already generated a gist of it" do
55+
fab!(:ai_gist) do
56+
Fabricate(
57+
:topic_ai_gist,
58+
target: topic_1,
59+
original_content_sha: AiSummary.build_sha("12"),
60+
)
61+
end
62+
63+
it "does nothing if the gist is up to date" do
64+
subject.execute({})
65+
66+
gist = AiSummary.gist.find_by(target: topic_1)
67+
expect(gist.summarized_text).to eq(ai_gist.summarized_text)
68+
expect(gist.original_content_sha).to eq(ai_gist.original_content_sha)
69+
end
70+
71+
it "regenerates it if it's outdated" do
72+
Fabricate(:post, topic: topic_1, post_number: 3)
73+
gist_result = "They updated me"
74+
75+
DiscourseAi::Completions::Llm.with_prepared_responses([gist_result]) do
76+
subject.execute({})
77+
end
78+
79+
gist = AiSummary.gist.find_by(target: topic_1)
80+
expect(gist.summarized_text).to eq(gist_result)
81+
expect(gist.original_content_sha).to eq(AiSummary.build_sha("123"))
82+
end
83+
end
84+
end
85+
86+
context "when there is a topic but it doesn't have a hot score" do
87+
it "does nothing" do
88+
subject.execute({})
89+
90+
gist = AiSummary.gist.find_by(target: topic_1)
91+
expect(gist).to be_nil
92+
end
93+
end
94+
95+
context "when there are multiple hot topics" do
96+
fab!(:topic_2) { Fabricate(:topic) }
97+
fab!(:post_2_1) { Fabricate(:post, topic: topic_2, post_number: 1) }
98+
fab!(:post_2_2) { Fabricate(:post, topic: topic_2, post_number: 2) }
99+
100+
before do
101+
TopicHotScore.create!(topic_id: topic_1.id, score: 0.2)
102+
TopicHotScore.create!(topic_id: topic_2.id, score: 0.4)
103+
end
104+
105+
it "processes them by score order" do
106+
topic_1_gist = "I'm gist of topic 1"
107+
topic_2_gist = "I'm gist of topic 2"
108+
109+
DiscourseAi::Completions::Llm.with_prepared_responses([topic_2_gist, topic_1_gist]) do
110+
subject.execute({})
111+
end
112+
113+
gist = AiSummary.gist.find_by(target: topic_1)
114+
expect(gist.summarized_text).to eq(topic_1_gist)
115+
116+
gist_2 = AiSummary.gist.find_by(target: topic_2)
117+
expect(gist_2.summarized_text).to eq(topic_2_gist)
118+
end
119+
end
120+
end
121+
end

spec/lib/modules/summarization/strategies/topic_gist_spec.rb renamed to spec/lib/modules/summarization/strategies/hot_topic_gists_spec.rb

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,20 @@
11
# frozen_string_literal: true
22

3-
RSpec.describe DiscourseAi::Summarization::Strategies::TopicGist do
3+
RSpec.describe DiscourseAi::Summarization::Strategies::HotTopicGists do
44
subject(:gist) { described_class.new(topic) }
55

66
fab!(:topic) { Fabricate(:topic, highest_post_number: 25) }
77
fab!(:post_1) { Fabricate(:post, topic: topic, post_number: 1) }
88
fab!(:post_2) { Fabricate(:post, topic: topic, post_number: 2) }
99

1010
describe "#targets_data" do
11-
context "when the topic has more than 20 posts" do
12-
before do
13-
offset = 3 # Already created posts 1 and 2
14-
(topic.highest_post_number - 2).times do |i|
15-
Fabricate(:post, topic: topic, post_number: i + offset)
16-
end
17-
end
18-
19-
it "includes the OP and the last 20 posts" do
20-
content = gist.targets_data
21-
post_numbers = content[:contents].map { |c| c[:id] }
11+
it "respects the `hot_topics_recent_days` setting" do
12+
post_2.update(created_at: (SiteSetting.hot_topics_recent_days + 1).days.ago)
13+
Fabricate(:post, topic: topic, post_number: 3)
2214

23-
expected = (6..25).to_a << 1
15+
post_numbers = gist.targets_data[:contents].map { |c| c[:id] }
2416

25-
expect(post_numbers).to contain_exactly(*expected)
26-
end
17+
expect(post_numbers).to contain_exactly(1, 3)
2718
end
2819

2920
it "only includes visible posts" do

0 commit comments

Comments
 (0)