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

Commit e49223f

Browse files
committed
transplant method to message builder
1 parent 90ec441 commit e49223f

File tree

2 files changed

+188
-158
lines changed

2 files changed

+188
-158
lines changed

lib/ai_bot/playground.rb

Lines changed: 40 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -227,87 +227,14 @@ def update_playground_with(post)
227227
schedule_bot_reply(post) if can_attach?(post)
228228
end
229229

230-
def post_prompt_messages(post, style: nil)
231-
# Pay attention to the `post_number <= ?` here.
232-
# We want to inject the last post as context because they are translated differently.
233-
234-
# also setting default to 40, allowing huge contexts costs lots of tokens
235-
max_posts = 40
236-
if bot.persona.class.respond_to?(:max_context_posts)
237-
max_posts = bot.persona.class.max_context_posts || 40
238-
end
239-
240-
post_types = [Post.types[:regular]]
241-
post_types << Post.types[:whisper] if post.post_type == Post.types[:whisper]
242-
243-
context =
244-
post
245-
.topic
246-
.posts
247-
.joins(:user)
248-
.joins("LEFT JOIN post_custom_prompts ON post_custom_prompts.post_id = posts.id")
249-
.where("post_number <= ?", post.post_number)
250-
.order("post_number desc")
251-
.where("post_type in (?)", post_types)
252-
.limit(max_posts)
253-
.pluck(
254-
"posts.raw",
255-
"users.username",
256-
"post_custom_prompts.custom_prompt",
257-
"(
258-
SELECT array_agg(ref.upload_id)
259-
FROM upload_references ref
260-
WHERE ref.target_type = 'Post' AND ref.target_id = posts.id
261-
) as upload_ids",
262-
)
263-
264-
builder = DiscourseAi::Completions::PromptMessagesBuilder.new
265-
builder.topic = post.topic
266-
267-
context.reverse_each do |raw, username, custom_prompt, upload_ids|
268-
custom_prompt_translation =
269-
Proc.new do |message|
270-
# We can't keep backwards-compatibility for stored functions.
271-
# Tool syntax requires a tool_call_id which we don't have.
272-
if message[2] != "function"
273-
custom_context = {
274-
content: message[0],
275-
type: message[2].present? ? message[2].to_sym : :model,
276-
}
277-
278-
custom_context[:id] = message[1] if custom_context[:type] != :model
279-
custom_context[:name] = message[3] if message[3]
280-
281-
thinking = message[4]
282-
custom_context[:thinking] = thinking if thinking
283-
284-
builder.push(**custom_context)
285-
end
286-
end
287-
288-
if custom_prompt.present?
289-
custom_prompt.each(&custom_prompt_translation)
290-
else
291-
context = {
292-
content: raw,
293-
type: (available_bot_usernames.include?(username) ? :model : :user),
294-
}
295-
296-
context[:id] = username if context[:type] == :user
297-
298-
if upload_ids.present? && context[:type] == :user && bot.persona.class.vision_enabled
299-
context[:upload_ids] = upload_ids.compact
300-
end
301-
302-
builder.push(**context)
303-
end
304-
end
305-
306-
builder.to_a(style: style || (post.topic.private_message? ? :bot : :topic))
307-
end
308-
309230
def title_playground(post, user)
310-
messages = post_prompt_messages(post)
231+
messages =
232+
DiscourseAi::Completions::PromptMessagesBuilder.messages_from_post(
233+
post,
234+
max_posts: 5,
235+
bot_usernames: available_bot_usernames,
236+
include_uploads: bot.persona.class.vision_enabled,
237+
)
311238

312239
bot
313240
.get_updated_title(messages, post, user)
@@ -326,94 +253,36 @@ def title_playground(post, user)
326253
)
327254
end
328255

329-
def chat_prompt_messages(message, channel, persona_user, context_post_ids)
330-
has_vision = bot.persona.class.vision_enabled
331-
include_thread_titles = !channel.direct_message_channel? && !message.thread_id
256+
def reply_to_chat_message(message, channel, context_post_ids)
257+
persona_user = User.find(bot.persona.class.user_id)
332258

