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

Commit b3d4f0c

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

File tree

23 files changed

+275
-261
lines changed

23 files changed

+275
-261
lines changed

app/models/ai_persona.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,16 @@ 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)
5352
.all
5453
.limit(MAX_PERSONAS_PER_SITE)
5554
.map(&:class_instance)
55+
56+
return persona_cache[:value] if !only_enabled
57+
58+
persona_cache[:value].select { |p| p.enabled }
5659
end
5760

5861
def self.persona_users(user: nil)

config/locales/server.en.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,12 @@ en:
303303
web_artifact_creator:
304304
name: "Web Artifact Creator"
305305
description: "AI Bot specialized in creating interactive web artifacts"
306+
summarizer:
307+
name: "Summarizer"
308+
description: "Default persona used to power AI summaries."
309+
short_summarizer:
310+
name: "Summarizer (short form)"
311+
description: "Default persona used to power AI short summaries for topic lists' items."
306312
topic_not_found: "Summary unavailable, topic not found!"
307313
summarizing: "Summarizing topic"
308314
searching: "Searching for: '%{query}'"

config/settings.yml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,18 +240,29 @@ 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
246251
default: ""
247-
ai_custom_summarization_allowed_groups:
252+
ai_custom_summarization_allowed_groups: # Deprecated. TODO(roman): Remove 2025-09-01
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
254-
ai_summary_gists_allowed_groups:
260+
ai_summary_gists_persona:
261+
default: "-12"
262+
type: enum
263+
enum: "DiscourseAi::Configuration::PersonaEnumerator"
264+
hidden: true
265+
ai_summary_gists_allowed_groups: # Deprecated. TODO(roman): Remove 2025-09-01
255266
type: group_list
256267
list_type: compact
257268
default: "0" #everyone

db/fixtures/personas/603_ai_personas.rb

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,39 @@
11
# frozen_string_literal: true
22

3+
summarization_personas = [DiscourseAi::Personas::Summarizer, DiscourseAi::Personas::ShortSummarizer]
4+
35
DiscourseAi::Personas::Persona.system_personas.each do |persona_class, id|
46
persona = AiPersona.find_by(id: id)
57
if !persona
68
persona = AiPersona.new
79
persona.id = id
10+
811
if persona_class == DiscourseAi::Personas::WebArtifactCreator
912
# this is somewhat sensitive, so we default it to staff
1013
persona.allowed_group_ids = [Group::AUTO_GROUPS[:staff]]
14+
elsif summarization_personas.include?(persona_class)
15+
# Copy group permissions from site settings.
16+
default_groups = [Group::AUTO_GROUPS[:staff], Group::AUTO_GROUPS[:trust_level_3]]
17+
18+
setting_name = "ai_custom_summarization_allowed_groups"
19+
if persona_class == DiscourseAi::Personas::ShortSummarizer
20+
setting_name = "ai_summary_gists_allowed_groups"
21+
default_groups = [] # Blank == everyone
22+
end
23+
24+
persona.allowed_group_ids =
25+
DB
26+
.query_single(
27+
"SELECT value FROM site_settings WHERE name = :setting_name",
28+
setting_name: setting_name,
29+
)
30+
.first
31+
&.split("|") || default_groups
1132
else
1233
persona.allowed_group_ids = [Group::AUTO_GROUPS[:trust_level_0]]
1334
end
14-
persona.enabled = true
35+
36+
persona.enabled = !summarization_personas.include?(persona_class)
1537
persona.priority = true if persona_class == DiscourseAi::Personas::General
1638
end
1739

@@ -22,16 +44,16 @@
2244
persona_class.name + SecureRandom.hex,
2345
]
2446
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
47+
SELECT guess_name
48+
FROM (
49+
SELECT unnest(Array[?]) AS guess_name
50+
FROM (SELECT 1) as t
51+
) x
52+
LEFT JOIN ai_personas ON ai_personas.name = x.guess_name AND ai_personas.id <> ?
53+
WHERE ai_personas.id IS NULL
54+
ORDER BY x.guess_name ASC
55+
LIMIT 1
56+
SQL
3557

3658
persona.description = persona_class.description
3759

lib/ai_bot/playground.rb

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -283,44 +283,6 @@ def title_playground(post, user)
283283
title: new_title.sub(/\A"/, "").sub(/"\Z/, ""),
284284
)
285285

