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

Commit 3a108ea

Browse files
committed
WIP: Use personas for summarization
1 parent 6729147 commit 3a108ea

File tree

13 files changed

+136
-92
lines changed

13 files changed

+136
-92
lines changed

app/models/ai_persona.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ def self.persona_cache
4646

4747
scope :ordered, -> { order("priority DESC, lower(name) ASC") }
4848

49-
def self.all_personas
49+
def self.all_personas(only_enabled: true)
5050
persona_cache[:value] ||= AiPersona
5151
.ordered
52-
.where(enabled: true)
52+
.where(enabled: only_enabled)
5353
.all
5454
.limit(MAX_PERSONAS_PER_SITE)
5555
.map(&:class_instance)

config/locales/server.en.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,9 @@ en:
303303
web_artifact_creator:
304304
name: "Web Artifact Creator"
305305
description: "AI Bot specialized in creating interactive web artifacts"
306+
summarization:
307+
name: "Summarization"
308+
description: "Default persona used to power AI summaries."
306309
topic_not_found: "Summary unavailable, topic not found!"
307310
summarizing: "Summarizing topic"
308311
searching: "Searching for: '%{query}'"

config/settings.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,11 @@ discourse_ai:
240240
type: enum
241241
enum: "DiscourseAi::Configuration::LlmEnumerator"
242242
validator: "DiscourseAi::Configuration::LlmValidator"
243+
ai_summarization_persona:
244+
default: "-11"
245+
type: enum
246+
enum: "DiscourseAi::Configuration::PersonaEnumerator"
247+
243248
ai_pm_summarization_allowed_groups:
244249
type: group_list
245250
list_type: compact
@@ -248,6 +253,7 @@ discourse_ai:
248253
type: group_list
249254
list_type: compact
250255
default: "3|13" # 3: @staff, 13: @trust_level_3
256+
hidden: true
251257
ai_summary_gists_enabled:
252258
default: false
253259
hidden: true

db/fixtures/personas/603_ai_personas.rb

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,19 @@
88
if persona_class == DiscourseAi::Personas::WebArtifactCreator
99
# this is somewhat sensitive, so we default it to staff
1010
persona.allowed_group_ids = [Group::AUTO_GROUPS[:staff]]
11+
elsif persona_class == DiscourseAi::Personas::Summarization
12+
persona.allowed_group_ids =
13+
DB
14+
.query_single(
15+
"SELECT value FROM site_settings WHERE name = :setting_name",
16+
setting_name: "ai_custom_summarization_allowed_groups",
17+
)
18+
.first
19+
&.split("|") || [Group::AUTO_GROUPS[:staff], Group::AUTO_GROUPS[:trust_level_3]]
1120
else
1221
persona.allowed_group_ids = [Group::AUTO_GROUPS[:trust_level_0]]
1322
end
14-
persona.enabled = true
23+
persona.enabled = persona_class != DiscourseAi::Personas::Summarization
1524
persona.priority = true if persona_class == DiscourseAi::Personas::General
1625
end
1726

@@ -22,16 +31,16 @@
2231
persona_class.name + SecureRandom.hex,
2332
]
2433
persona.name = DB.query_single(<<~SQL, names, id).first
25-
SELECT guess_name
26-
FROM (
27-
SELECT unnest(Array[?]) AS guess_name
28-
FROM (SELECT 1) as t
29-
) x
30-
LEFT JOIN ai_personas ON ai_personas.name = x.guess_name AND ai_personas.id <> ?
31-
WHERE ai_personas.id IS NULL
32-
ORDER BY x.guess_name ASC
33-
LIMIT 1
34-
SQL
34+
SELECT guess_name
35+
FROM (
36+
SELECT unnest(Array[?]) AS guess_name
37+
FROM (SELECT 1) as t
38+
) x
39+
LEFT JOIN ai_personas ON ai_personas.name = x.guess_name AND ai_personas.id <> ?
40+
WHERE ai_personas.id IS NULL
41+
ORDER BY x.guess_name ASC
42+
LIMIT 1
43+
SQL
3544

