|
2 | 2 |
|
3 | 3 | describe DiscourseAi::Completions::PromptMessagesBuilder do |
4 | 4 | let(:builder) { DiscourseAi::Completions::PromptMessagesBuilder.new } |
| 5 | + fab!(:user) |
| 6 | + fab!(:bot_user) { Fabricate(:user) } |
| 7 | + fab!(:other_user) { Fabricate(:user) } |
5 | 8 |
|
6 | 9 | it "should allow merging user messages" do |
7 | 10 | builder.push(type: :user, content: "Hello", name: "Alice") |
|
14 | 17 | builder.push(type: :user, content: "Hello", name: "Alice", upload_ids: [1, 2]) |
15 | 18 |
|
16 | 19 | 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" }], |
18 | 21 | ) |
19 | 22 | end |
20 | 23 |
|
|
64 | 67 | expect(content).to include("Alice") |
65 | 68 | expect(content).to include("How do I solve this") |
66 | 69 | 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 |
67 | 331 | end |
0 commit comments