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

Commit 2c63ae3

Browse files
committed
image support is improved but not fully fixed yet
partially working in anthropic, still got quite a few dialects to go
1 parent 8ce3cf1 commit 2c63ae3

File tree

3 files changed

+111
-40
lines changed

3 files changed

+111
-40
lines changed

lib/completions/dialects/claude.rb

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -126,29 +126,38 @@ def system_msg(msg)
126126
def user_msg(msg)
127127
content = +""
128128
content << "#{msg[:id]}: " if msg[:id]
129-
content << msg[:content]
130-
content = inline_images(content, msg) if vision_support?
129+
message_content = msg[:content]
130+
message_content = [message_content] if !message_content.is_a?(Array)
131+
132+
content_array = []
133+
134+
message_content.each do |content_part|
135+
if content_part.is_a?(String)
136+
content << content_part
137+
elsif content_part.is_a?(Hash) && vision_support?
138+
content_array << { type: "text", text: content } if content.present?
139+
image = image_node(content_part[:upload_id])
140+
content_array << image if image
141+
content = +""
142+
end
143+
end
131144

132-
{ role: "user", content: content }
133-
end
145+
content_array << { type: "text", text: content } if content.present?
134146

135-
def inline_images(content, message)
136-
encoded_uploads = prompt.encoded_uploads(message)
137-
return content if encoded_uploads.blank?
138-
139-
content_w_imgs =
140-
encoded_uploads.reduce([]) do |memo, details|
141-
memo << {
142-
source: {
143-
type: "base64",
144-
data: details[:base64],
145-
media_type: details[:mime_type],
146-
},
147-
type: "image",
148-
}
149-
end
147+
{ role: "user", content: content_array }
148+
end
150149

151-
content_w_imgs << { type: "text", text: content }
150+
def image_node(upload_id)
151+
details = prompt.encode_upload(upload_id)
152+
return nil if details.blank?
153+
{
154+
source: {
155+
type: "base64",
156+
data: details[:base64],
157+
media_type: details[:mime_type],
158+
},
159+
type: "image",
160+
}
152161
end
153162
end
154163
end

lib/completions/prompt_messages_builder.rb

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -280,51 +280,49 @@ def push(type:, content:, name: nil, upload_ids: nil, id: nil, thinking: nil)
280280

281281
def topic_array
282282
raw_messages = @raw_messages.dup
283-
user_content = +"You are operating in a Discourse forum.\n\n"
283+
content_array = []
284+
content_array << "You are operating in a Discourse forum.\n\n"
284285

285286
if @topic
286287
if @topic.private_message?
287-
user_content << "Private message info.\n"
288+
content_array << "Private message info.\n"
288289
else
289-
user_content << "Topic information:\n"
290+
content_array << "Topic information:\n"
290291
end
291292

292-
user_content << "- URL: #{@topic.url}\n"
293-
user_content << "- Title: #{@topic.title}\n"
293+
content_array << "- URL: #{@topic.url}\n"
294+
content_array << "- Title: #{@topic.title}\n"
294295
if SiteSetting.tagging_enabled
295296
tags = @topic.tags.pluck(:name)
296297
tags -= DiscourseTagging.hidden_tag_names if tags.present?
297-
user_content << "- Tags: #{tags.join(", ")}\n" if tags.present?
298+
content_array << "- Tags: #{tags.join(", ")}\n" if tags.present?
298299
end
299300
if !@topic.private_message?
300-
user_content << "- Category: #{@topic.category.name}\n" if @topic.category
301+
content_array << "- Category: #{@topic.category.name}\n" if @topic.category
301302
end
302-
user_content << "- Number of replies: #{@topic.posts_count - 1}\n\n"
303+
content_array << "- Number of replies: #{@topic.posts_count - 1}\n\n"
303304
end
304305

305306
last_user_message = raw_messages.pop
306307

307-
upload_ids = []
308308
if raw_messages.present?
309-
user_content << "Here is the conversation so far:\n"
309+
content_array << "Here is the conversation so far:\n"
310310
raw_messages.each do |message|
311-
user_content << "#{message[:name] || "User"}: #{message[:content]}\n"
312-
upload_ids.concat(message[:upload_ids]) if message[:upload_ids].present?
311+
content_array << "#{message[:name] || "User"}: "
312+
content_array << message[:content]
313+
content_array << "\n\n"
313314
end
314315
end
315316