3645
persona.description = persona_class.description
3746

lib/configuration/persona_enumerator.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ def self.valid_value?(val)
1010
end
1111

1212
def self.values
13-
AiPersona.all_personas.map { |persona| { name: persona.name, value: persona.id } }
13+
AiPersona
14+
.all_personas(only_enabled: false)
15+
.map { |persona| { name: persona.name, value: persona.id } }
1416
end
1517
end
1618
end

lib/personas/bot.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def reply(context, &update_blk)
6767
llm_kwargs = { user: user }
6868
llm_kwargs[:temperature] = persona.temperature if persona.temperature
6969
llm_kwargs[:top_p] = persona.top_p if persona.top_p
70+
llm_kwargs[:max_tokens] = context[:max_tokens] if context[:max_tokens].present?
7071

7172
needs_newlines = false
7273
tools_ran = 0
@@ -84,7 +85,7 @@ def reply(context, &update_blk)
8485
result =
8586
current_llm.generate(
8687
prompt,
87-
feature_name: "bot",
88+
feature_name: context[:feature_name] || "bot",
8889
partial_tool_calls: allow_partial_tool_calls,
8990
output_thinking: true,
9091
**llm_kwargs,

lib/personas/persona.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def system_personas
4444
DiscourseHelper => -8,
4545
GithubHelper => -9,
4646
WebArtifactCreator => -10,
47+
Summarization => -11,
4748
}
4849
end
4950

