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

Commit 542f53c

Browse files
committed
FEATURE: smarter persona tethering
Splits persona permissions so you can allow a persona on: - chat dms - personal messages - topic mentions - chat channels (any combination is allowed) Allow operator to tether a persona to an llm. (Still TODO frontend changes)
1 parent 94010a5 commit 542f53c

File tree

7 files changed

+185
-62
lines changed

7 files changed

+185
-62
lines changed

app/models/ai_persona.rb

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# frozen_string_literal: true
22

33
class AiPersona < ActiveRecord::Base
4-
# TODO remove this line 01-11-2024
5-
self.ignored_columns = [:commands]
4+
# TODO remove this line 01-1-2025
5+
self.ignored_columns = [:commands, :allow_chat, :mentionable]
66

77
# places a hard limit, so per site we cache a maximum of 500 classes
88
MAX_PERSONAS_PER_SITE = 500
@@ -52,49 +52,54 @@ def self.persona_users(user: nil)
5252
persona_cache[:persona_users] ||= AiPersona
5353
.where(enabled: true)
5454
.joins(:user)
55-
.pluck(
56-
"ai_personas.id, users.id, users.username_lower, allowed_group_ids, default_llm, mentionable, allow_chat",
57-
)
58-
.map do |id, user_id, username, allowed_group_ids, default_llm, mentionable, allow_chat|
55+
.map do |persona|
5956
{
60-
id: id,
61-
user_id: user_id,
62-
username: username,
63-
allowed_group_ids: allowed_group_ids,
64-
default_llm: default_llm,
65-
mentionable: mentionable,
66-
allow_chat: allow_chat,
57+
id: persona.id,
58+
user_id: persona.user_id,
59+
username: persona.user.username_lower,
60+
allowed_group_ids: persona.allowed_group_ids,
61+
default_llm: persona.default_llm,
62+
force_default_llm: persona.force_default_llm,
63+
allow_chat_channel_mentions: persona.allow_chat_channel_mentions,
64+
allow_chat_direct_messages: persona.allow_chat_direct_messages,
65+
allow_topic_mentions: persona.allow_topic_mentions,
66+
allow_personal_messages: persona.allow_personal_messages,
6767
}
6868
end
6969

7070
if user
71-
persona_users.select { |mentionable| user.in_any_groups?(mentionable[:allowed_group_ids]) }
71+
persona_users.select { |user| user.in_any_groups?(mentionable[:allowed_group_ids]) }
7272
else
7373
persona_users
7474
end
7575
end
7676

77-
def self.allowed_chat(user: nil)
78-
personas = persona_cache[:allowed_chat] ||= persona_users.select { |u| u[:allow_chat] }
77+
def self.allowed_modalities(
78+
user: nil,
79+
allow_chat_channel_mentions: false,
80+
allow_chat_direct_messages: false,
81+
allow_topic_mentions: false,
82+
allow_personal_messages: false
83+
)
84+
index =
85+
"modality-#{allow_chat_channel_mentions}-#{allow_chat_direct_messages}-#{allow_topic_mentions}-#{allow_personal_messages}"
86+
87+
personas =
88+
persona_cache[index.to_sym] ||= persona_users.select do |persona|
89+
next true if allow_chat_channel_mentions && persona[:allow_chat_channel_mentions]
90+
next true if allow_chat_direct_messages && persona[:allow_chat_direct_messages]
91+
next true if allow_topic_mentions && persona[:allow_topic_mentions]
92+
next true if allow_personal_messages && persona[:allow_personal_messages]
93+
false
94+
end
95+
7996
if user
8097
personas.select { |u| user.in_any_groups?(u[:allowed_group_ids]) }
8198
else
8299
personas
83100
end
84101
end
85102

86-
def self.mentionables(user: nil)
87-
all_mentionables =
88-
persona_cache[:mentionables] ||= persona_users.select do |mentionable|
89-
mentionable[:mentionable]
90-
end
91-
if user
92-
all_mentionables.select { |mentionable| user.in_any_groups?(mentionable[:allowed_group_ids]) }
93-
else
94-
all_mentionables
95-
end
96-
end
97-
98103
after_commit :bump_cache
99104

