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

Commit 8ce3cf1

Browse files
committed
chipping away at inline uploads
1 parent e49223f commit 8ce3cf1

File tree

3 files changed

+300
-88
lines changed

3 files changed

+300
-88
lines changed

lib/completions/prompt_messages_builder.rb

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,9 @@ def push(type:, content:, name: nil, upload_ids: nil, id: nil, thinking: nil)
259259
end
260260
raise ArgumentError, "upload_ids must be an array" if upload_ids && !upload_ids.is_a?(Array)
261261

262+
content = [content, *upload_ids.map { |upload_id| { upload_id: upload_id } }] if upload_ids
262263
message = { type: type, content: content }
263264
message[:name] = name.to_s if name
264-
message[:upload_ids] = upload_ids if upload_ids
265265
message[:id] = id.to_s if id
266266
if thinking
267267
message[:thinking] = thinking["thinking"] if thinking["thinking"]
@@ -331,16 +331,13 @@ def topic_array
331331

332332
def chat_array(limit:)
333333
if @raw_messages.length > 1
334-
buffer =
335-
+"You are replying inside a Discourse chat channel. Here is a summary of the conversation so far:\n{{{"
336-
337-
upload_ids = []
334+
buffer = [
335+
+"You are replying inside a Discourse chat channel. Here is a summary of the conversation so far:\n{{{",
336+
]
338337

339338
@raw_messages[0..-2].each do |message|
340339
buffer << "\n"
341340

342-
upload_ids.concat(message[:upload_ids]) if message[:upload_ids].present?
343-
344341
if message[:type] == :user
345342
buffer << "#{message[:name] || "User"}: "
346343
else
@@ -357,15 +354,41 @@ def chat_array(limit:)
357354
end
358355

359356
last_message = @raw_messages[-1]
360-
buffer << "#{last_message[:name] || "User"}: #{last_message[:content]} "
357+
buffer << "#{last_message[:name] || "User"}: "
358+
buffer << last_message[:content]
359+
360+
buffer = compress_messages_buffer(buffer.flatten, max_uploads: MAX_CHAT_UPLOADS)
361361

362362
message = { type: :user, content: buffer }
363-
upload_ids.concat(last_message[:upload_ids]) if last_message[:upload_ids].present?
363+
[message]
364+
end
364365

365-
message[:upload_ids] = upload_ids[-MAX_CHAT_UPLOADS..-1] ||
366-
upload_ids if upload_ids.present?
366+
# caps uploads to maximum uploads allowed in message stream
367+
# and concats string elements
368+
def compress_messages_buffer(buffer, max_uploads:)
369+
compressed = []
370+
current_text = +""
371+
upload_count = 0
372+
373+
buffer.each do |item|
374+
if item.is_a?(String)
375+
current_text << item
376+
elsif item.is_a?(Hash)
377+
compressed << current_text if current_text.present?
378+
compressed << item
379+
current_text = +""
380+
upload_count += 1
381+
end
382+
end
367383

368-
[message]
384+
compressed << current_text if current_text.present?
385+
386+
if upload_count > max_uploads
387+
counter = max_uploads - upload_count
388+
compressed.delete_if { |item| item.is_a?(Hash) && (counter += 1) > 0 }
389+
end
390+
391+
compressed
369392
end
370393
end
371394
end

spec/lib/completions/prompt_messages_builder_spec.rb

Lines changed: 265 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
describe DiscourseAi::Completions::PromptMessagesBuilder do
44
let(:builder) { DiscourseAi::Completions::PromptMessagesBuilder.new }
5+
fab!(:user)
6+
fab!(:bot_user) { Fabricate(:user) }
7+
fab!(:other_user) { Fabricate(:user) }
58

69
it "should allow merging user messages" do
710
builder.push(type: :user, content: "Hello", name: "Alice")
@@ -14,7 +17,7 @@
1417
builder.push(type: :user, content: "Hello", name: "Alice", upload_ids: [1, 2])
1518

1619
expect(builder.to_a).to eq(
17-
[{ type: :user, name: "Alice", content: "Hello", upload_ids: [1, 2] }],
20+
[{ type: :user, content: ["Hello", { upload_id: 1 }, { upload_id: 2 }], name: "Alice" }],
1821
)
1922
end
2023

