discourse/plugins/discourse-ai/spec/lib/completions/prompt_messages_builder_spec.rb
Rafael dos Santos Silva bc39aacc3d
FEATURE: Provider-native built-in tools for agents (web search) (#40809)
Adds a fourth kind of agent tool: provider-native built-in tools that
the
LLM provider executes server-side, rather than tools Discourse runs and
feeds back. The first one is web search, supported on Gemini (Google
Search
grounding), OpenAI (web search via the Responses API) and Anthropic
(Claude
web search).

Native tools are stored on the agent's `tools` column with a `native-`
prefix, flow to the prompt as a separate `native_tools` list (never as
runnable Tool classes), and each provider dialect renders them into its
own
request payload. Response processors already ignore the server-side
tool/grounding blocks, so the bot loop never tries to execute them.

They are only selectable when the agent forces a default LLM whose
provider
supports the tool; this is enforced both in the editor UI (filtered by
the
selected LLM's `supported_native_tools`) and by server-side validation.

Also fixes the Gemini endpoint sending `function_calling_config` without
any
`function_declarations`, which the API rejects when only native tools
are
present.

---------

Co-authored-by: Sam Saffron <sam.saffron@gmail.com>
2026-06-16 14:37:51 -03:00

964 lines
30 KiB
Ruby
Vendored

# frozen_string_literal: true
describe DiscourseAi::Completions::PromptMessagesBuilder do
let(:builder) { DiscourseAi::Completions::PromptMessagesBuilder.new }
fab!(:user)
fab!(:admin)
fab!(:bot_user, :user)
fab!(:other_user, :user)
fab!(:image_upload1) do
Fabricate(:upload, user: user, original_filename: "image.png", extension: "png")
end
fab!(:image_upload2) do
Fabricate(:upload, user: user, original_filename: "image.png", extension: "png")
end
let(:document_upload) do
Fabricate(:upload, user: user, original_filename: "notes.txt", extension: "txt")
end
before do
enable_current_plugin
SiteSetting.authorized_extensions = "*"
end
it "correctly merges user messages with uploads" do
builder.push(type: :user, content: "Hello", id: "Alice", upload_ids: [1])
builder.push(type: :user, content: "World", id: "Bob", upload_ids: [2])
messages = builder.to_a
# Check the structure of the merged message
expect(messages.length).to eq(1)
expect(messages[0][:type]).to eq(:user)
# The content should contain the text and both uploads
content = messages[0][:content]
expect(content).to be_an(Array)
expect(content[0]).to eq("Alice: Hello")
expect(content[1]).to eq({ upload_id: 1 })
expect(content[2]).to eq("\nBob: World")
expect(content[3]).to eq({ upload_id: 2 })
end
it "should allow merging user messages" do
builder.push(type: :user, content: "Hello", id: "Alice")
builder.push(type: :user, content: "World", id: "Bob")
expect(builder.to_a).to eq([{ type: :user, content: "Alice: Hello\nBob: World" }])
end
it "should allow adding uploads" do
builder.push(type: :user, content: "Hello", name: "Alice", upload_ids: [1, 2])
expect(builder.to_a).to eq(
[{ type: :user, content: ["Hello", { upload_id: 1 }, { upload_id: 2 }], name: "Alice" }],
)
end
it "should support function calls" do
builder.push(type: :user, content: "Echo 123 please", name: "Alice")
builder.push(type: :tool_call, content: "echo(123)", name: "echo", id: 1)
builder.push(type: :tool, content: "123", name: "echo", id: 1)
builder.push(type: :user, content: "Hello", name: "Alice")
expected = [
{ type: :user, content: "Echo 123 please", name: "Alice" },
{ type: :tool_call, content: "echo(123)", name: "echo", id: "1" },
{ type: :tool, content: "123", name: "echo", id: "1" },
{ type: :user, content: "Hello", name: "Alice" },
]
expect(builder.to_a).to eq(expected)
end
it "should drop a tool call if it is not followed by tool" do
builder.push(type: :user, content: "Echo 123 please", id: "Alice")
builder.push(type: :tool_call, content: "echo(123)", name: "echo", id: 1)
builder.push(type: :user, content: "OK", id: "James")
expected = [{ type: :user, content: "Alice: Echo 123 please\nJames: OK" }]
expect(builder.to_a).to eq(expected)
end
it "should format messages for topic style" do
# Create a topic with tags
topic = Fabricate(:topic, title: "This is an Example Topic")
# Add tags to the topic
topic.tags = [Fabricate(:tag, name: "tag1"), Fabricate(:tag, name: "tag2")]
topic.save!
builder.topic = topic
builder.push(type: :user, content: "I like frogs", id: "Bob")
builder.push(type: :user, content: "How do I solve this?", id: "Alice")
result = builder.to_a(style: :topic)
content = result[0][:content]
expect(content).to include("This is an Example Topic")
expect(content).to include("tag1")
expect(content).to include("tag2")
expect(content).to include("Bob: I like frogs")
expect(content).to include("Alice")
expect(content).to include("How do I solve this")
end
describe "chat context posts in direct messages" do
fab!(:dm_channel) { Fabricate(:direct_message_channel, users: [user, bot_user]) }
fab!(:dm_message) do
Fabricate(
:chat_message,
chat_channel: dm_channel,
user: user,
message: "I have a question about the topic",
)
end
fab!(:topic) { Fabricate(:topic, title: "Important topic for context") }
fab!(:post1) { Fabricate(:post, topic: topic, user: other_user, raw: "This is the first post") }
fab!(:post2) { Fabricate(:post, topic: topic, user: user, raw: "And here's a follow-up") }
it "correctly includes topic posts as context in direct message channels" do
context =
described_class.messages_from_chat(
dm_message,
channel: dm_channel,
context_post_ids: [post1.id, post2.id],
max_messages: 10,
include_uploads: false,
bot_user_ids: [bot_user.id],
instruction_message: nil,
)
expect(context.length).to eq(1)
content = context.first[:content]
# First part should contain the context intro
expect(content).to include("You are replying inside a Discourse chat")
expect(content).to include(
"This chat is in the context of the Discourse topic 'Important topic for context'",
)
expect(content).to include(post1.username)
expect(content).to include("This is the first post")
expect(content).to include(post2.username)
expect(content).to include("And here's a follow-up")
# Last part should have the user's message
expect(content).to include("I have a question about the topic")
end
it "includes uploads from context posts when include_uploads is true" do
upload = Fabricate(:upload, user: user, original_filename: "image.png", extension: "png")
UploadReference.create!(target: post1, upload: upload)
context =
described_class.messages_from_chat(
dm_message,
channel: dm_channel,
context_post_ids: [post1.id],
max_messages: 10,
include_uploads: true,
bot_user_ids: [bot_user.id],
instruction_message: nil,
)
# Verify the upload reference is included
upload_hashes = context.first[:content].select { |item| item.is_a?(Hash) && item[:upload_id] }
expect(upload_hashes).to be_present
expect(upload_hashes.first[:upload_id]).to eq(upload.id)
end
context "with a secure upload the chat guardian cannot see" do
fab!(:secure_upload) do
private_category = Fabricate(:private_category, group: Fabricate(:group))
private_topic = Fabricate(:topic, category: private_category)
private_post = Fabricate(:post, topic: private_topic)
Fabricate(
:secure_upload,
original_filename: "secure.png",
extension: "png",
access_control_post: private_post,
)
end
it "drops the upload from the chat context" do
SiteSetting.embedded_media_post_allowed_groups = Group::AUTO_GROUPS[:everyone]
post_with_secure_upload =
create_post(
topic_id: topic.id,
user: user,
raw: "Look at this ![image](#{secure_upload.short_url})",
)
builder = described_class.new
builder.set_chat_context_posts(
[post_with_secure_upload.id],
Guardian.new(user),
include_image_uploads: true,
include_document_uploads: false,
)
upload_hashes =
builder.chat_context_posts.select { |item| item.is_a?(Hash) && item[:upload_id] }
expect(upload_hashes).to be_empty
end
end
end
describe ".messages_from_chat" do
fab!(:dm_channel) { Fabricate(:direct_message_channel, users: [user, bot_user]) }
fab!(:dm_message1) do
Fabricate(:chat_message, chat_channel: dm_channel, user: user, message: "Hello bot")
end
fab!(:dm_message2) do
Fabricate(:chat_message, chat_channel: dm_channel, user: bot_user, message: "Hello human")
end
fab!(:dm_message3) do
Fabricate(:chat_message, chat_channel: dm_channel, user: user, message: "How are you?")
end
fab!(:public_channel, :category_channel)
fab!(:public_message1) do
Fabricate(:chat_message, chat_channel: public_channel, user: user, message: "Hello everyone")
end
fab!(:public_message2) do
Fabricate(:chat_message, chat_channel: public_channel, user: bot_user, message: "Hi there")
end
fab!(:thread_original) do
Fabricate(:chat_message, chat_channel: public_channel, user: user, message: "Thread starter")
end
fab!(:thread) do
Fabricate(:chat_thread, channel: public_channel, original_message: thread_original)
end
fab!(:thread_reply1) do
Fabricate(
:chat_message,
chat_channel: public_channel,
user: other_user,
message: "Thread reply",
thread: thread,
)
end
fab!(:upload) { Fabricate(:upload, user: user) }
fab!(:message_with_upload) do
Fabricate(
:chat_message,
chat_channel: dm_channel,
user: user,
message: "Check this image",
upload_ids: [upload.id],
)
end
it "processes messages from direct message channels" do
context =
described_class.messages_from_chat(
dm_message3,
channel: dm_channel,
context_post_ids: nil,
max_messages: 10,
include_uploads: false,
bot_user_ids: [bot_user.id],
instruction_message: nil,
)
# this is all we got cause it is assuming threading
expect(context).to eq([{ type: :user, content: "How are you?", id: user.username }])
end
it "includes uploads when include_uploads is true" do
message_with_upload.reload
expect(message_with_upload.uploads).to include(upload)
context =
described_class.messages_from_chat(
message_with_upload,
channel: dm_channel,
context_post_ids: nil,
max_messages: 10,
include_uploads: true,
bot_user_ids: [bot_user.id],
instruction_message: nil,
)
# Find the message with upload
message =
context.find do |m|
m[:content] ==
["Check this image -- uploaded(#{upload.short_url})", { upload_id: upload.id }]
end
expect(message).to be_present
end
it "doesn't include uploads when include_uploads is false" do
# Make sure the upload is associated with the message
message_with_upload.reload
expect(message_with_upload.uploads).to include(upload)
context =
described_class.messages_from_chat(
message_with_upload,
channel: dm_channel,
context_post_ids: nil,
max_messages: 10,
include_uploads: false,
bot_user_ids: [bot_user.id],
instruction_message: nil,
)
# Find the message with upload
message =
context.find { |m| m[:content] == "Check this image -- uploaded(#{upload.short_url})" }
expect(message).to be_present
expect(message[:upload_ids]).to be_nil
end
it "can include document uploads while excluding image uploads" do
message_with_mixed_uploads =
Fabricate(
:chat_message,
chat_channel: dm_channel,
user: user,
message: "Check these files",
upload_ids: [image_upload1.id, document_upload.id],
)
context =
described_class.messages_from_chat(
message_with_mixed_uploads,
channel: dm_channel,
context_post_ids: nil,
max_messages: 10,
include_image_uploads: false,
include_document_uploads: true,
allowed_attachment_types: ["txt"],
bot_user_ids: [bot_user.id],
instruction_message: nil,
)
expect(context.first[:content]).to eq(
[
"Check these files -- uploaded(#{image_upload1.short_url}, #{document_upload.short_url})",
{ upload_id: document_upload.id },
],
)
end
it "can include image uploads while excluding document uploads" do
message_with_mixed_uploads =
Fabricate(
:chat_message,
chat_channel: dm_channel,
user: user,
message: "Check these files",
upload_ids: [image_upload1.id, document_upload.id],
)
context =
described_class.messages_from_chat(
message_with_mixed_uploads,
channel: dm_channel,
context_post_ids: nil,
max_messages: 10,
include_image_uploads: true,
include_document_uploads: false,
bot_user_ids: [bot_user.id],
instruction_message: nil,
)
expect(context.first[:content]).to eq(
[
"Check these files -- uploaded(#{image_upload1.short_url}, #{document_upload.short_url})",
{ upload_id: image_upload1.id },
],
)
end
context "with a secure upload attached to a message the chat guardian cannot see" do
fab!(:secure_upload) do
private_category = Fabricate(:private_category, group: Fabricate(:group))
private_topic = Fabricate(:topic, category: private_category)
private_post = Fabricate(:post, topic: private_topic)
Fabricate(
:secure_upload,
original_filename: "secure.png",
extension: "png",
access_control_post: private_post,
)
end
fab!(:message_with_secure_upload) do
Fabricate(
:chat_message,
chat_channel: dm_channel,
user: user,
message: "Look at this",
upload_ids: [secure_upload.id],
)
end
it "drops the upload from the prompt" do
context =
described_class.messages_from_chat(
message_with_secure_upload,
channel: dm_channel,
context_post_ids: nil,
max_messages: 10,
include_uploads: true,
bot_user_ids: [bot_user.id],
instruction_message: nil,
)
content = Array(context.first[:content])
upload_hashes = content.select { |item| item.is_a?(Hash) && item[:upload_id] }
expect(upload_hashes).to be_empty
end
end
it "properly handles uploads in public channels with multiple users" do
_first_message =
Fabricate(:chat_message, chat_channel: public_channel, user: user, message: "First message")
_message_with_upload =
Fabricate(
:chat_message,
chat_channel: public_channel,
user: other_user,
message: "Message with image",
upload_ids: [upload.id],
)
last_message =
Fabricate(:chat_message, chat_channel: public_channel, user: user, message: "Final message")
context =
described_class.messages_from_chat(
last_message,
channel: public_channel,
context_post_ids: nil,
max_messages: 3,
include_uploads: true,
bot_user_ids: [bot_user.id],
instruction_message: nil,
)
expect(context.length).to eq(1)
content = context.first[:content]
expect(content.length).to eq(3)
expect(content[0]).to include("First message")
expect(content[0]).to include("Message with image")
expect(content[1]).to include({ upload_id: upload.id })
expect(content[2]).to include("Final message")
end
end
describe "upload limits in messages_from_chat" do
fab!(:test_channel, :category_channel)
fab!(:test_user, :user)
# Create MAX_CHAT_UPLOADS + 1 uploads
fab!(:uploads) do
(described_class::MAX_CHAT_UPLOADS + 1).times.map do |i|
Fabricate(:upload, user: test_user, original_filename: "image#{i}.png", extension: "png")
end
end
# Create MAX_CHAT_UPLOADS + 1 messages with those uploads
fab!(:messages_with_uploads) do
uploads.map do |upload|
Fabricate(
:chat_message,
chat_channel: test_channel,
user: test_user,
message: "Message with upload #{upload.id}",
).tap do |msg|
UploadReference.create!(target: msg, upload: upload)
msg.update!(upload_ids: [upload.id])
end
end
end
let(:max_uploads) { described_class::MAX_CHAT_UPLOADS }
it "limits uploads to MAX_CHAT_UPLOADS in the final result" do
last_message = messages_with_uploads.last
# Make sure uploads are properly associated
messages_with_uploads.each_with_index do |msg, i|
expect(msg.uploads.first.id).to eq(uploads[i].id)
end
context =
described_class.messages_from_chat(
last_message,
channel: test_channel,
context_post_ids: nil,
max_messages: messages_with_uploads.size,
include_uploads: true,
bot_user_ids: [],
instruction_message: nil,
)
# We should have one message containing all message content
expect(context.length).to eq(1)
content = context.first[:content]
# Count the upload hashes in the content
upload_hashes = content.select { |item| item.is_a?(Hash) && item[:upload_id] }
# Should have exactly MAX_CHAT_UPLOADS upload references
expect(upload_hashes.size).to eq(max_uploads)
# The most recent uploads should be preserved (not the oldest)
expected_upload_ids = uploads.last(max_uploads).map(&:id)
actual_upload_ids = upload_hashes.map { |h| h[:upload_id] }
expect(actual_upload_ids).to match_array(expected_upload_ids)
end
it "filters disallowed documents before applying upload limits" do
allowed_upload =
Fabricate(:upload, user: test_user, original_filename: "notes.txt", extension: "txt")
disallowed_uploads =
described_class::MAX_CHAT_UPLOADS.times.map do |i|
Fabricate(
:upload,
user: test_user,
original_filename: "archive#{i}.zip",
extension: "zip",
)
end
mixed_uploads = [allowed_upload, *disallowed_uploads]
messages =
mixed_uploads.map do |upload|
Fabricate(
:chat_message,
chat_channel: test_channel,
user: test_user,
message: "Message with upload #{upload.id}",
).tap do |msg|
UploadReference.create!(target: msg, upload: upload)
msg.update!(upload_ids: [upload.id])
end
end
context =
described_class.messages_from_chat(
messages.last,
channel: test_channel,
context_post_ids: nil,
max_messages: messages.size,
include_image_uploads: false,
include_document_uploads: true,
allowed_attachment_types: ["txt"],
bot_user_ids: [],
instruction_message: nil,
)
upload_hashes = context.first[:content].select { |item| item.is_a?(Hash) && item[:upload_id] }
expect(upload_hashes).to eq([{ upload_id: allowed_upload.id }])
end
end
describe ".messages_from_post" do
fab!(:pm) do
Fabricate(
:private_message_topic,
title: "This is my special PM",
user: user,
topic_allowed_users: [
Fabricate.build(:topic_allowed_user, user: user),
Fabricate.build(:topic_allowed_user, user: bot_user),
],
)
end
fab!(:first_post) do
Fabricate(:post, topic: pm, user: user, post_number: 1, raw: "This is a reply by the user")
end
fab!(:second_post) do
Fabricate(:post, topic: pm, user: bot_user, post_number: 2, raw: "This is a bot reply")
end
fab!(:third_post) do
Fabricate(
:post,
topic: pm,
user: user,
post_number: 3,
raw: "This is a second reply by the user",
)
end
it "provides rich context for for style topic messages" do
freeze_time
user.update!(trust_level: 2, created_at: 1.year.ago)
admin.update!(trust_level: 4, created_at: 1.month.ago)
user.user_stat.update!(post_count: 10, days_visited: 50)
reply_to_first_post =
Fabricate(
:post,
topic: pm,
user: admin,
reply_to_post_number: first_post.post_number,
raw: "This is a reply to the first post",
)
context =
described_class.messages_from_post(
reply_to_first_post,
style: :topic,
max_posts: 10,
bot_usernames: [bot_user.username],
include_uploads: false,
)
expect(context.length).to eq(1)
content = context[0][:content]
expect(content).to include(user.name)
expect(content).to include("Trust level 2")
expect(content).to include("account age: 1 year")
# I am mixed on asserting everything cause the test
# will be brittle, but open to changing this
end
it "includes document post uploads independently from image uploads" do
UploadReference.create!(target: third_post, upload: image_upload1)
UploadReference.create!(target: third_post, upload: document_upload)
context =
described_class.messages_from_post(
third_post,
max_posts: 1,
bot_usernames: [bot_user.username],
include_image_uploads: false,
include_document_uploads: true,
allowed_attachment_types: ["txt"],
)
expect(context).to contain_exactly(
{
type: :user,
id: user.username,
content: [third_post.raw, { upload_id: document_upload.id }],
},
)
end
it "includes image post uploads independently from document uploads" do
UploadReference.create!(target: third_post, upload: image_upload1)
UploadReference.create!(target: third_post, upload: document_upload)
context =
described_class.messages_from_post(
third_post,
max_posts: 1,
bot_usernames: [bot_user.username],
include_image_uploads: true,
include_document_uploads: false,
)
expect(context).to contain_exactly(
{
type: :user,
id: user.username,
content: [third_post.raw, { upload_id: image_upload1.id }],
},
)
end
it "handles uploads correctly in topic style messages (and times)" do
freeze_time 32.days.ago
# Use Discourse's upload format in the post raw content
upload_markdown = "![test|658x372](#{image_upload1.short_url})"
post_with_upload =
Fabricate(
:post,
topic: pm,
user: admin,
raw: "This is the original #{upload_markdown} I just added",
)
UploadReference.create!(target: post_with_upload, upload: image_upload1)
upload2_markdown = "![test|658x372](#{image_upload2.short_url})"
freeze_time 32.days.from_now
post2_with_upload =
Fabricate(
:post,
topic: pm,
user: admin,
raw: "This post has a different image #{upload2_markdown} I just added",
)
UploadReference.create!(target: post2_with_upload, upload: image_upload2)
messages =
described_class.messages_from_post(
post2_with_upload,
style: :topic,
max_posts: 3,
bot_usernames: [bot_user.username],
include_uploads: true,
)
# this is not quite ideal yet, images are attached at the end of the post
# long term we may want to extract them out using a regex and create N parts
# so people can talk about multiple images in a single post
# this is the initial ground work though
expect(messages.length).to eq(1)
content = messages[0][:content]
# first part
# first image
# second part
# second image
expect(content.length).to eq(4)
expect(content[0]).to include("This is the original")
expect(content[0]).to include("(1 month ago)")
expect(content[1]).to eq({ upload_id: image_upload1.id })
expect(content[2]).to include("different image")
expect(content[3]).to eq({ upload_id: image_upload2.id })
end
context "with limited context" do
it "respects max_context_posts" do
context =
described_class.messages_from_post(
third_post,
max_posts: 1,
bot_usernames: [bot_user.username],
include_uploads: false,
)
expect(context).to contain_exactly(
*[{ type: :user, id: user.username, content: third_post.raw }],
)
end
end
it "includes previous posts ordered by post_number" do
context =
described_class.messages_from_post(
third_post,
max_posts: 10,
bot_usernames: [bot_user.username],
include_uploads: false,
)
expect(context).to eq(
[
{ type: :user, content: "This is a reply by the user", id: user.username },
{ type: :model, content: "This is a bot reply" },
{ type: :user, content: "This is a second reply by the user", id: user.username },
],
)
end
it "handles uploads correctly in topic style messages (and times)" do
freeze_time 32.days.ago
# Use Discourse's upload format in the post raw content
upload_markdown = "![test1|658x372](#{image_upload1.short_url})"
post1 =
Fabricate(
:post,
topic: pm,
user: admin,
raw: "This is the original #{upload_markdown} I just added",
)
UploadReference.create!(target: post1, upload: image_upload1)
long_title = "A" * 40
upload2_markdown = "![#{long_title}|658x372](#{image_upload2.short_url})"
freeze_time 32.days.from_now
post2_with_upload =
Fabricate(
:post,
topic: pm,
user: admin,
raw: "This post has a different image #{upload2_markdown} I just added",
)
UploadReference.create!(target: post2_with_upload, upload: image_upload2)
messages =
described_class.messages_from_post(
post2_with_upload,
style: :topic,
max_posts: 3,
bot_usernames: [bot_user.username],
include_uploads: true,
)
expect(messages.length).to eq(1)
content = messages[0][:content]
upload_hashes = content.select { |c| c.is_a?(Hash) }
expect(upload_hashes).to include(
{ upload_id: image_upload1.id },
{ upload_id: image_upload2.id },
)
text = content.select { |c| c.is_a?(String) }.join(" ")
expect(text).to include("This is the original")
expect(text).to include("(1 month ago)")
expect(text).to include("#{upload_markdown}")
expect(text).to include("#{upload2_markdown}")
end
it "only include regular posts" do
first_post.update!(post_type: Post.types[:whisper])
context =
described_class.messages_from_post(
third_post,
max_posts: 10,
bot_usernames: [bot_user.username],
include_uploads: false,
)
# skips leading model reply which makes no sense cause first post was whisper
expect(context).to eq(
[{ type: :user, content: "This is a second reply by the user", id: user.username }],
)
end
context "with a secure upload not visible to the triggering user" do
fab!(:secure_upload) do
private_category = Fabricate(:private_category, group: Fabricate(:group))
private_topic = Fabricate(:topic, category: private_category)
private_post = Fabricate(:post, topic: private_topic)
Fabricate(
:secure_upload,
original_filename: "secret.png",
extension: "png",
access_control_post: private_post,
)
end
before { SiteSetting.embedded_media_post_allowed_groups = Group::AUTO_GROUPS[:everyone] }
it "drops the upload from the prompt" do
post_with_secure_upload =
create_post(
topic_id: pm.id,
user: user,
raw: "Look at this ![image](#{secure_upload.short_url})",
)
context =
described_class.messages_from_post(
post_with_secure_upload,
max_posts: 1,
bot_usernames: [bot_user.username],
include_image_uploads: true,
include_document_uploads: false,
)
expect(context).to contain_exactly(
{ type: :user, id: user.username, content: post_with_secure_upload.raw },
)
end
end
context "with custom prompts" do
it "When post custom prompt is present, we use that instead of the post content" do
custom_prompt = [
[
{ name: "time", arguments: { name: "time", timezone: "Buenos Aires" } }.to_json,
"time",
"tool_call",
],
[
{ args: { timezone: "Buenos Aires" }, time: "2023-12-14 17:24:00 -0300" }.to_json,
"time",
"tool",
],
["I replied to the time command", bot_user.username],
]
PostCustomPrompt.create!(post: second_post, custom_prompt: custom_prompt)
context =
described_class.messages_from_post(
third_post,
max_posts: 10,
bot_usernames: [bot_user.username],
include_uploads: false,
)
expect(context).to eq(
[
{ type: :user, content: "This is a reply by the user", id: user.username },
{ type: :tool_call, content: custom_prompt.first.first, id: "time" },
{ type: :tool, id: "time", content: custom_prompt.second.first },
{ type: :model, content: custom_prompt.third.first },
{ type: :user, content: "This is a second reply by the user", id: user.username },
],
)
end
it "normalizes saved thinking provider info" do
custom_prompt = [
[
"Grounded answer",
bot_user.username,
nil,
nil,
{
"message" => "Web search: OpenAI news",
"provider_info" => {
"gemini" => {
"grounding_metadata" => {
"webSearchQueries" => ["OpenAI news"],
},
},
},
},
],
]
PostCustomPrompt.create!(post: second_post, custom_prompt: custom_prompt)
context =
described_class.messages_from_post(
third_post,
max_posts: 10,
bot_usernames: [bot_user.username],
include_uploads: false,
)
expect(context).to include(
{
type: :model,
content: "Grounded answer",
thinking: "Web search: OpenAI news",
thinking_provider_info: {
gemini: {
grounding_metadata: {
webSearchQueries: ["OpenAI news"],
},
},
},
},
)
end
end
end
end