333-
current_id = message.id
334-
if !channel.direct_message_channel?
335-
# we are interacting via mentions ... strip mention
336-
instruction_message = message.message.gsub(/@#{bot.bot_user.username}/i, "").strip
337-
end
259+
participants = channel.user_chat_channel_memberships.map { |m| m.user.username }
338260

339-
messages = nil
261+
context_post_ids = nil if !channel.direct_message_channel?
340262

341-
max_messages = 40
263+
max_chat_messages = 40
342264
if bot.persona.class.respond_to?(:max_context_posts)
343-
max_messages = bot.persona.class.max_context_posts || 40
265+
max_chat_messages = bot.persona.class.max_context_posts || 40
344266
end
345267

346-
if !message.thread_id && channel.direct_message_channel?
347-
messages = [message]
348-
elsif !channel.direct_message_channel? && !message.thread_id
349-
messages =
350-
Chat::Message
351-
.joins("left join chat_threads on chat_threads.id = chat_messages.thread_id")
352-
.where(chat_channel_id: channel.id)
353-
.where(
354-
"chat_messages.thread_id IS NULL OR chat_threads.original_message_id = chat_messages.id",
355-
)
356-
.order(id: :desc)
357-
.limit(max_messages)
358-
.to_a
359-
.reverse
360-
end
361-
362-
messages ||=
363-
ChatSDK::Thread.last_messages(
364-
thread_id: message.thread_id,
365-
guardian: Discourse.system_user.guardian,
366-
page_size: max_messages,
367-
)
368-
369-
builder = DiscourseAi::Completions::PromptMessagesBuilder.new
370-
371-
guardian = Guardian.new(message.user)
372-
if context_post_ids
373-
builder.set_chat_context_posts(context_post_ids, guardian, include_uploads: has_vision)
374-
end
375-
376-
messages.each do |m|
377-
# restore stripped message
378-
m.message = instruction_message if m.id == current_id && instruction_message
379-
380-
if available_bot_user_ids.include?(m.user_id)
381-
builder.push(type: :model, content: m.message)
382-
else
383-
upload_ids = nil
384-
upload_ids = m.uploads.map(&:id) if has_vision && m.uploads.present?
385-
mapped_message = m.message
386-
387-
thread_title = nil
388-
thread_title = m.thread&.title if include_thread_titles && m.thread_id
389-
mapped_message = "(#{thread_title})\n#{m.message}" if thread_title
390-
391-
builder.push(
392-
type: :user,
393-
content: mapped_message,
394-
name: m.user.username,
395-
upload_ids: upload_ids,
396-
)
397-
end
268+
if !channel.direct_message_channel?
269+
# we are interacting via mentions ... strip mention
270+
instruction_message = message.message.gsub(/@#{bot.bot_user.username}/i, "").strip
398271
end
399272

400-
builder.to_a(
401-
limit: max_messages,
402-
style: channel.direct_message_channel? ? :chat_with_context : :chat,
403-
)
404-
end
405-
406-
def reply_to_chat_message(message, channel, context_post_ids)
407-
persona_user = User.find(bot.persona.class.user_id)
408-
409-
participants = channel.user_chat_channel_memberships.map { |m| m.user.username }
410-
411-
context_post_ids = nil if !channel.direct_message_channel?
412-
413273
context =
414274
BotContext.new(
415275
participants: participants,
416-
messages: chat_prompt_messages(message, channel, persona_user, context_post_ids),
276+
messages:
277+
DiscourseAi::Completions::PromptMessagesBuilder.messages_from_chat(
278+
message,
279+
channel: channel,
280+
context_post_ids: context_post_ids,
281+
include_uploads: bot.persona.class.vision_enabled,
282+
max_messages: max_chat_messages,
283+
bot_user_ids: available_bot_user_ids,
284+
instruction_message: instruction_message,
285+
),
417286
user: message.user,
418287
skip_tool_details: true,
419288
)
@@ -493,11 +362,24 @@ def reply_to(
493362
end
494363
)
495364

365+
# safeguard
366+
max_context_posts = 40
367+
if bot.persona.class.respond_to?(:max_context_posts)
368+
max_context_posts = bot.persona.class.max_context_posts || 40
369+
end
370+
496371
context =
497372
BotContext.new(
498373
post: post,
499374
custom_instructions: custom_instructions,
500-
messages: post_prompt_messages(post, style: context_style),
375+
messages:
376+
DiscourseAi::Completions::PromptMessagesBuilder.messages_from_post(
377+
post,
378+
style: context_style,
379+
max_posts: max_context_posts,
380+
include_uploads: bot.persona.class.vision_enabled,
381+
bot_usernames: available_bot_usernames,
382+
),
501383
)
502384

503385
reply_user = bot.bot_user

lib/completions/prompt_messages_builder.rb

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,154 @@ class PromptMessagesBuilder
99
attr_reader :chat_context_post_upload_ids
1010
attr_accessor :topic
1111

12+
def self.messages_from_chat(
13+
message,
14+
channel:,
15+
context_post_ids:,
16+
max_messages:,
17+
include_uploads:,
18+
bot_user_ids:,
19+
instruction_message: nil
20+
)
21+
include_thread_titles = !channel.direct_message_channel? && !message.thread_id
22+
23+
current_id = message.id
24+
messages = nil
25+
26+
if !message.thread_id && channel.direct_message_channel?
27+
messages = [message]
28+
elsif !channel.direct_message_channel? && !message.thread_id
29+
messages =
30+
Chat::Message
31+
.joins("left join chat_threads on chat_threads.id = chat_messages.thread_id")
32+
.where(chat_channel_id: channel.id)
33+
.where(
34+
"chat_messages.thread_id IS NULL OR chat_threads.original_message_id = chat_messages.id",
35+
)
36+
.order(id: :desc)
37+
.limit(max_messages)
38+
.to_a
39+
.reverse
40+
end
41+
42+
messages ||=
43+
ChatSDK::Thread.last_messages(
44+
thread_id: message.thread_id,
45+
guardian: Discourse.system_user.guardian,
46+
page_size: max_messages,
47+
)
48+
49+
builder = new
50+
51+
guardian = Guardian.new(message.user)
52+
if context_post_ids
53+
builder.set_chat_context_posts(
54+
context_post_ids,
55+
guardian,
56+
include_uploads: include_uploads,
57+
)
58+
end
59+
60+
messages.each do |m|
61+
# restore stripped message
62+
m.message = instruction_message if m.id == current_id && instruction_message
63+
64+
if bot_user_ids.include?(m.user_id)
65+
builder.push(type: :model, content: m.message)
66+
else
67+
upload_ids = nil
68+
upload_ids = m.uploads.map(&:id) if include_uploads && m.uploads.present?
69+
mapped_message = m.message
70+
71+
thread_title = nil
72+
thread_title = m.thread&.title if include_thread_titles && m.thread_id
73+
mapped_message = "(#{thread_title})\n#{m.message}" if thread_title
74+
75+
builder.push(
76+
type: :user,
77+
content: mapped_message,
78+
name: m.user.username,
79+
upload_ids: upload_ids,
80+
)
81+
end
82+
end
83+
84+
builder.to_a(
85+
limit: max_messages,
86+
style: channel.direct_message_channel? ? :chat_with_context : :chat,
87+
)
88+
end
89+
90+
def self.messages_from_post(post, style: nil, max_posts:, bot_usernames:, include_uploads:)
91+
# Pay attention to the `post_number <= ?` here.
92+
# We want to inject the last post as context because they are translated differently.
93+
94+
post_types = [Post.types[:regular]]
95+
post_types << Post.types[:whisper] if post.post_type == Post.types[:whisper]
96+
97+
context =
98+
post
99+
.topic
100+
.posts
101+
.joins(:user)
102+
.joins("LEFT JOIN post_custom_prompts ON post_custom_prompts.post_id = posts.id")
103+
.where("post_number <= ?", post.post_number)
104+
.order("post_number desc")
105+
.where("post_type in (?)", post_types)
106+
.limit(max_posts)
107+
.pluck(
108+
"posts.raw",
109+
"users.username",
110+
"post_custom_prompts.custom_prompt",
111+
"(
112+
SELECT array_agg(ref.upload_id)
113+
FROM upload_references ref
114+
WHERE ref.target_type = 'Post' AND ref.target_id = posts.id
115+
) as upload_ids",
116+
)
117+
118+
builder = new
119+
builder.topic = post.topic
120+
121+
context.reverse_each do |raw, username, custom_prompt, upload_ids|
122+
custom_prompt_translation =
123+
Proc.new do |message|
124+
# We can't keep backwards-compatibility for stored functions.
125+
# Tool syntax requires a tool_call_id which we don't have.
126+
if message[2] != "function"
127+
custom_context = {
128+
content: message[0],
129+
type: message[2].present? ? message[2].to_sym : :model,
130+
}
131+
132+
custom_context[:id] = message[1] if custom_context[:type] != :model
133+
custom_context[:name] = message[3] if message[3]
134+
135+
thinking = message[4]
136+
custom_context[:thinking] = thinking if thinking
137+
138+
builder.push(**custom_context)
139+
end
140+
end
141+
142+
if custom_prompt.present?
143+
custom_prompt.each(&custom_prompt_translation)
144+
else
145+
context = { content: raw, type: (bot_usernames.include?(username) ? :model : :user) }
146+
147+
context[:id] = username if context[:type] == :user
148+
149+
if upload_ids.present? && context[:type] == :user && include_uploads
150+
context[:upload_ids] = upload_ids.compact
151+
end
152+
153+
builder.push(**context)
154+
end
155+
end
156+
157+
builder.to_a(style: style || (post.topic.private_message? ? :bot : :topic))
158+
end
159+
12160
def initialize
13161
@raw_messages = []
14162
end

0 commit comments

Comments
 (0)