lib/personas/summarization.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
module DiscourseAi
4+
module Personas
5+
class Summarization < Persona
6+
def system_prompt
7+
<<~PROMPT.strip
8+
You are an advanced summarization bot that generates concise, coherent summaries of provided text.
9+
You are also capable of enhancing an existing summaries by incorporating additional posts if asked to.
10+
11+
- Only include the summary, without any additional commentary.
12+
- You understand and generate Discourse forum Markdown; including links, _italics_, **bold**.
13+
- Maintain the original language of the text being summarized.
14+
- Aim for summaries to be 400 words or less.
15+
- Each post is formatted as "<POST_NUMBER>) <USERNAME> <MESSAGE>"
16+
- Use the provided <BASE_TOPIC_URL> when building links.
17+
- Cite specific noteworthy posts using the format [DESCRIPTION]({topic_url}/POST_NUMBER)
18+
- Example: links to the 3rd and 6th posts by sam: sam ([#3]({topic_url}/3), [#6]({topic_url}/6))
19+
- Example: link to the 6th post by jane: [agreed with]({topic_url}/6)
20+
- Example: link to the 13th post by joe: [joe]({topic_url}/13)
21+
- When formatting usernames either use @USERNMAE OR [USERNAME]({topic_url}/POST_NUMBER)
22+
PROMPT
23+
end
24+
end
25+
end
26+
end

lib/summarization.rb

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@
33
module DiscourseAi
44
module Summarization
55
def self.topic_summary(topic)
6-
if SiteSetting.ai_summarization_model.present? && SiteSetting.ai_summarization_enabled
7-
DiscourseAi::Summarization::FoldContent.new(
8-
DiscourseAi::Completions::Llm.proxy(SiteSetting.ai_summarization_model),
9-
DiscourseAi::Summarization::Strategies::TopicSummary.new(topic),
10-
)
11-
else
12-
nil
6+
return nil if (model = SiteSetting.ai_summarization_model).blank?
7+
if (ai_persona = AiPersona.find_by(id: SiteSetting.ai_summarization_persona)).blank?
8+
return nil
139
end
10+
return nil if !SiteSetting.ai_summarization_enabled
11+
12+
persona_class = ai_persona.class_instance
13+
persona = persona_class.new
14+
user = User.find_by(id: persona_class.user_id) || Discourse.system_user
15+
16+
bot = DiscourseAi::Personas::Bot.as(user, persona: persona, model: model)
17+
18+
DiscourseAi::Summarization::FoldContent.new(
19+
bot,
20+
DiscourseAi::Summarization::Strategies::TopicSummary.new(topic),
21+
)
1422
end
1523

1624
def self.topic_gist(topic)
@@ -25,15 +33,17 @@ def self.topic_gist(topic)
2533
end
2634

2735
def self.chat_channel_summary(channel, time_window_in_hours)
28-
if SiteSetting.ai_summarization_model.present? && SiteSetting.ai_summarization_enabled
29-
DiscourseAi::Summarization::FoldContent.new(
30-
DiscourseAi::Completions::Llm.proxy(SiteSetting.ai_summarization_model),
31-
DiscourseAi::Summarization::Strategies::ChatMessages.new(channel, time_window_in_hours),
32-
persist_summaries: false,
33-
)
34-
else
35-
nil
36-
end
36+
return nil if model = SiteSetting.ai_summarization_model.present?
37+
return nil if persona = AiPersona.find_by(id: SiteSetting.ai_summarization_persona)
38+
return nil if !SiteSetting.ai_summarization_enabled
39+
40+
bot = DiscourseAi::Personas::Bot.as(Discourse.system_user, persona: persona, model: model)
41+
42+
DiscourseAi::Summarization::FoldContent.new(
43+
bot,
44+
DiscourseAi::Summarization::Strategies::ChatMessages.new(channel, time_window_in_hours),
45+
persist_summaries: false,
46+
)
3747
end
3848
end
3949
end

lib/summarization/fold_content.rb

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ module Summarization
99
# into a final version.
1010
#
1111
class FoldContent
12-
def initialize(llm, strategy, persist_summaries: true)
13-
@llm = llm
12+
def initialize(bot, strategy, persist_summaries: true)
13+
@bot = bot
1414
@strategy = strategy
1515
@persist_summaries = persist_summaries
1616
end
1717

18-
attr_reader :llm, :strategy
18+
attr_reader :bot, :strategy
1919

2020
# @param user { User } - User object used for auditing usage.
2121
# @param &on_partial_blk { Block - Optional } - The passed block will get called with the LLM partial response alongside a cancel function.
@@ -76,7 +76,7 @@ def delete_cached_summaries!
7676
attr_reader :persist_summaries
7777

7878
def llm_model
79-
llm.llm_model
79+
bot.llm.llm_model
8080
end
8181

8282
def content_to_summarize
@@ -118,20 +118,29 @@ def fold(items, summary, cursor, user, &on_partial_blk)
118118
end
119119
end
120120

121-
prompt =
122-
(
123-
if summary.blank?
124-
strategy.first_summary_prompt(iteration_content)
125-
else
126-
strategy.summary_extension_prompt(summary, iteration_content)
127-
end
128-
)
121+
context = {
122+
target_url: "#{Discourse.base_path}/t/-/#{strategy.target.id}",
123+
skip_tool_details: true,
124+
user: user,
125+
feature_name: strategy.feature,
126+
}
127+
context[:conversation_context] = (
128+
if summary.blank?
129+
strategy.first_summary_messages(iteration_content)
130+
else
131+
strategy.summary_extension_messages(summary, iteration_content)
132+
end
133+
)
129134

130135
if cursor == items.length
131-
llm.generate(prompt, user: user, feature_name: strategy.feature, &on_partial_blk)
136+
bot.reply(context, &text_only_update(&on_partial_blk))
132137
else
133-
latest_summary =
134-
llm.generate(prompt, user: user, max_tokens: 600, feature_name: strategy.feature)
138+
context[:max_tokens] = 600
139+
140+
latest_summary = +""
141+
update_blk = text_only_update(Proc.new { |partial, cancel| latest_summary << partial })
142+
bot.reply(context, &update_blk)
143+
135144
fold(items, latest_summary, cursor, user, &on_partial_blk)
136145
end
137146
end
@@ -159,6 +168,12 @@ def truncate(item)
159168

160169
item
161170
end
171+
172+
def text_only_update(&on_partial_blk)
173+
Proc.new do |partial, cancel, placeholder, type|
174+
on_partial_blk.call(partial, cancel) if type.blank?
175+
end
176+
end
162177
end
163178
end
164179
end

0 commit comments

Comments
 (0)