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

Commit 0d60aca

Browse files
authored
FEATURE: Personas powered summaries. (#1232)
* REFACTOR: Move personas into it's own module. * WIP: Use personas for summarization * Prioritize persona default LLM or fallback to newest one * Simplify summarization strategy * Keep ai_sumarization_model as a fallback
1 parent 32da999 commit 0d60aca

31 files changed

+331
-351
lines changed

app/controllers/discourse_ai/ai_bot/bot_controller.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ def show_bot_username
4747

4848
def discover
4949
ai_persona =
50-
AiPersona.all_personas.find do |persona|
51-
persona.id == SiteSetting.ai_bot_discover_persona.to_i
52-
end
50+
AiPersona
51+
.all_personas(enabled_only: false)
52+
.find { |persona| persona.id == SiteSetting.ai_bot_discover_persona.to_i }
5353

5454
if ai_persona.nil? || !current_user.in_any_groups?(ai_persona.allowed_group_ids.to_a)
5555
raise Discourse::InvalidAccess.new

app/jobs/regular/stream_discover_reply.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ def execute(args)
99
return if (query = args[:query]).blank?
1010

1111
ai_persona_klass =
12-
AiPersona.all_personas.find do |persona|
13-
persona.id == SiteSetting.ai_bot_discover_persona.to_i
14-
end
12+
AiPersona
13+
.all_personas(enabled_only: false)
14+
.find { |persona| persona.id == SiteSetting.ai_bot_discover_persona.to_i }
1515

1616
if ai_persona_klass.nil? || !user.in_any_groups?(ai_persona_klass.allowed_group_ids.to_a)
1717
return

app/models/ai_persona.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,18 @@ 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(enabled_only: 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+
if enabled_only
57+
persona_cache[:value].select { |p| p.enabled }
58+
else
59+
persona_cache[:value]
60+
end
5661
end
5762

5863
def self.persona_users(user: nil)
@@ -176,6 +181,7 @@ def class_instance
176181
description
177182
allowed_group_ids
178183
tool_details
184+
enabled
179185
]
180186

181187
instance_attributes = {}

config/locales/server.en.yml

Lines changed: 7 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}'"
@@ -452,6 +458,7 @@ en:
452458

453459
llm:
454460
configuration:
461+
create_llm: "You need to setup an LLM before enabling this feature"
455462
disable_module_first: "You have to disable %{setting} first."
456463
set_llm_first: "Set %{setting} first"
457464
model_unreachable: "We couldn't get a response from this model. Check your settings first."

config/settings.yml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,18 +240,30 @@ discourse_ai:
240240
type: enum
241241
enum: "DiscourseAi::Configuration::LlmEnumerator"
242242
validator: "DiscourseAi::Configuration::LlmValidator"
243+
hidden: true
244+
ai_summarization_persona:
245+
default: "-11"
246+
type: enum
247+
enum: "DiscourseAi::Configuration::PersonaEnumerator"
248+
243249
ai_pm_summarization_allowed_groups:
244250
type: group_list
245251
list_type: compact
246252
default: ""
247-
ai_custom_summarization_allowed_groups:
253+
ai_custom_summarization_allowed_groups: # Deprecated. TODO(roman): Remove 2025-09-01
248254
type: group_list
249255
list_type: compact
250256
default: "3|13" # 3: @staff, 13: @trust_level_3
257+
hidden: true
251258
ai_summary_gists_enabled:
252259
default: false
253260
hidden: true
254-
ai_summary_gists_allowed_groups:
261+
ai_summary_gists_persona:
262+
default: "-12"
263+
type: enum
264+
enum: "DiscourseAi::Configuration::PersonaEnumerator"
265+
hidden: true
266+
ai_summary_gists_allowed_groups: # Deprecated. TODO(roman): Remove 2025-09-01
255267
type: group_list
256268
list_type: compact
257269
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+
5+
def from_setting(setting_name)
6+
DB.query_single(
7+
"SELECT value FROM site_settings WHERE name = :setting_name",
8+
setting_name: setting_name,
9+
)
10+
end
11+
312
DiscourseAi::Personas::Persona.system_personas.each do |persona_class, id|
413
persona = AiPersona.find_by(id: id)
514
if !persona
615
persona = AiPersona.new
716
persona.id = id
17+
818
if persona_class == DiscourseAi::Personas::WebArtifactCreator
919
# this is somewhat sensitive, so we default it to staff
1020
persona.allowed_group_ids = [Group::AUTO_GROUPS[:staff]]
21+
elsif summarization_personas.include?(persona_class)
22+
# Copy group permissions from site settings.
23+
default_groups = [Group::AUTO_GROUPS[:staff], Group::AUTO_GROUPS[:trust_level_3]]
24+
25+
setting_name = "ai_custom_summarization_allowed_groups"
26+
if persona_class == DiscourseAi::Personas::ShortSummarizer
27+
setting_name = "ai_summary_gists_allowed_groups"
28+
default_groups = [] # Blank == everyone
29+
end
30+
31+
persona.allowed_group_ids = from_setting(setting_name).first&.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/configuration/llm_dependency_validator.rb

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,27 @@ def initialize(opts = {})
1010
def valid_value?(val)
1111
return true if val == "f"
1212

13-
@llm_dependency_setting_name =
14-
DiscourseAi::Configuration::LlmValidator.new.choose_llm_setting_for(@opts[:name])
13+
if @opts[:name] == :ai_summarization_enabled
14+
has_llms = LlmModel.count > 0
15+
@no_llms_configured = !has_llms
16+
has_llms
17+
else
18+
@llm_dependency_setting_name =
19+
DiscourseAi::Configuration::LlmValidator.new.choose_llm_setting_for(@opts[:name])
1520

16-
SiteSetting.public_send(@llm_dependency_setting_name).present?
21+
SiteSetting.public_send(@llm_dependency_setting_name).present?
22+
end
1723
end
1824

1925
def error_message
20-
I18n.t(
21-
"discourse_ai.llm.configuration.set_llm_first",
22-
setting: @llm_dependency_setting_name,
23-
)
26+
if @llm_dependency_setting_name
27+
I18n.t(
28+
"discourse_ai.llm.configuration.set_llm_first",
29+
setting: @llm_dependency_setting_name,
30+
)
31+
elsif @no_llms_configured
32+
I18n.t("discourse_ai.llm.configuration.create_llm")
33+
end
2434
end
2535
end
2636
end

lib/configuration/llm_enumerator.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ def self.global_usage
2626
end
2727

2828
if SiteSetting.ai_summarization_enabled
29-
model_id = SiteSetting.ai_summarization_model.split(":").last.to_i
29+
summarization_persona = AiPersona.find_by(id: SiteSetting.ai_summarization_persona)
30+
model_id = summarization_persona.default_llm_id || LlmModel.last&.id
31+
3032
rval[model_id] << { type: :ai_summarization }
3133
end
3234

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(enabled_only: false)
15+
.map { |persona| { name: persona.name, value: persona.id } }
1416
end
1517
end
1618
end

lib/discord/bot/persona_replier.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class PersonaReplier < Base
66
def initialize(body)
77
@persona =
88
AiPersona
9-
.all_personas
9+
.all_personas(enabled_only: false)
1010
.find { |persona| persona.id == SiteSetting.ai_discord_search_persona.to_i }
1111
.new
1212
@bot =

0 commit comments

Comments
 (0)