@convex-dev/agent 0.2.6-alpha.1 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/createTool.js +1 -1
- package/dist/client/createTool.js.map +1 -1
- package/dist/client/definePlaygroundAPI.d.ts +6 -4
- package/dist/client/definePlaygroundAPI.d.ts.map +1 -1
- package/dist/client/definePlaygroundAPI.js +15 -6
- package/dist/client/definePlaygroundAPI.js.map +1 -1
- package/dist/client/index.d.ts +55 -122
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +47 -371
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +1 -1
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/mockModel.d.ts +5 -3
- package/dist/client/mockModel.d.ts.map +1 -1
- package/dist/client/mockModel.js +33 -24
- package/dist/client/mockModel.js.map +1 -1
- package/dist/client/saveInputMessages.d.ts +20 -0
- package/dist/client/saveInputMessages.d.ts.map +1 -0
- package/dist/client/saveInputMessages.js +57 -0
- package/dist/client/saveInputMessages.js.map +1 -0
- package/dist/client/search.d.ts +110 -9
- package/dist/client/search.d.ts.map +1 -1
- package/dist/client/search.js +271 -39
- package/dist/client/search.js.map +1 -1
- package/dist/client/start.d.ts +84 -0
- package/dist/client/start.d.ts.map +1 -0
- package/dist/client/start.js +171 -0
- package/dist/client/start.js.map +1 -0
- package/dist/client/streaming.d.ts +8 -8
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +2 -1
- package/dist/client/streaming.js.map +1 -1
- package/dist/client/textStreamParts.d.ts.map +1 -1
- package/dist/client/textStreamParts.js +2 -9
- package/dist/client/textStreamParts.js.map +1 -1
- package/dist/client/threads.d.ts +1 -1
- package/dist/client/threads.d.ts.map +1 -1
- package/dist/client/types.d.ts +147 -17
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/api.d.ts +11 -3
- package/dist/component/messages.d.ts +13 -4
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +67 -25
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +2 -1643
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +0 -24
- package/dist/component/schema.js.map +1 -1
- package/dist/mapping.d.ts +5 -8
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +72 -6
- package/dist/mapping.js.map +1 -1
- package/dist/react/deltas.d.ts.map +1 -1
- package/dist/react/deltas.js +1 -0
- package/dist/react/deltas.js.map +1 -1
- package/dist/react/fromUIMessages.d.ts +13 -0
- package/dist/react/fromUIMessages.d.ts.map +1 -0
- package/dist/react/fromUIMessages.js +75 -0
- package/dist/react/fromUIMessages.js.map +1 -0
- package/dist/react/toUIMessages.d.ts +5 -2
- package/dist/react/toUIMessages.d.ts.map +1 -1
- package/dist/react/toUIMessages.js +3 -0
- package/dist/react/toUIMessages.js.map +1 -1
- package/dist/shared.d.ts +10 -0
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +26 -0
- package/dist/shared.js.map +1 -1
- package/dist/validators.d.ts +1640 -0
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +41 -0
- package/dist/validators.js.map +1 -1
- package/package.json +4 -4
- package/src/client/createTool.ts +1 -1
- package/src/client/definePlaygroundAPI.ts +16 -7
- package/src/client/index.test.ts +11 -46
- package/src/client/index.ts +128 -573
- package/src/client/messages.ts +1 -1
- package/src/client/mock.json +68 -0
- package/src/client/mockModel.ts +47 -31
- package/src/client/saveInputMessages.test.ts +576 -0
- package/src/client/saveInputMessages.ts +100 -0
- package/src/client/search.test.ts +1017 -0
- package/src/client/search.ts +446 -68
- package/src/client/start.ts +315 -0
- package/src/client/stream.json +48 -0
- package/src/client/streaming.ts +3 -3
- package/src/client/textStreamParts.ts +2 -11
- package/src/client/threads.ts +1 -1
- package/src/client/types.ts +153 -22
- package/src/component/_generated/api.d.ts +11 -3
- package/src/component/messages.ts +73 -27
- package/src/component/schema.ts +1 -29
- package/src/mapping.ts +83 -6
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/src/react/deltas.ts +1 -0
- package/src/react/fromUIMessages.test.ts +467 -0
- package/src/react/fromUIMessages.ts +90 -0
- package/src/react/toUIMessages.ts +21 -13
- package/src/shared.ts +33 -0
- package/src/validators.test.ts +13 -2
- package/src/validators.ts +48 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { toUIMessages } from "./toUIMessages.js";
|
|
3
|
+
import { fromUIMessages } from "./fromUIMessages.js";
|
|
4
|
+
import type { MessageDoc } from "../client/index.js";
|
|
5
|
+
import type { UIMessage } from "./toUIMessages.js";
|
|
6
|
+
|
|
7
|
+
// Helper to create a base message doc
|
|
8
|
+
function baseMessageDoc<T = unknown>(overrides: Partial<MessageDoc & { streaming?: boolean; metadata?: T }> = {}): MessageDoc & { streaming?: boolean; metadata?: T } {
|
|
9
|
+
return {
|
|
10
|
+
_id: "msg1",
|
|
11
|
+
_creationTime: Date.now(),
|
|
12
|
+
order: 0,
|
|
13
|
+
stepOrder: 0,
|
|
14
|
+
status: "success",
|
|
15
|
+
threadId: "thread1",
|
|
16
|
+
tool: false,
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("fromUIMessages round-trip tests", () => {
|
|
22
|
+
it("preserves essential data for simple user message", () => {
|
|
23
|
+
const originalMessages = [
|
|
24
|
+
baseMessageDoc({
|
|
25
|
+
message: {
|
|
26
|
+
role: "user",
|
|
27
|
+
content: "Hello world!",
|
|
28
|
+
},
|
|
29
|
+
text: "Hello world!",
|
|
30
|
+
}),
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const uiMessages = toUIMessages(originalMessages);
|
|
34
|
+
const backToMessageDocs = fromUIMessages("thread1", uiMessages);
|
|
35
|
+
|
|
36
|
+
expect(uiMessages).toHaveLength(1);
|
|
37
|
+
expect(uiMessages[0].role).toBe("user");
|
|
38
|
+
expect(uiMessages[0].text).toBe("Hello world!");
|
|
39
|
+
|
|
40
|
+
expect(backToMessageDocs).toHaveLength(1);
|
|
41
|
+
expect(backToMessageDocs[0].text).toBe("Hello world!");
|
|
42
|
+
expect(backToMessageDocs[0].threadId).toBe("thread1");
|
|
43
|
+
|
|
44
|
+
// Content gets normalized to array format
|
|
45
|
+
expect(Array.isArray(backToMessageDocs[0].message?.content)).toBe(true);
|
|
46
|
+
if (Array.isArray(backToMessageDocs[0].message?.content)) {
|
|
47
|
+
expect(backToMessageDocs[0].message.content[0]).toMatchObject({
|
|
48
|
+
type: "text",
|
|
49
|
+
text: "Hello world!",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("preserves essential data for assistant message", () => {
|
|
55
|
+
const originalMessages = [
|
|
56
|
+
baseMessageDoc({
|
|
57
|
+
message: {
|
|
58
|
+
role: "assistant",
|
|
59
|
+
content: "Hi there! How can I help?",
|
|
60
|
+
},
|
|
61
|
+
text: "Hi there! How can I help?",
|
|
62
|
+
}),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const uiMessages = toUIMessages(originalMessages);
|
|
66
|
+
const backToMessageDocs = fromUIMessages("thread1", uiMessages);
|
|
67
|
+
|
|
68
|
+
expect(uiMessages).toHaveLength(1);
|
|
69
|
+
expect(uiMessages[0].role).toBe("assistant");
|
|
70
|
+
expect(uiMessages[0].text).toBe("Hi there! How can I help?");
|
|
71
|
+
|
|
72
|
+
expect(backToMessageDocs).toHaveLength(1);
|
|
73
|
+
expect(backToMessageDocs[0].text).toBe("Hi there! How can I help?");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("preserves system messages correctly", () => {
|
|
77
|
+
const originalMessages = [
|
|
78
|
+
baseMessageDoc({
|
|
79
|
+
message: {
|
|
80
|
+
role: "system",
|
|
81
|
+
content: "You are a helpful assistant.",
|
|
82
|
+
},
|
|
83
|
+
text: "You are a helpful assistant.",
|
|
84
|
+
}),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const uiMessages = toUIMessages(originalMessages);
|
|
88
|
+
const backToMessageDocs = fromUIMessages("thread1", uiMessages);
|
|
89
|
+
|
|
90
|
+
expect(uiMessages).toHaveLength(1);
|
|
91
|
+
expect(uiMessages[0].role).toBe("system");
|
|
92
|
+
expect(uiMessages[0].text).toBe("You are a helpful assistant.");
|
|
93
|
+
|
|
94
|
+
expect(backToMessageDocs).toHaveLength(1);
|
|
95
|
+
expect(backToMessageDocs[0].text).toBe("You are a helpful assistant.");
|
|
96
|
+
expect(backToMessageDocs[0].message?.role).toBe("system");
|
|
97
|
+
|
|
98
|
+
// System content stays as string
|
|
99
|
+
expect(backToMessageDocs[0].message?.content).toBe(
|
|
100
|
+
"You are a helpful assistant.",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("preserves reasoning in assistant messages", () => {
|
|
105
|
+
const originalMessages = [
|
|
106
|
+
baseMessageDoc({
|
|
107
|
+
message: {
|
|
108
|
+
role: "assistant",
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "reasoning",
|
|
112
|
+
text: "Let me think about this...",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: "text",
|
|
116
|
+
text: "Here's my response.",
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
text: "Here's my response.",
|
|
121
|
+
reasoning: "Let me think about this...",
|
|
122
|
+
}),
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const uiMessages = toUIMessages(originalMessages);
|
|
126
|
+
const backToMessageDocs = fromUIMessages("thread1", uiMessages);
|
|
127
|
+
|
|
128
|
+
expect(uiMessages).toHaveLength(1);
|
|
129
|
+
expect(uiMessages[0].text).toBe("Here's my response.");
|
|
130
|
+
|
|
131
|
+
// Check that reasoning parts are preserved in UI message
|
|
132
|
+
const reasoningParts = uiMessages[0].parts.filter(
|
|
133
|
+
(part) => part.type === "reasoning",
|
|
134
|
+
);
|
|
135
|
+
expect(reasoningParts).toHaveLength(1);
|
|
136
|
+
expect(reasoningParts[0]).toMatchObject({
|
|
137
|
+
type: "reasoning",
|
|
138
|
+
text: "Let me think about this...",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(backToMessageDocs).toHaveLength(1);
|
|
142
|
+
expect(backToMessageDocs[0].text).toBe("Here's my response.");
|
|
143
|
+
expect(backToMessageDocs[0].reasoning).toBe("Let me think about this...");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("handles tool calls and groups them correctly", () => {
|
|
147
|
+
// Tool calls get grouped into single UI message but expanded back to multiple message docs
|
|
148
|
+
const originalMessages = [
|
|
149
|
+
baseMessageDoc({
|
|
150
|
+
_id: "msg1",
|
|
151
|
+
order: 1,
|
|
152
|
+
stepOrder: 1,
|
|
153
|
+
message: {
|
|
154
|
+
role: "assistant",
|
|
155
|
+
content: [
|
|
156
|
+
{
|
|
157
|
+
type: "tool-call",
|
|
158
|
+
toolName: "calculator",
|
|
159
|
+
toolCallId: "call1",
|
|
160
|
+
args: { operation: "add", a: 2, b: 3 },
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
tool: true,
|
|
165
|
+
}),
|
|
166
|
+
baseMessageDoc({
|
|
167
|
+
_id: "msg2",
|
|
168
|
+
order: 1,
|
|
169
|
+
stepOrder: 2,
|
|
170
|
+
message: {
|
|
171
|
+
role: "tool",
|
|
172
|
+
content: [
|
|
173
|
+
{
|
|
174
|
+
type: "tool-result",
|
|
175
|
+
toolCallId: "call1",
|
|
176
|
+
toolName: "calculator",
|
|
177
|
+
output: {
|
|
178
|
+
type: "json",
|
|
179
|
+
value: { result: 5 },
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
tool: true,
|
|
185
|
+
}),
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
const uiMessages = toUIMessages(originalMessages);
|
|
189
|
+
const backToMessageDocs = fromUIMessages("thread1", uiMessages);
|
|
190
|
+
|
|
191
|
+
// Should be grouped into single UI message
|
|
192
|
+
expect(uiMessages).toHaveLength(1);
|
|
193
|
+
expect(uiMessages[0].role).toBe("assistant");
|
|
194
|
+
|
|
195
|
+
// Check tool parts exist
|
|
196
|
+
const toolParts = uiMessages[0].parts.filter(
|
|
197
|
+
(part) => part.type === "tool-calculator",
|
|
198
|
+
);
|
|
199
|
+
expect(toolParts).toHaveLength(1);
|
|
200
|
+
expect(toolParts[0]).toMatchObject({
|
|
201
|
+
type: "tool-calculator",
|
|
202
|
+
toolCallId: "call1",
|
|
203
|
+
state: "output-available",
|
|
204
|
+
input: { operation: "add", a: 2, b: 3 },
|
|
205
|
+
output: { result: 5 },
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Should expand back to multiple message docs
|
|
209
|
+
expect(backToMessageDocs.length).toBeGreaterThanOrEqual(1);
|
|
210
|
+
|
|
211
|
+
// Check that tool information is preserved
|
|
212
|
+
const toolMessages = backToMessageDocs.filter((msg) => msg.tool);
|
|
213
|
+
expect(toolMessages.length).toBeGreaterThan(0);
|
|
214
|
+
expect(toolMessages[0].stepOrder).toBe(1);
|
|
215
|
+
expect(toolMessages[1].stepOrder).toBe(2);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("preserves file attachments in user messages", () => {
|
|
219
|
+
const originalMessages = [
|
|
220
|
+
baseMessageDoc({
|
|
221
|
+
message: {
|
|
222
|
+
role: "user",
|
|
223
|
+
content: [
|
|
224
|
+
{
|
|
225
|
+
type: "text",
|
|
226
|
+
text: "What's in this image?",
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
type: "file",
|
|
230
|
+
mimeType: "image/png",
|
|
231
|
+
data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
text: "What's in this image?",
|
|
236
|
+
}),
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
const uiMessages = toUIMessages(originalMessages);
|
|
240
|
+
const backToMessageDocs = fromUIMessages("thread1", uiMessages);
|
|
241
|
+
|
|
242
|
+
expect(uiMessages).toHaveLength(1);
|
|
243
|
+
expect(uiMessages[0].role).toBe("user");
|
|
244
|
+
expect(uiMessages[0].text).toBe("What's in this image?");
|
|
245
|
+
|
|
246
|
+
// Check file parts exist in UI message
|
|
247
|
+
const fileParts = uiMessages[0].parts.filter(
|
|
248
|
+
(part) => part.type === "file",
|
|
249
|
+
);
|
|
250
|
+
expect(fileParts).toHaveLength(1);
|
|
251
|
+
|
|
252
|
+
expect(backToMessageDocs).toHaveLength(1);
|
|
253
|
+
expect(backToMessageDocs[0].text).toBe("What's in this image?");
|
|
254
|
+
|
|
255
|
+
// Check file content is preserved
|
|
256
|
+
const content = backToMessageDocs[0].message?.content;
|
|
257
|
+
expect(Array.isArray(content)).toBe(true);
|
|
258
|
+
if (Array.isArray(content)) {
|
|
259
|
+
const fileContent = content.find((c) => c.type === "file");
|
|
260
|
+
expect(fileContent).toBeDefined();
|
|
261
|
+
expect(fileContent).toMatchObject({
|
|
262
|
+
type: "file",
|
|
263
|
+
mimeType: "image/png",
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("preserves sources correctly", () => {
|
|
269
|
+
const originalMessages = [
|
|
270
|
+
baseMessageDoc({
|
|
271
|
+
message: {
|
|
272
|
+
role: "assistant",
|
|
273
|
+
content: [
|
|
274
|
+
{
|
|
275
|
+
type: "text",
|
|
276
|
+
text: "I found some relevant sources.",
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
text: "I found some relevant sources.",
|
|
281
|
+
sources: [
|
|
282
|
+
{
|
|
283
|
+
type: "source",
|
|
284
|
+
sourceType: "url",
|
|
285
|
+
id: "source1",
|
|
286
|
+
url: "https://example.com",
|
|
287
|
+
title: "Example Source",
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
type: "source",
|
|
291
|
+
sourceType: "document",
|
|
292
|
+
id: "source2",
|
|
293
|
+
mediaType: "application/pdf",
|
|
294
|
+
title: "Document Source",
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
}),
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
const uiMessages = toUIMessages(originalMessages);
|
|
301
|
+
const backToMessageDocs = fromUIMessages("thread1", uiMessages);
|
|
302
|
+
|
|
303
|
+
expect(uiMessages).toHaveLength(1);
|
|
304
|
+
|
|
305
|
+
// Check source parts exist in UI message
|
|
306
|
+
const sourceParts = uiMessages[0].parts.filter(
|
|
307
|
+
(part) => part.type === "source-url" || part.type === "source-document",
|
|
308
|
+
);
|
|
309
|
+
expect(sourceParts).toHaveLength(2);
|
|
310
|
+
|
|
311
|
+
expect(backToMessageDocs).toHaveLength(1);
|
|
312
|
+
expect(backToMessageDocs[0].sources).toHaveLength(2);
|
|
313
|
+
expect(backToMessageDocs[0].sources![0]).toMatchObject({
|
|
314
|
+
type: "source",
|
|
315
|
+
sourceType: "url",
|
|
316
|
+
id: "source1",
|
|
317
|
+
url: "https://example.com",
|
|
318
|
+
title: "Example Source",
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("preserves metadata when provided", () => {
|
|
323
|
+
const testMetadata = {
|
|
324
|
+
customField: "customValue",
|
|
325
|
+
timestamp: Date.now(),
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const originalMessages = [
|
|
329
|
+
baseMessageDoc({
|
|
330
|
+
message: {
|
|
331
|
+
role: "user",
|
|
332
|
+
content: "Test message",
|
|
333
|
+
},
|
|
334
|
+
text: "Test message",
|
|
335
|
+
metadata: testMetadata,
|
|
336
|
+
}),
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
const uiMessages = toUIMessages(originalMessages);
|
|
340
|
+
const backToMessageDocs = fromUIMessages("thread1", uiMessages);
|
|
341
|
+
|
|
342
|
+
expect(uiMessages).toHaveLength(1);
|
|
343
|
+
expect(uiMessages[0].metadata).toEqual(testMetadata);
|
|
344
|
+
|
|
345
|
+
expect(backToMessageDocs).toHaveLength(1);
|
|
346
|
+
expect(backToMessageDocs[0].metadata).toEqual(testMetadata);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("handles streaming status correctly", () => {
|
|
350
|
+
const originalMessages = [
|
|
351
|
+
baseMessageDoc({
|
|
352
|
+
message: {
|
|
353
|
+
role: "assistant",
|
|
354
|
+
content: "Streaming response...",
|
|
355
|
+
},
|
|
356
|
+
text: "Streaming response...",
|
|
357
|
+
streaming: true,
|
|
358
|
+
status: "pending",
|
|
359
|
+
}),
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
const uiMessages = toUIMessages(originalMessages);
|
|
363
|
+
const backToMessageDocs = fromUIMessages("thread1", uiMessages);
|
|
364
|
+
|
|
365
|
+
expect(uiMessages).toHaveLength(1);
|
|
366
|
+
expect(uiMessages[0].status).toBe("streaming");
|
|
367
|
+
|
|
368
|
+
expect(backToMessageDocs).toHaveLength(1);
|
|
369
|
+
expect(backToMessageDocs[0].streaming).toBe(true);
|
|
370
|
+
expect(backToMessageDocs[0].status).toBe("pending");
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe("fromUIMessages functionality tests", () => {
|
|
375
|
+
it("handles empty messages array", () => {
|
|
376
|
+
const uiMessages: UIMessage[] = [];
|
|
377
|
+
const result = fromUIMessages("thread1", uiMessages);
|
|
378
|
+
expect(result).toHaveLength(0);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("correctly assigns thread ID", () => {
|
|
382
|
+
const uiMessage: UIMessage = {
|
|
383
|
+
id: "test-id",
|
|
384
|
+
_creationTime: Date.now(),
|
|
385
|
+
order: 0,
|
|
386
|
+
stepOrder: 0,
|
|
387
|
+
status: "success",
|
|
388
|
+
key: "test-key",
|
|
389
|
+
text: "Hello",
|
|
390
|
+
role: "user",
|
|
391
|
+
parts: [{ type: "text", text: "Hello" }],
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const result = fromUIMessages("custom-thread-id", [uiMessage]);
|
|
395
|
+
expect(result).toHaveLength(1);
|
|
396
|
+
expect(result[0].threadId).toBe("custom-thread-id");
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("correctly determines tool status", () => {
|
|
400
|
+
const toolUIMessage: UIMessage = {
|
|
401
|
+
id: "tool-id",
|
|
402
|
+
_creationTime: Date.now(),
|
|
403
|
+
order: 0,
|
|
404
|
+
stepOrder: 0,
|
|
405
|
+
status: "success",
|
|
406
|
+
key: "tool-key",
|
|
407
|
+
text: "",
|
|
408
|
+
role: "assistant",
|
|
409
|
+
parts: [
|
|
410
|
+
{
|
|
411
|
+
type: "tool-calculator",
|
|
412
|
+
toolCallId: "call1",
|
|
413
|
+
input: { a: 1, b: 2 },
|
|
414
|
+
state: "output-available",
|
|
415
|
+
output: { result: 3 },
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const result = fromUIMessages("thread1", [toolUIMessage]);
|
|
421
|
+
expect(result.length).toBeGreaterThan(0);
|
|
422
|
+
|
|
423
|
+
// Should have tool messages
|
|
424
|
+
const toolMessages = result.filter((msg) => msg.tool);
|
|
425
|
+
expect(toolMessages.length).toBeGreaterThan(0);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("handles tool calls without responses", () => {
|
|
429
|
+
const toolUIMessage: UIMessage = {
|
|
430
|
+
id: "tool-id",
|
|
431
|
+
_creationTime: Date.now(),
|
|
432
|
+
order: 0,
|
|
433
|
+
stepOrder: 0,
|
|
434
|
+
status: "success",
|
|
435
|
+
key: "tool-key",
|
|
436
|
+
text: "",
|
|
437
|
+
role: "assistant",
|
|
438
|
+
parts: [
|
|
439
|
+
{ type: "text", text: "Tool call" },
|
|
440
|
+
{
|
|
441
|
+
type: "tool-calculator",
|
|
442
|
+
toolCallId: "call1",
|
|
443
|
+
input: { a: 1, b: 2 },
|
|
444
|
+
state: "input-available",
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const result = fromUIMessages("thread1", [toolUIMessage]);
|
|
450
|
+
expect(result.length).toBeGreaterThan(0);
|
|
451
|
+
|
|
452
|
+
// Should have tool messages
|
|
453
|
+
const toolMessages = result.filter((msg) => msg.tool);
|
|
454
|
+
expect(toolMessages.length).toBe(1);
|
|
455
|
+
expect(toolMessages[0].message?.role).toBe("assistant");
|
|
456
|
+
expect(toolMessages[0].message?.content[0]).toMatchObject({
|
|
457
|
+
type: "text",
|
|
458
|
+
text: "Tool call",
|
|
459
|
+
});
|
|
460
|
+
expect(toolMessages[0].message?.content[1]).toMatchObject({
|
|
461
|
+
args: { a: 1, b: 2 },
|
|
462
|
+
toolCallId: "call1",
|
|
463
|
+
toolName: "calculator",
|
|
464
|
+
type: "tool-call",
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { convertToModelMessages } from "ai";
|
|
2
|
+
import { extractReasoning, extractText, isTool } from "../shared.js";
|
|
3
|
+
import type { MessageDoc, vSource } from "../validators.js";
|
|
4
|
+
import type { UIMessage } from "./toUIMessages.js";
|
|
5
|
+
import type { ProviderOptions } from "@ai-sdk/provider-utils";
|
|
6
|
+
import { omit } from "convex-helpers";
|
|
7
|
+
import { fromModelMessage } from "../mapping.js";
|
|
8
|
+
import type { Infer } from "convex/values";
|
|
9
|
+
|
|
10
|
+
export function fromUIMessages<METADATA = unknown>(
|
|
11
|
+
threadId: string,
|
|
12
|
+
messages: (UIMessage<METADATA> & {
|
|
13
|
+
userId?: string;
|
|
14
|
+
model?: string;
|
|
15
|
+
provider?: string;
|
|
16
|
+
providerOptions?: ProviderOptions;
|
|
17
|
+
})[],
|
|
18
|
+
): (MessageDoc & { streaming: boolean; metadata?: METADATA })[] {
|
|
19
|
+
return messages.flatMap((uiMessage) => {
|
|
20
|
+
const stepOrder = uiMessage.stepOrder;
|
|
21
|
+
const commonFields = {
|
|
22
|
+
_id: uiMessage.id,
|
|
23
|
+
...omit(uiMessage, ["parts", "role", "key", "text"]),
|
|
24
|
+
threadId,
|
|
25
|
+
status: uiMessage.status === "streaming" ? "pending" : "success",
|
|
26
|
+
streaming: uiMessage.status === "streaming",
|
|
27
|
+
// to override
|
|
28
|
+
tool: false,
|
|
29
|
+
} satisfies MessageDoc & { streaming: boolean; metadata?: METADATA };
|
|
30
|
+
const modelMessages = convertToModelMessages([uiMessage]);
|
|
31
|
+
return modelMessages
|
|
32
|
+
.map((modelMessage, i) => {
|
|
33
|
+
if (modelMessage.content.length === 0) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const message = fromModelMessage(modelMessage);
|
|
37
|
+
const tool = isTool(message);
|
|
38
|
+
const doc: MessageDoc & { streaming: boolean; metadata?: METADATA } = {
|
|
39
|
+
...commonFields,
|
|
40
|
+
stepOrder: stepOrder + i,
|
|
41
|
+
message,
|
|
42
|
+
tool,
|
|
43
|
+
text: extractText(message),
|
|
44
|
+
reasoning: extractReasoning(message),
|
|
45
|
+
finishReason: tool ? "tool-calls" : "stop",
|
|
46
|
+
sources: fromSourceParts(uiMessage.parts),
|
|
47
|
+
};
|
|
48
|
+
if (Array.isArray(modelMessage.content)) {
|
|
49
|
+
const providerOptions = modelMessage.content.find(
|
|
50
|
+
(c) => c.providerOptions,
|
|
51
|
+
)?.providerOptions;
|
|
52
|
+
if (providerOptions) {
|
|
53
|
+
// convertToModelMessages changes providerMetadata to providerOptions
|
|
54
|
+
doc.providerMetadata = providerOptions;
|
|
55
|
+
doc.providerOptions ??= providerOptions;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return doc;
|
|
59
|
+
})
|
|
60
|
+
.filter((d) => d !== undefined);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fromSourceParts(parts: UIMessage["parts"]): Infer<typeof vSource>[] {
|
|
65
|
+
return parts
|
|
66
|
+
.map((part) => {
|
|
67
|
+
if (part.type === "source-url") {
|
|
68
|
+
return {
|
|
69
|
+
type: "source",
|
|
70
|
+
sourceType: "url",
|
|
71
|
+
url: part.url,
|
|
72
|
+
id: part.sourceId,
|
|
73
|
+
providerMetadata: part.providerMetadata,
|
|
74
|
+
title: part.title,
|
|
75
|
+
} satisfies Infer<typeof vSource>;
|
|
76
|
+
}
|
|
77
|
+
if (part.type === "source-document") {
|
|
78
|
+
return {
|
|
79
|
+
type: "source",
|
|
80
|
+
sourceType: "document",
|
|
81
|
+
mediaType: part.mediaType,
|
|
82
|
+
id: part.sourceId,
|
|
83
|
+
providerMetadata: part.providerMetadata,
|
|
84
|
+
title: part.title,
|
|
85
|
+
} satisfies Infer<typeof vSource>;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
})
|
|
89
|
+
.filter((p) => p !== undefined);
|
|
90
|
+
}
|
|
@@ -30,12 +30,17 @@ export type UIMessage<
|
|
|
30
30
|
_creationTime: number;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
type ExtraFields<METADATA = unknown> = {
|
|
34
|
+
streaming?: boolean;
|
|
35
|
+
metadata?: METADATA;
|
|
36
|
+
};
|
|
37
|
+
|
|
33
38
|
export function toUIMessages<
|
|
34
39
|
METADATA = unknown,
|
|
35
40
|
DATA_PARTS extends UIDataTypes = UIDataTypes,
|
|
36
41
|
TOOLS extends UITools = UITools,
|
|
37
42
|
>(
|
|
38
|
-
messages: (MessageDoc &
|
|
43
|
+
messages: (MessageDoc & ExtraFields<METADATA>)[],
|
|
39
44
|
): UIMessage<METADATA, DATA_PARTS, TOOLS>[] {
|
|
40
45
|
// Group assistant and tool messages together
|
|
41
46
|
const assistantGroups = groupAssistantMessages(messages);
|
|
@@ -55,29 +60,29 @@ export function toUIMessages<
|
|
|
55
60
|
return uiMessages;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
type Group =
|
|
63
|
+
type Group<METADATA = unknown> =
|
|
59
64
|
| {
|
|
60
65
|
role: "user";
|
|
61
|
-
message: MessageDoc &
|
|
66
|
+
message: MessageDoc & ExtraFields<METADATA>;
|
|
62
67
|
}
|
|
63
68
|
| {
|
|
64
69
|
role: "system";
|
|
65
|
-
message: MessageDoc &
|
|
70
|
+
message: MessageDoc & ExtraFields<METADATA>;
|
|
66
71
|
}
|
|
67
72
|
| {
|
|
68
73
|
role: "assistant";
|
|
69
|
-
messages: (MessageDoc &
|
|
74
|
+
messages: (MessageDoc & ExtraFields<METADATA>)[];
|
|
70
75
|
};
|
|
71
76
|
|
|
72
|
-
function groupAssistantMessages(
|
|
73
|
-
messages: (MessageDoc &
|
|
74
|
-
): Group[] {
|
|
75
|
-
const groups: Group[] = [];
|
|
77
|
+
function groupAssistantMessages<METADATA = unknown>(
|
|
78
|
+
messages: (MessageDoc & ExtraFields<METADATA>)[],
|
|
79
|
+
): Group<METADATA>[] {
|
|
80
|
+
const groups: Group<METADATA>[] = [];
|
|
76
81
|
|
|
77
82
|
// Sort messages by order and stepOrder first to handle out-of-order arrivals
|
|
78
83
|
const sortedMessages = sorted(messages);
|
|
79
84
|
|
|
80
|
-
let currentAssistantGroup: (MessageDoc &
|
|
85
|
+
let currentAssistantGroup: (MessageDoc & ExtraFields<METADATA>)[] = [];
|
|
81
86
|
let currentOrder: number | undefined;
|
|
82
87
|
|
|
83
88
|
for (const message of sortedMessages) {
|
|
@@ -145,7 +150,7 @@ function createSystemUIMessage<
|
|
|
145
150
|
DATA_PARTS extends UIDataTypes = UIDataTypes,
|
|
146
151
|
TOOLS extends UITools = UITools,
|
|
147
152
|
>(
|
|
148
|
-
message: MessageDoc &
|
|
153
|
+
message: MessageDoc & ExtraFields<METADATA>,
|
|
149
154
|
): UIMessage<METADATA, DATA_PARTS, TOOLS> {
|
|
150
155
|
const text = extractTextFromMessageDoc(message);
|
|
151
156
|
const partCommon = {
|
|
@@ -166,6 +171,7 @@ function createSystemUIMessage<
|
|
|
166
171
|
role: "system",
|
|
167
172
|
agentName: message.agentName,
|
|
168
173
|
parts: [{ type: "text", text, ...partCommon } satisfies TextUIPart],
|
|
174
|
+
metadata: message.metadata,
|
|
169
175
|
};
|
|
170
176
|
}
|
|
171
177
|
|
|
@@ -180,7 +186,7 @@ function createUserUIMessage<
|
|
|
180
186
|
DATA_PARTS extends UIDataTypes = UIDataTypes,
|
|
181
187
|
TOOLS extends UITools = UITools,
|
|
182
188
|
>(
|
|
183
|
-
message: MessageDoc &
|
|
189
|
+
message: MessageDoc & ExtraFields<METADATA>,
|
|
184
190
|
): UIMessage<METADATA, DATA_PARTS, TOOLS> {
|
|
185
191
|
const text = extractTextFromMessageDoc(message);
|
|
186
192
|
const coreMessage = deserializeMessage(message.message!);
|
|
@@ -224,6 +230,7 @@ function createUserUIMessage<
|
|
|
224
230
|
text,
|
|
225
231
|
role: "user",
|
|
226
232
|
parts,
|
|
233
|
+
metadata: message.metadata,
|
|
227
234
|
};
|
|
228
235
|
}
|
|
229
236
|
|
|
@@ -232,7 +239,7 @@ function createAssistantUIMessage<
|
|
|
232
239
|
DATA_PARTS extends UIDataTypes = UIDataTypes,
|
|
233
240
|
TOOLS extends UITools = UITools,
|
|
234
241
|
>(
|
|
235
|
-
groupUnordered: (MessageDoc &
|
|
242
|
+
groupUnordered: (MessageDoc & ExtraFields<METADATA>)[],
|
|
236
243
|
): UIMessage<METADATA, DATA_PARTS, TOOLS> {
|
|
237
244
|
const group = sorted(groupUnordered);
|
|
238
245
|
const firstMessage = group[0];
|
|
@@ -415,6 +422,7 @@ function createAssistantUIMessage<
|
|
|
415
422
|
text: allText,
|
|
416
423
|
status,
|
|
417
424
|
parts: allParts,
|
|
425
|
+
metadata: group.find((m) => m.metadata)?.metadata,
|
|
418
426
|
};
|
|
419
427
|
}
|
|
420
428
|
|
package/src/shared.ts
CHANGED
|
@@ -37,6 +37,16 @@ export function extractText(message: Message | ModelMessage) {
|
|
|
37
37
|
return undefined;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export function extractReasoning(message: Message | ModelMessage) {
|
|
41
|
+
if (typeof message.content === "string") {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return message.content
|
|
45
|
+
.filter((c) => c.type === "reasoning")
|
|
46
|
+
.map((c) => c.text)
|
|
47
|
+
.join("");
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
export const DEFAULT_MESSAGE_RANGE = { before: 2, after: 1 };
|
|
41
51
|
|
|
42
52
|
export function sorted<T extends { order: number; stepOrder: number }>(
|
|
@@ -49,3 +59,26 @@ export function sorted<T extends { order: number; stepOrder: number }>(
|
|
|
49
59
|
: (a, b) => b.order - a.order || b.stepOrder - a.stepOrder,
|
|
50
60
|
);
|
|
51
61
|
}
|
|
62
|
+
|
|
63
|
+
export type ModelOrMetadata =
|
|
64
|
+
| string
|
|
65
|
+
| ({ provider: string } & ({ modelId: string } | { model: string }));
|
|
66
|
+
|
|
67
|
+
export function getModelName(embeddingModel: ModelOrMetadata): string {
|
|
68
|
+
if (typeof embeddingModel === "string") {
|
|
69
|
+
if (embeddingModel.includes("/")) {
|
|
70
|
+
return embeddingModel.split("/").slice(1).join("/");
|
|
71
|
+
}
|
|
72
|
+
return embeddingModel;
|
|
73
|
+
}
|
|
74
|
+
return "modelId" in embeddingModel
|
|
75
|
+
? embeddingModel.modelId
|
|
76
|
+
: embeddingModel.model;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getProviderName(embeddingModel: ModelOrMetadata): string {
|
|
80
|
+
if (typeof embeddingModel === "string") {
|
|
81
|
+
return embeddingModel.split("/").at(0)!;
|
|
82
|
+
}
|
|
83
|
+
return embeddingModel.provider;
|
|
84
|
+
}
|