100105
def bump_cache
@@ -113,7 +118,11 @@ def class_instance
113118
vision_max_pixels
114119
rag_conversation_chunks
115120
question_consolidator_llm
116-
allow_chat
121+
allow_chat_channel_mentions
122+
allow_chat_direct_messages
123+
allow_topic_mentions
124+
allow_personal_messages
125+
force_default_llm
117126
name
118127
description
119128
allowed_group_ids
@@ -243,7 +252,7 @@ def regenerate_rag_fragments
243252
private
244253

245254
def chat_preconditions
246-
if allow_chat && !default_llm
255+
if (allow_chat_channel_mentions || allow_chat_direct_messages) && !default_llm
247256
errors.add(:default_llm, I18n.t("discourse_ai.ai_bot.personas.default_llm_required"))
248257
end
249258
end
@@ -281,7 +290,6 @@ def ensure_not_system
281290
# temperature :float
282291
# top_p :float
283292
# user_id :integer
284-
# mentionable :boolean default(FALSE), not null
285293
# default_llm :text
286294
# max_context_posts :integer
287295
# max_post_context_tokens :integer
@@ -291,16 +299,15 @@ def ensure_not_system
291299
# rag_chunk_tokens :integer default(374), not null
292300
# rag_chunk_overlap_tokens :integer default(10), not null
293301
# rag_conversation_chunks :integer default(10), not null
294-
# role :enum default("bot"), not null
295-
# role_category_ids :integer default([]), not null, is an Array
296-
# role_tags :string default([]), not null, is an Array
297-
# role_group_ids :integer default([]), not null, is an Array
298-
# role_whispers :boolean default(FALSE), not null
299-
# role_max_responses_per_hour :integer default(50), not null
300302
# question_consolidator_llm :text
301-
# allow_chat :boolean default(FALSE), not null
302303
# tool_details :boolean default(TRUE), not null
303304
# tools :json not null
305+
# forced_tool_count :integer default(-1), not null
306+
# allow_chat_channel_mentions :boolean default(FALSE), not null
307+
# allow_chat_direct_messages :boolean default(FALSE), not null
308+
# allow_topic_mentions :boolean default(FALSE), not null
309+
# allow_personal_message :boolean default(TRUE), not null
310+
# force_default_llm :boolean default(FALSE), not null
304311
#
305312
# Indexes
306313
#
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
class AiPersonaChatTopicRefactor < ActiveRecord::Migration[7.1]
4+
def change
5+
add_column :ai_personas, :allow_chat_channel_mentions, :boolean, default: false, null: false
6+
add_column :ai_personas, :allow_chat_direct_messages, :boolean, default: false, null: false
7+
add_column :ai_personas, :allow_topic_mentions, :boolean, default: false, null: false
8+
add_column :ai_personas, :allow_personal_messages, :boolean, default: true, null: false
9+
add_column :ai_personas, :force_default_llm, :boolean, default: false, null: false
10+
11+
execute <<~SQL
12+
UPDATE ai_personas
13+
SET allow_chat_channel_mentions = mentionable, allow_chat_direct_messages = true
14+
WHERE allow_chat = true
15+
SQL
16+
17+
execute <<~SQL
18+
UPDATE ai_personas
19+
SET allow_topic_mentions = true
20+
WHERE mentionable = true
21+
SQL
22+
end
23+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
class AiPersonaPostMigrateDropCols < ActiveRecord::Migration[7.1]
3+
def change
4+
remove_columns :ai_personas, :allow_chat
5+
remove_columns :ai_personas, :mentionable
6+
end
7+
end

lib/ai_bot/entry_point.rb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,7 @@ class EntryPoint
88
Bot = Struct.new(:id, :name, :llm)
99

1010
def self.all_bot_ids
11-
mentionable_persona_user_ids =
12-
AiPersona.mentionables.map { |mentionable| mentionable[:user_id] }
13-
mentionable_bot_users = LlmModel.joins(:user).pluck("users.id")
14-
15-
mentionable_bot_users + mentionable_persona_user_ids
11+
AiPersona.persona_users.map { |persona| persona[:user_id] }
1612
end
1713

