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

Commit 94c026d

Browse files
committed
FEATURE: improve context management
1. Add age of post to topic context (1 month ago, 1 year ago, etc) 2. Refactor code for simplicity 3. Fix handling of post context in DMs which was not using new handling of uploads
1 parent 67e3a61 commit 94c026d

File tree

2 files changed

+213
-83
lines changed

2 files changed

+213
-83
lines changed

lib/completions/prompt_messages_builder.rb

Lines changed: 141 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ class PromptMessagesBuilder
66
MAX_CHAT_UPLOADS = 5
77
MAX_TOPIC_UPLOADS = 5
88
attr_reader :chat_context_posts
9-
attr_reader :chat_context_post_upload_ids
109
attr_accessor :topic
1110

1211
def self.messages_from_chat(
@@ -113,12 +112,13 @@ def self.messages_from_post(post, style: nil, max_posts:, bot_usernames:, includ
113112
FROM upload_references ref
114113
WHERE ref.target_type = 'Post' AND ref.target_id = posts.id
115114
) as upload_ids",
115+
"posts.created_at",
116116
)
117117

118118
builder = new
119119
builder.topic = post.topic
120120

121-
context.reverse_each do |raw, username, custom_prompt, upload_ids|
121+
context.reverse_each do |raw, username, custom_prompt, upload_ids, created_at|
122122
custom_prompt_translation =
123123
Proc.new do |message|
124124
# We can't keep backwards-compatibility for stored functions.
@@ -134,6 +134,7 @@ def self.messages_from_post(post, style: nil, max_posts:, bot_usernames:, includ
134134

135135
thinking = message[4]
136136
custom_context[:thinking] = thinking if thinking
137+
custom_context[:created_at] = created_at
137138

138139
builder.push(**custom_context)
139140
end
@@ -149,6 +150,7 @@ def self.messages_from_post(post, style: nil, max_posts:, bot_usernames:, includ
149150
if upload_ids.present? && context[:type] == :user && include_uploads
150151
context[:upload_ids] = upload_ids.compact
151152
end
153+
context[:created_at] = created_at
152154

153155
builder.push(**context)
154156
end
@@ -159,6 +161,7 @@ def self.messages_from_post(post, style: nil, max_posts:, bot_usernames:, includ
159161

160162
def initialize
161163
@raw_messages = []
164+
@timestamps = {}
162165
end
163166

164167
def set_chat_context_posts(post_ids, guardian, include_uploads:)
@@ -171,35 +174,74 @@ def set_chat_context_posts(post_ids, guardian, include_uploads:)
171174
posts << post
172175
end
173176
if posts.present?
174-
posts_context =
175-
+"\nThis chat is in the context of the Discourse topic '#{posts[0].topic.title}':\n\n"
176-
posts_context = +"{{{\n"
177+
posts_context = []
178+
posts_context << "\nThis chat is in the context of the Discourse topic '#{posts[0].topic.title}':\n\n"
179+
posts_context << "{{{\n"
177180
posts.each do |post|
178181
posts_context << "url: #{post.url}\n"
179182
posts_context << "#{post.username}: #{post.raw}\n\n"
183+
if include_uploads
184+
post.uploads.each { |upload| posts_context << { upload_id: upload.id } }
185+
end
180186
end
181187
posts_context << "}}}"
182188
@chat_context_posts = posts_context
183-
if include_uploads
184-
uploads = []
185-
posts.each { |post| uploads.concat(post.uploads.pluck(:id)) }
186-
uploads.uniq!
187-
@chat_context_post_upload_ids = uploads.take(MAX_CHAT_UPLOADS)
188-
end
189189
end
190190
end
191191

192192
def to_a(limit: nil, style: nil)
193+
# topic and chat array are special, they are single messages that contain all history
193194
return chat_array(limit: limit) if style == :chat
194195
return topic_array if style == :topic
196+
197+
# the rest of the styles can include multiple messages
198+
result = valid_messages_array(@raw_messages)
199+
prepend_chat_post_context(result) if style == :chat_with_context
200+
201+
if limit
202+
result[0..limit]
203+
else
204+
result
205+
end
206+
end
207+
208+
def push(type:, content:, name: nil, upload_ids: nil, id: nil, thinking: nil, created_at: nil)
209+
if !%i[user model tool tool_call system].include?(type)
210+
raise ArgumentError, "type must be either :user, :model, :tool, :tool_call or :system"
211+
end
212+
raise ArgumentError, "upload_ids must be an array" if upload_ids && !upload_ids.is_a?(Array)
213+
214+
content = [content, *upload_ids.map { |upload_id| { upload_id: upload_id } }] if upload_ids
215+
message = { type: type, content: content }
216+
message[:name] = name.to_s if name
217+
message[:id] = id.to_s if id
218+
if thinking
219+
message[:thinking] = thinking["thinking"] if thinking["thinking"]
220+
message[:thinking_signature] = thinking["thinking_signature"] if thinking[
221+
"thinking_signature"
222+
]
223+
message[:redacted_thinking_signature] = thinking[
224+
"redacted_thinking_signature"
225+
] if thinking["redacted_thinking_signature"]
226+
end
227+
228+
@raw_messages << message
229+
@timestamps[message] = created_at if created_at
230+
231+
message
232+
end
233+
234+
private
235+
236+
def valid_messages_array(messages)
195237
result = []
196238

197239
# this will create a "valid" messages array
198240
# 1. ensures we always start with a user message
199241
# 2. ensures we always end with a user message
200242
# 3. ensures we always interleave user and model messages
201243
last_type = nil
202-
@raw_messages.each do |message|
244+
messages.each do |message|
203245
next if !last_type && message[:type] != :user
204246

205247
if last_type == :tool_call && message[:type] != :tool
@@ -239,51 +281,26 @@ def to_a(limit: nil, style: nil)
239281
last_type = message[:type]
240282
end
241283

242-
if style == :chat_with_context && @chat_context_posts
243-
buffer = +"You are replying inside a Discourse chat."
244-
buffer << "\n"
245-
buffer << @chat_context_posts
246-
buffer << "\n"
247-
buffer << "Your instructions are:\n"
248-
result[0][:content] = "#{buffer}#{result[0][:content]}"
249-
if @chat_context_post_upload_ids.present?
250-
result[0][:upload_ids] = (result[0][:upload_ids] || []).concat(
251-
@chat_context_post_upload_ids,
252-
)
253-
end
254-
end
255-
256-
if limit
257-
result[0..limit]
258-
else
259-
result
260-
end
284+
result
261285
end
262286

263-
def push(type:, content:, name: nil, upload_ids: nil, id: nil, thinking: nil)
264-
if !%i[user model tool tool_call system].include?(type)
265-
raise ArgumentError, "type must be either :user, :model, :tool, :tool_call or :system"
266-
end
267-
raise ArgumentError, "upload_ids must be an array" if upload_ids && !upload_ids.is_a?(Array)
287+
def prepend_chat_post_context(messages)
288+
return if @chat_context_posts.blank?
268289

269-
content = [content, *upload_ids.map { |upload_id| { upload_id: upload_id } }] if upload_ids
270-
message = { type: type, content: content }
271-
message[:name] = name.to_s if name
272-
message[:id] = id.to_s if id
273-
if thinking
274-
message[:thinking] = thinking["thinking"] if thinking["thinking"]
275-
message[:thinking_signature] = thinking["thinking_signature"] if thinking[
276-
"thinking_signature"
277-
]
278-
message[:redacted_thinking_signature] = thinking[
279-
"redacted_thinking_signature"
280-
] if thinking["redacted_thinking_signature"]
281-
end
290+
old_content = messages[0][:content]
291+
old_content = [old_content] if !old_content.is_a?(Array)
282292

283-
@raw_messages << message
284-
end
293+
new_content = []
294+
new_content << "You are replying inside a Discourse chat.\n"
295+
new_content.concat(@chat_context_posts)
296+
new_content << "\n"
297+
new_content << "Your instructions are:\n"
298+
new_content.concat(old_content)
285299

286-
private
300+
compressed = compress_messages_buffer(new_content.flatten, max_uploads: MAX_CHAT_UPLOADS)
301+
302+
messages[0][:content] = compressed
303+
end
287304

288305
def format_user_info(user)
289306
info = []
@@ -294,6 +311,34 @@ def format_user_info(user)
294311
"#{user.username} (#{user.name}): #{info.compact.join(", ")}"
295312
end
296313

314+
def format_timestamp(timestamp)
315+
return nil unless timestamp
316+
317+
time_diff = Time.now - timestamp
318+
319+
if time_diff < 1.minute
320+
"just now"
321+
elsif time_diff < 1.hour
322+
mins = (time_diff / 1.minute).round
323+
"#{mins} #{mins == 1 ? "minute" : "minutes"} ago"
324+
elsif time_diff < 1.day
325+
hours = (time_diff / 1.hour).round
326+
"#{hours} #{hours == 1 ? "hour" : "hours"} ago"
327+
elsif time_diff < 7.days
328+
days = (time_diff / 1.day).round
329+
"#{days} #{days == 1 ? "day" : "days"} ago"
330+
elsif time_diff < 30.days
331+
weeks = (time_diff / 7.days).round
332+
"#{weeks} #{weeks == 1 ? "week" : "weeks"} ago"
333+
elsif time_diff < 365.days
334+
months = (time_diff / 30.days).round
335+
"#{months} #{months == 1 ? "month" : "months"} ago"
336+
else
337+
years = (time_diff / 365.days).round
338+
"#{years} #{years == 1 ? "year" : "years"} ago"
339+
end
340+
end
341+
297342
def user_role(user)
298343
return "moderator" if user.moderator?
299344
return "admin" if user.admin?
@@ -323,45 +368,57 @@ def account_age(user)
323368
end
324369
end
325370

326-
def topic_array
327-
raw_messages = @raw_messages.dup
371+
def format_topic_info(topic)
328372
content_array = []
329-
content_array << "You are operating in a Discourse forum.\n\n"
330373

331-
if @topic
332-
if @topic.private_message?
333-
content_array << "Private message info.\n"
334-
else
335-
content_array << "Topic information:\n"
336-
end
374+
if topic.private_message?
375+
content_array << "Private message info.\n"
376+
else
377+
content_array << "Topic information:\n"
378+
end
337379

338-
content_array << "- URL: #{@topic.url}\n"
339-
content_array << "- Title: #{@topic.title}\n"
340-
if SiteSetting.tagging_enabled
341-
tags = @topic.tags.pluck(:name)
342-
tags -= DiscourseTagging.hidden_tag_names if tags.present?
343-
content_array << "- Tags: #{tags.join(", ")}\n" if tags.present?
344-
end
345-
if !@topic.private_message?
346-
content_array << "- Category: #{@topic.category.name}\n" if @topic.category
347-
end
348-
content_array << "- Number of replies: #{@topic.posts_count - 1}\n\n"
380+
content_array << "- URL: #{topic.url}\n"
381+
content_array << "- Title: #{topic.title}\n"
382+
if SiteSetting.tagging_enabled
383+
tags = topic.tags.pluck(:name)
384+
tags -= DiscourseTagging.hidden_tag_names if tags.present?
385+
content_array << "- Tags: #{tags.join(", ")}\n" if tags.present?
386+
end
387+
if !topic.private_message?
388+
content_array << "- Category: #{topic.category.name}\n" if topic.category
389+
end
390+
content_array << "- Number of replies: #{topic.posts_count - 1}\n\n"
391+
392+
content_array.join
393+
end
394+
395+
def format_user_infos(usernames)
396+
content_array = []
397+
398+
if usernames.present?
399+
users_details =
400+
User
401+
.where(username: usernames)
402+
.includes(:user_stat)
403+
.map { |user| format_user_info(user) }
404+
.compact
405+
content_array << "User information:\n"
406+
content_array << "- #{users_details.join("\n- ")}\n\n" if users_details.present?
349407
end
408+
content_array.join
409+
end
410+
411+
def topic_array
412+
raw_messages = @raw_messages.dup
413+
content_array = []
414+
content_array << "You are operating in a Discourse forum.\n\n"
415+
content_array << format_topic_info(@topic) if @topic
350416

351417
if raw_messages.present?
352418
usernames =
353419
raw_messages.filter { |message| message[:type] == :user }.map { |message| message[:id] }
354420

355-
if usernames.present?
356-
users_details =
357-
User
358-
.where(username: usernames)
359-
.includes(:user_stat)
360-
.map { |user| format_user_info(user) }
361-
.compact
362-
content_array << "User information:\n"
363-
content_array << "- #{users_details.join("\n- ")}\n\n" if users_details.present?
364-
end
421+
content_array << format_user_infos(usernames) if usernames.present?
365422
end
366423

367424
last_user_message = raw_messages.pop
@@ -370,6 +427,8 @@ def topic_array
370427
content_array << "Here is the conversation so far:\n"
371428
raw_messages.each do |message|
372429
content_array << "#{message[:id] || "User"}: "
430+
timestamp = @timestamps[message]
431+
content_array << "(#{format_timestamp(timestamp)}) " if timestamp
373432
content_array << message[:content]
374433
content_array << "\n\n"
375434
end

0 commit comments

Comments
 (0)