@@ -64,4 +67,265 @@
6467
expect(content).to include("Alice")
6568
expect(content).to include("How do I solve this")
6669
end
70+
71+
describe ".messages_from_chat" do
72+
fab!(:dm_channel) { Fabricate(:direct_message_channel, users: [user, bot_user]) }
73+
fab!(:dm_message1) do
74+
Fabricate(:chat_message, chat_channel: dm_channel, user: user, message: "Hello bot")
75+
end
76+
fab!(:dm_message2) do
77+
Fabricate(:chat_message, chat_channel: dm_channel, user: bot_user, message: "Hello human")
78+
end
79+
fab!(:dm_message3) do
80+
Fabricate(:chat_message, chat_channel: dm_channel, user: user, message: "How are you?")
81+
end
82+
83+
fab!(:public_channel) { Fabricate(:category_channel) }
84+
fab!(:public_message1) do
85+
Fabricate(:chat_message, chat_channel: public_channel, user: user, message: "Hello everyone")
86+
end
87+
fab!(:public_message2) do
88+
Fabricate(:chat_message, chat_channel: public_channel, user: bot_user, message: "Hi there")
89+
end
90+
91+
fab!(:thread_original) do
92+
Fabricate(:chat_message, chat_channel: public_channel, user: user, message: "Thread starter")
93+
end
94+
fab!(:thread) do
95+
Fabricate(:chat_thread, channel: public_channel, original_message: thread_original)
96+
end
97+
fab!(:thread_reply1) do
98+
Fabricate(
99+
:chat_message,
100+
chat_channel: public_channel,
101+
user: other_user,
102+
message: "Thread reply",
103+
thread: thread,
104+
)
105+
end
106+
107+
fab!(:upload) { Fabricate(:upload, user: user) }
108+
fab!(:message_with_upload) do
109+
Fabricate(
110+
:chat_message,
111+
chat_channel: dm_channel,
112+
user: user,
113+
message: "Check this image",
114+
upload_ids: [upload.id],
115+
)
116+
end
117+
118+
it "processes messages from direct message channels" do
119+
context =
120+
described_class.messages_from_chat(
121+
dm_message3,
122+
channel: dm_channel,
123+
context_post_ids: nil,
124+
max_messages: 10,
125+
include_uploads: false,
126+
bot_user_ids: [bot_user.id],
127+
instruction_message: nil,
128+
)
129+
130+
# this is all we got cause it is assuming threading
131+
expect(context).to eq([{ type: :user, content: "How are you?", name: user.username }])
132+
end
133+
134+
it "includes uploads when include_uploads is true" do
135+
message_with_upload.reload
136+
expect(message_with_upload.uploads).to include(upload)
137+
138+
context =
139+
described_class.messages_from_chat(
140+
message_with_upload,
141+
channel: dm_channel,
142+
context_post_ids: nil,
143+
max_messages: 10,
144+
include_uploads: true,
145+
bot_user_ids: [bot_user.id],
146+
instruction_message: nil,
147+
)
148+
149+
# Find the message with upload
150+
message = context.find { |m| m[:content] == ["Check this image", { upload_id: upload.id }] }
151+
expect(message).to be_present
152+
end
153+
154+
it "doesn't include uploads when include_uploads is false" do
155+
# Make sure the upload is associated with the message
156+
message_with_upload.reload
157+
expect(message_with_upload.uploads).to include(upload)
158+
159+
context =
160+
described_class.messages_from_chat(
161+
message_with_upload,
162+
channel: dm_channel,
163+
context_post_ids: nil,
164+
max_messages: 10,
165+
include_uploads: false,
166+
bot_user_ids: [bot_user.id],
167+
instruction_message: nil,
168+
)
169+
170+
# Find the message with upload
171+
message = context.find { |m| m[:content] == "Check this image" }
172+
expect(message).to be_present
173+
expect(message[:upload_ids]).to be_nil
174+
end
175+
176+
it "properly handles uploads in public channels with multiple users" do
177+
_first_message =
178+
Fabricate(:chat_message, chat_channel: public_channel, user: user, message: "First message")
179+
180+
_message_with_upload =
181+
Fabricate(
182+
:chat_message,
183+
chat_channel: public_channel,
184+
user: other_user,
185+
message: "Message with image",
186+
upload_ids: [upload.id],
187+
)
188+
189+
last_message =
190+
Fabricate(:chat_message, chat_channel: public_channel, user: user, message: "Final message")
191+
192+
context =
193+
described_class.messages_from_chat(
194+
last_message,
195+
channel: public_channel,
196+
context_post_ids: nil,
197+
max_messages: 3,
198+
include_uploads: true,
199+
bot_user_ids: [bot_user.id],
200+
instruction_message: nil,
201+
)
202+
203+
expect(context.length).to eq(1)
204+
content = context.first[:content]
205+
206+
expect(content.length).to eq(3)
207+
expect(content[0]).to include("First message")
208+
expect(content[0]).to include("Message with image")
209+
expect(content[1]).to include({ upload_id: upload.id })
210+
expect(content[2]).to include("Final message")
211+
end
212+
end
213+
214+
describe ".messages_from_post" do
215+
fab!(:pm) do
216+
Fabricate(
217+
:private_message_topic,
218+
title: "This is my special PM",
219+
user: user,
220+
topic_allowed_users: [
221+
Fabricate.build(:topic_allowed_user, user: user),
222+
Fabricate.build(:topic_allowed_user, user: bot_user),
223+
],
224+
)
225+
end
226+
fab!(:first_post) do
227+
Fabricate(:post, topic: pm, user: user, post_number: 1, raw: "This is a reply by the user")
228+
end
229+
fab!(:second_post) do
230+
Fabricate(:post, topic: pm, user: bot_user, post_number: 2, raw: "This is a bot reply")
231+
end
232+
fab!(:third_post) do
233+
Fabricate(
234+
:post,
235+
topic: pm,
236+
user: user,
237+
post_number: 3,
238+
raw: "This is a second reply by the user",
239+
)
240+
end
241+
242+
context "with limited context" do
243+
it "respects max_context_posts" do
244+
context =
245+
described_class.messages_from_post(
246+
third_post,
247+
max_posts: 1,
248+
bot_usernames: [bot_user.username],
249+
include_uploads: false,
250+
)
251+
252+
expect(context).to contain_exactly(
253+
*[{ type: :user, id: user.username, content: third_post.raw }],
254+
)
255+
end
256+
end
257+
258+
it "includes previous posts ordered by post_number" do
259+
context =
260+
described_class.messages_from_post(
261+
third_post,
262+
max_posts: 10,
263+
bot_usernames: [bot_user.username],
264+
include_uploads: false,
265+
)
266+
267+
expect(context).to eq(
268+
[
269+
{ type: :user, content: "This is a reply by the user", id: user.username },
270+
{ type: :model, content: "This is a bot reply" },
271+
{ type: :user, content: "This is a second reply by the user", id: user.username },
272+
],
273+
)
274+
end
275+
276+
it "only include regular posts" do
277+
first_post.update!(post_type: Post.types[:whisper])
278+
279+
context =
280+
described_class.messages_from_post(
281+
third_post,
282+
max_posts: 10,
283+
bot_usernames: [bot_user.username],
284+
include_uploads: false,
285+
)
286+
287+
# skips leading model reply which makes no sense cause first post was whisper
288+
expect(context).to eq(
289+
[{ type: :user, content: "This is a second reply by the user", id: user.username }],
290+
)
291+
end
292+
293+
context "with custom prompts" do
294+
it "When post custom prompt is present, we use that instead of the post content" do
295+
custom_prompt = [
296+
[
297+
{ name: "time", arguments: { name: "time", timezone: "Buenos Aires" } }.to_json,
298+
"time",
299+
"tool_call",
300+
],
301+
[
302+
{ args: { timezone: "Buenos Aires" }, time: "2023-12-14 17:24:00 -0300" }.to_json,
303+
"time",
304+
"tool",
305+
],
306+
["I replied to the time command", bot_user.username],
307+
]
308+
309+
PostCustomPrompt.create!(post: second_post, custom_prompt: custom_prompt)
310+
311+
context =
312+
described_class.messages_from_post(
313+
third_post,
314+
max_posts: 10,
315+
bot_usernames: [bot_user.username],
316+
include_uploads: false,
317+
)
318+
319+
expect(context).to eq(
320+
[
321+
{ type: :user, content: "This is a reply by the user", id: user.username },
322+
{ type: :tool_call, content: custom_prompt.first.first, id: "time" },
323+
{ type: :tool, id: "time", content: custom_prompt.second.first },
324+
{ type: :model, content: custom_prompt.third.first },
325+
{ type: :user, content: "This is a second reply by the user", id: user.username },
326+
],
327+
)
328+
end
329+
end
330+
end
67331
end

0 commit comments

Comments
 (0)