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

Commit 27b5542

Browse files
romanrizzixfalcox
andauthored
FEATURE: Generate topic gists for the hot topics list. (#837)
* Display gists in the hot topics list * Adjust hot topics gist strategy and add a job to generate gists * Replace setting with a configurable batch size * Avoid loading summaries for other topic lists * Tweak gist prompt to focus on latest posts in the context of the OP * Remove serializer hack and rely on core change from discourse/discourse#29291 * Update lib/summarization/strategies/hot_topic_gists.rb Co-authored-by: Rafael dos Santos Silva <[email protected]> --------- Co-authored-by: Rafael dos Santos Silva <[email protected]>
1 parent decf1bb commit 27b5542

File tree

16 files changed

+455
-107
lines changed

16 files changed

+455
-107
lines changed

.discourse-compatibility

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
< 3.4.0.beta3-dev: ecf1bb49d737ea15308400f22f89d1d1e71d13d
12
< 3.4.0.beta1-dev: 9d887ad4ace8e33c3fe7dbb39237e882c08b4f0b
23
< 3.3.0.beta5-dev: 4d8090002f6dcd8e34d41033606bf131fa221475
34
< 3.3.0.beta2-dev: 61890b667c06299841ae88946f84a112f00060e1
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_max_hot_topics_gists_per_batch.zero?
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(SiteSetting.ai_summarize_max_hot_topics_gists_per_batch)
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
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Component from "@glimmer/component";
2+
3+
export default class AiTopicGist extends Component {
4+
static shouldRender(outletArgs) {
5+
return outletArgs?.topic?.ai_topic_gist && !outletArgs.topic.excerpt;
6+
}
7+
8+
<template>
9+
<div class="ai-topic-gist">
10+
<div class="ai-topic-gist__text">
11+
{{@outletArgs.topic.ai_topic_gist}}
12+
</div>
13+
</div>
14+
</template>
15+
}

assets/stylesheets/modules/summarization/common/ai-summary.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,11 @@
215215
opacity: 1;
216216
}
217217
}
218+
219+
.ai-topic-gist {
220+
margin-top: 0.5em;
221+
222+
&__text {
223+
font-size: var(--font-down-2);
224+
}
225+
}

config/locales/server.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ en:
8484
ai_summarization_model: "Model to use for summarization."
8585
ai_custom_summarization_allowed_groups: "Groups allowed to use create new summaries."
8686
ai_pm_summarization_allowed_groups: "Groups allowed to create and view summaries in PMs."
87+
ai_summarize_max_hot_topics_gists_per_batch: "After updating topics in the hot list, we'll generate brief summaries of the first N ones. (Disabled when 0)"
8788

8889
ai_bot_enabled: "Enable the AI Bot module."
8990
ai_bot_enable_chat_warning: "Display a warning when PM chat is initiated. Can be overriden by editing the translation string: discourse_ai.ai_bot.pm_warning"