286-
system_insts = <<~TEXT.strip
287-
You are titlebot. Given a conversation, you will suggest a title.
288-
289-
- You will never respond with anything but the suggested title.
290-
- You will always match the conversation language in your title suggestion.
291-
- Title will capture the essence of the conversation.
292-
TEXT
293-
294-
instruction = <<~TEXT.strip
295-
Given the following conversation:
296-
297-
{{{
298-
#{conversation}
299-
}}}
300-
301-
Reply only with a title that is 7 words or less.
302-
TEXT
303-
304-
title_prompt =
305-
DiscourseAi::Completions::Prompt.new(
306-
system_insts,
307-
messages: [type: :user, content: instruction],
308-
topic_id: post.topic_id,
309-
)
310-
311-
title =
312-
bot
313-
.llm
314-
.generate(title_prompt, user: user, feature_name: "bot_title")
315-
.strip
316-
.split("\n")
317-
.last
318-
319-
PostRevisor.new(post.topic.first_post, post.topic).revise!(
320-
bot.bot_user,
321-
title: title.sub(/\A"/, "").sub(/"\Z/, ""),
322-
)
323-
324286
allowed_users = post.topic.topic_allowed_users.pluck(:user_id)
325287
MessageBus.publish(
326288
"/discourse-ai/ai-bot/topic/#{post.topic.id}",

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/guardian_extensions.rb

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,26 @@ def can_see_summary?(target)
2424
def can_see_gists?
2525
return false if !SiteSetting.ai_summarization_enabled
2626
return false if !SiteSetting.ai_summary_gists_enabled
27-
if SiteSetting.ai_summary_gists_allowed_groups.to_s == Group::AUTO_GROUPS[:everyone].to_s
28-
return true
27+
28+
if (ai_persona = AiPersona.find_by(id: SiteSetting.ai_summary_gists_persona)).blank?
29+
return false
2930
end
31+
persona_groups = ai_persona.allowed_group_ids.to_a
32+
return true if persona_groups.empty?
3033
return false if anonymous?
31-
return false if SiteSetting.ai_summary_gists_allowed_groups_map.empty?
3234

33-
SiteSetting.ai_summary_gists_allowed_groups_map.any? do |group_id|
34-
user.group_ids.include?(group_id)
35-
end
35+
ai_persona.allowed_group_ids.to_a.any? { |group_id| user.group_ids.include?(group_id) }
3636
end
3737

3838
def can_request_summary?
3939
return false if anonymous?
4040

4141
user_group_ids = user.group_ids
42-
SiteSetting.ai_custom_summarization_allowed_groups_map.any? do |group_id|
43-
user_group_ids.include?(group_id)
42+
if (ai_persona = AiPersona.find_by(id: SiteSetting.ai_summarization_persona)).blank?
43+
return false
4444
end
45+
46+
ai_persona.allowed_group_ids.to_a.any? { |group_id| user.group_ids.include?(group_id) }
4547
end
4648

4749
def can_debug_ai_bot_conversation?(target)

lib/personas/bot.rb

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def initialize(bot_user, persona, model = nil)
2828
attr_accessor :persona
2929

3030
def llm
31-
@llm ||= DiscourseAi::Completions::Llm.proxy(model)
31+
DiscourseAi::Completions::Llm.proxy(model)
3232
end
3333

3434
def force_tool_if_needed(prompt, context)
@@ -51,7 +51,7 @@ def force_tool_if_needed(prompt, context)
5151
end
5252
end
5353

54-
def reply(context, &update_blk)
54+
def reply(context, llm_args: {}, &update_blk)
5555
unless context.is_a?(BotContext)
5656
raise ArgumentError, "context must be an instance of BotContext"
5757
end
@@ -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] = llm_args[:max_tokens] if llm_args[: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,
8889
partial_tool_calls: allow_partial_tool_calls,
8990
output_thinking: true,
9091
**llm_kwargs,
@@ -246,13 +247,8 @@ def process_tool(
246247
raw_context << [invocation_result_json, tool_call_id, "tool", tool.name]
247248
end
248249

249-
<<<<<<< HEAD
250-
def invoke_tool(tool, llm, cancel, context, &update_blk)
251-
show_placeholder = !context.skip_tool_details && !tool.class.allow_partial_tool_calls?
252-
=======
253250
def invoke_tool(tool, cancel, context, &update_blk)
254-
show_placeholder = !context[:skip_tool_details] && !tool.class.allow_partial_tool_calls?
255-
>>>>>>> 5bfe2314 (REFACTOR: Move personas into it's own module.)
251+
show_placeholder = !context.skip_tool_details && !tool.class.allow_partial_tool_calls?
256252

257253
update_blk.call("", cancel, build_placeholder(tool.summary, "")) if show_placeholder
258254

lib/personas/bot_context.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ class BotContext
1414
:chosen_tools,
1515
:message_id,
1616
:channel_id,
17-
:context_post_ids
17+
:context_post_ids,
18+
:feature_name,
19+
:resource_url
1820

1921
def initialize(
2022
post: nil,
@@ -29,7 +31,9 @@ def initialize(
2931
time: nil,
3032
message_id: nil,
3133
channel_id: nil,
32-
context_post_ids: nil
34+
context_post_ids: nil,
35+
feature_name: "bot",
36+
resource_url: nil
3337
)
3438
@participants = participants
3539
@user = user
@@ -45,6 +49,7 @@ def initialize(
4549
@site_title = site_title
4650
@site_description = site_description
4751
@time = time
52+
@feature_name = feature_name
4853

4954
if post
5055
@post_id = post.id
@@ -56,7 +61,7 @@ def initialize(
5661
end
5762

5863
# these are strings that can be safely interpolated into templates
59-
TEMPLATE_PARAMS = %w[time site_url site_title site_description participants]
64+
TEMPLATE_PARAMS = %w[time site_url site_title site_description participants resource_url]
6065

6166
def lookup_template_param(key)
6267
public_send(key.to_sym) if TEMPLATE_PARAMS.include?(key)
@@ -100,6 +105,8 @@ def to_json
100105
site_title: @site_title,
101106
site_description: @site_description,
102107
skip_tool_details: @skip_tool_details,
108+
feature_name: @feature_name,
109+
resource_url: @resource_url,
103110
}
104111
end
105112
end

lib/personas/persona.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ def system_personas
4444
DiscourseHelper => -8,
4545
GithubHelper => -9,
4646
WebArtifactCreator => -10,
47+
Summarizer => -11,
48+
ShortSummarizer => -12,
4749
}
4850
end
4951

0 commit comments

Comments
 (0)