316317
if last_user_message
317-
user_content << "You are responding to #{last_user_message[:name] || "User"} who just said:\n #{last_user_message[:content]}"
318-
if last_user_message[:upload_ids].present?
319-
upload_ids.concat(last_user_message[:upload_ids])
320-
end
318+
content_array << "You are responding to #{last_user_message[:name] || "User"} who just said:\n"
319+
content_array << last_user_message[:content]
321320
end
322321

323-
user_message = { type: :user, content: user_content }
322+
content_array =
323+
compress_messages_buffer(content_array.flatten, max_uploads: MAX_TOPIC_UPLOADS)
324324

325-
if upload_ids.present?
326-
user_message[:upload_ids] = upload_ids[-MAX_TOPIC_UPLOADS..-1] || upload_ids
327-
end
325+
user_message = { type: :user, content: content_array }
328326

329327
[user_message]
330328
end
@@ -388,6 +386,8 @@ def compress_messages_buffer(buffer, max_uploads:)
388386
compressed.delete_if { |item| item.is_a?(Hash) && (counter += 1) > 0 }
389387
end
390388

389+
compressed = compressed[0] if compressed.length == 1 && compressed[0].is_a?(String)
390+
391391
compressed
392392
end
393393
end

spec/lib/completions/prompt_messages_builder_spec.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,17 @@
33
describe DiscourseAi::Completions::PromptMessagesBuilder do
44
let(:builder) { DiscourseAi::Completions::PromptMessagesBuilder.new }
55
fab!(:user)
6+
fab!(:admin)
67
fab!(:bot_user) { Fabricate(:user) }
78
fab!(:other_user) { Fabricate(:user) }
89

10+
fab!(:image_upload1) do
11+
Fabricate(:upload, user: user, original_filename: "image.png", extension: "png")
12+
end
13+
fab!(:image_upload2) do
14+
Fabricate(:upload, user: user, original_filename: "image.png", extension: "png")
15+
end
16+
917
it "should allow merging user messages" do
1018
builder.push(type: :user, content: "Hello", name: "Alice")
1119
builder.push(type: :user, content: "World", name: "Bob")
@@ -239,6 +247,60 @@
239247
)
240248
end
241249

250+
it "handles uploads correctly in topic style messages" do
251+
# Use Discourse's upload format in the post raw content
252+
upload_markdown = "![test|658x372](#{image_upload1.short_url})"
253+
254+
post_with_upload =
255+
Fabricate(
256+
:post,
257+
topic: pm,
258+
user: admin,
259+
raw: "This is the original #{upload_markdown} I just added",
260+
)
261+
262+
UploadReference.create!(target: post_with_upload, upload: image_upload1)
263+
264+
upload2_markdown = "![test|658x372](#{image_upload2.short_url})"
265+
266+
post2_with_upload =
267+
Fabricate(
268+
:post,
269+
topic: pm,
270+
user: admin,
271+
raw: "This post has a different image #{upload2_markdown} I just added",
272+
)
273+
274+
UploadReference.create!(target: post2_with_upload, upload: image_upload2)
275+
276+
messages =
277+
described_class.messages_from_post(
278+
post2_with_upload,
279+
style: :topic,
280+
max_posts: 3,
281+
bot_usernames: [bot_user.username],
282+
include_uploads: true,
283+
)
284+
285+
# this is not quite ideal yet, images are attached at the end of the post
286+
# long term we may want to extract them out using a regex and create N parts
287+
# so people can talk about multiple images in a single post
288+
# this is the initial ground work though
289+
290+
expect(messages.length).to eq(1)
291+
content = messages[0][:content]
292+
293+
# first part
294+
# first image
295+
# second part
296+
# second image
297+
expect(content.length).to eq(4)
298+
expect(content[0]).to include("This is the original")
299+
expect(content[1]).to eq({ upload_id: image_upload1.id })
300+
expect(content[2]).to include("different image")
301+
expect(content[3]).to eq({ upload_id: image_upload2.id })
302+
end
303+
242304
context "with limited context" do
243305
it "respects max_context_posts" do
244306
context =

0 commit comments

Comments
 (0)