config/settings.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,10 @@ discourse_ai:
376376
type: group_list
377377
list_type: compact
378378
default: "3|13" # 3: @staff, 13: @trust_level_3
379+
ai_summarize_max_hot_topics_gists_per_batch:
380+
default: 0
381+
min: 0
382+
max: 1000
379383
ai_summarization_strategy: # TODO(roman): Deprecated. Remove by Sept 2024
380384
type: enum
381385
default: ""

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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,38 @@ def inject_into(plugin)
1616
plugin.add_to_serializer(:web_hook_topic_view, :summarizable) do
1717
scope.can_see_summary?(object.topic, AiSummary.summary_types[:complete])
1818
end
19+
20+
plugin.register_modifier(:topic_query_create_list_topics) do |topics, options|
21+
if options[:filter] == :hot && SiteSetting.ai_summarization_enabled &&
22+
SiteSetting.ai_summarize_max_hot_topics_gists_per_batch > 0
23+
topics.includes(:ai_summaries).where(
24+
"ai_summaries.id IS NULL OR ai_summaries.summary_type = ?",
25+
AiSummary.summary_types[:gist],
26+
)
27+
else
28+
topics
29+
end
30+
end
31+
32+
plugin.add_to_serializer(
33+
:topic_list_item,
34+
:ai_topic_gist,
35+
include_condition: -> do
36+
SiteSetting.ai_summarization_enabled &&
37+
SiteSetting.ai_summarize_max_hot_topics_gists_per_batch > 0 &&
38+
options[:filter] == :hot
39+
end,
40+
) do
41+
summaries = object.ai_summaries.to_a
42+
43+
# Summaries should always have one or zero elements here.
44+
# This is an extra safeguard to avoid including regular summaries.
45+
summaries.find { |s| s.summary_type == "gist" }&.summarized_text
46+
end
47+
48+
# To make sure hot topic gists are inmediately up to date, we rely on this event
49+
# instead of using a scheduled job.
50+
plugin.on(:topic_hot_scores_updated) { Jobs.enqueue(:hot_topics_gist_batch) }
1951
end
2052
end
2153
end
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+
module DiscourseAi
4+
module Summarization
5+
module Strategies
6+
class HotTopicGists < Base
7+
def type
8+
AiSummary.summary_types[:gist]
9+
end
10+
11+
def targets_data
12+
content = { content_title: target.title, contents: [] }
13+
14+
op_post_number = 1
15+
16+
hot_topics_recent_cutoff = Time.zone.now - SiteSetting.hot_topics_recent_days.days
17+
18+
recent_hot_posts =
19+
Post
20+
.where(topic_id: target.id)
21+
.where("post_type = ?", Post.types[:regular])
22+
.where("NOT hidden")
23+
.where("created_at >= ?", hot_topics_recent_cutoff)
24+
.pluck(:post_number)
25+
26+
# It may happen that a topic is hot without any recent posts
27+
# In that case, we'll just grab the last 20 posts
28+
# for an useful summary of the current state of the topic
29+
if recent_hot_posts.empty?
30+
recent_hot_posts =
31+
Post
32+
.where(topic_id: target.id)
33+
.where("post_type = ?", Post.types[:regular])
34+
.where("NOT hidden")
35+
.order("post_number DESC")
36+
.limit(20)
37+
.pluck(:post_number)
38+
end
39+
posts_data =
40+
Post
41+
.where(topic_id: target.id)
42+
.joins(:user)
43+
.where("post_number IN (?)", recent_hot_posts << op_post_number)
44+
.order(:post_number)
45+
.pluck(:post_number, :raw, :username)
46+
47+
posts_data.each do |(pn, raw, username)|
48+
raw_text = raw
49+
50+
if pn == 1 && target.topic_embed&.embed_content_cache.present?
51+
raw_text = target.topic_embed&.embed_content_cache
52+
end
53+
54+
content[:contents] << { poster: username, id: pn, text: raw_text }
55+
end
56+
57+
content
58+
end
59+
60+
def concatenation_prompt(texts_to_summarize)
61+
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip)
62+
You are a summarization bot tasked with creating a single, concise sentence by merging disjointed summaries into a cohesive statement.
63+
Your response should strictly be this single, comprehensive sentence, without any additional text or comments.
64+
65+
- Focus on the central theme or issue being addressed, maintaining an objective and neutral tone.
66+
- Exclude extraneous details or subjective opinions.
67+
- Use the original language of the text.
68+
- Begin directly with the main topic or issue, avoiding introductory phrases.
69+
- Limit the summary to a maximum of 20 words.
70+
TEXT
71+
72+
prompt.push(type: :user, content: <<~TEXT.strip)
73+
THESE are the summaries, each one separated by a newline, all of them inside <input></input> XML tags:
74+
75+
<input>
76+
#{texts_to_summarize.join("\n")}
77+
</input>
78+
TEXT
79+
80+
prompt
81+
end
82+
83+
def summarize_single_prompt(input, opts)
84+
statements = input.split(/(?=\d+\) \w+ said:)/)
85+
86+
prompt = DiscourseAi::Completions::Prompt.new(<<~TEXT.strip)
87+
You are an advanced summarization bot. Analyze a given conversation and produce a concise,
88+
single-sentence summary that conveys the main topic and current developments to someone with no prior context.
89+
90+
### Guidelines:
91+
92+
- Emphasize the most recent updates while considering their significance within the original post.
93+
- Focus on the central theme or issue being addressed, maintaining an objective and neutral tone.
94+
- Exclude extraneous details or subjective opinions.
95+
- Use the original language of the text.
96+
- Begin directly with the main topic or issue, avoiding introductory phrases.
97+
- Limit the summary to a maximum of 20 words.
98+
TEXT
99+
100+
prompt.push(type: :user, content: <<~TEXT.strip)
101+
### Context:
102+
103+
The conversation began with the following statement:
104+
105+
#{opts[:content_title].present? ? "The discussion title is: " + opts[:content_title] + ".\n" : ""}
106+
107+
#{statements&.pop}
108+
109+
Subsequent discussion includes the following:
110+
111+
#{statements&.join}
112+
113+
Your task is to focus on these latest messages, capturing their meaning in the context of the initial post.
114+
TEXT
115+
116+
prompt
117+
end
118+
end
119+
end
120+
end
121+
end

lib/summarization/strategies/topic_gist.rb

Lines changed: 0 additions & 90 deletions
This file was deleted.

0 commit comments

Comments
 (0)