1814
def self.find_participant_in(participant_ids)
@@ -140,7 +136,7 @@ def inject_into(plugin)
140136
{
141137
"id" => persona_user[:user_id],
142138
"username" => persona_user[:username],
143-
"mentionable" => persona_user[:mentionable],
139+
"force_default_llm" => persona_user[:force_default_llm],
144140
"is_persona" => true,
145141
}
146142
end,

lib/ai_bot/personas/persona.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,15 @@ def question_consolidator_llm
2121
nil
2222
end
2323

24-
def allow_chat
24+
def force_default_llm
25+
false
26+
end
27+
28+
def allow_chat_channel_mentions
29+
false
30+
end
31+
32+
def allow_chat_direct_messages
2533
false
2634
end
2735

lib/ai_bot/playground.rb

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,34 @@ class Playground
1111

1212
def self.find_chat_persona(message, channel, user)
1313
if channel.direct_message_channel?
14-
AiPersona.allowed_chat.find do |p|
15-
p[:user_id].in?(channel.allowed_user_ids) && (user.group_ids & p[:allowed_group_ids])
16-
end
14+
AiPersona
15+
.allowed_modalities(allow_chat_direct_messages: true)
16+
.find do |p|
17+
p[:user_id].in?(channel.allowed_user_ids) && (user.group_ids & p[:allowed_group_ids])
18+
end
1719
else
1820
# let's defer on the parse if there is no @ in the message
1921
if message.message.include?("@")
2022
mentions = message.parsed_mentions.parsed_direct_mentions
2123
if mentions.present?
22-
AiPersona.allowed_chat.find do |p|
23-
p[:username].in?(mentions) && (user.group_ids & p[:allowed_group_ids])
24-
end
24+
AiPersona
25+
.allowed_modalities(allow_chat_channel_mentions: true)
26+
.find { |p| p[:username].in?(mentions) && (user.group_ids & p[:allowed_group_ids]) }
2527
end
2628
end
2729
end
2830
end
2931

3032
def self.schedule_chat_reply(message, channel, user, context)
3133
return if !SiteSetting.ai_bot_enabled
32-
return if AiPersona.allowed_chat.blank?
33-
return if AiPersona.allowed_chat.any? { |m| m[:user_id] == user.id }
34+
35+
all_chat =
36+
AiPersona.allowed_modalities(
37+
allow_chat_channel_mentions: true,
38+
allow_chat_direct_messages: true,
39+
)
40+
return if all_chat.blank?
41+
return if all_chat.any? { |m| m[:user_id] == user.id }
3442

3543
persona = find_chat_persona(message, channel, user)
3644
return if !persona
@@ -56,15 +64,22 @@ def self.is_bot_user_id?(user_id)
5664

5765
def self.schedule_reply(post)
5866
return if is_bot_user_id?(post.user_id)
67+
mentionables = nil
5968

60-
bot_ids = LlmModel.joins(:user).pluck("users.id")
61-
mentionables = AiPersona.mentionables(user: post.user)
69+
if post.topic.private_message?
70+
mentionables = AiPersona.allowed_modalities(user: post.user, allow_personal_messages: true)
71+
else
72+
mentionables = AiPersona.allowed_modalities(user: post.user, allow_topic_mentions: true)
73+
end
6274

6375
bot_user = nil
6476
mentioned = nil
6577

78+
all_llm_user_ids = LlmModel.joins(:user).pluck("users.id")
79+
6680
if post.topic.private_message?
67-
bot_user = post.topic.topic_allowed_users.where(user_id: bot_ids).first&.user
81+
# this is an edge case, you started a PM with a different bot
82+
bot_user = post.topic.topic_allowed_users.where(user_id: all_llm_user_ids).first&.user
6883
bot_user ||=
6984
post
7085
.topic
@@ -114,6 +129,10 @@ def self.schedule_reply(post)
114129

115130
persona ||= DiscourseAi::AiBot::Personas::General
116131

132+
if persona && persona.force_default_llm
133+
bot_user = User.find(persona.user_id)
134+
end
135+
117136
bot = DiscourseAi::AiBot::Bot.as(bot_user, persona: persona.new)
118137
new(bot).update_playground_with(post)
119138
end

0 commit comments

Comments
 (0)