@convex-dev/agent 0.2.7 → 0.2.8-alpha.0
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/UIMessages.d.ts +43 -0
- package/dist/UIMessages.d.ts.map +1 -0
- package/dist/{react/toUIMessages.js → UIMessages.js} +97 -4
- package/dist/UIMessages.js.map +1 -0
- package/dist/client/index.d.ts +9 -9
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +22 -19
- package/dist/client/index.js.map +1 -1
- package/dist/client/search.d.ts.map +1 -1
- package/dist/client/search.js +2 -0
- package/dist/client/search.js.map +1 -1
- package/dist/client/start.d.ts.map +1 -1
- package/dist/client/start.js +0 -1
- package/dist/client/start.js.map +1 -1
- package/dist/client/streaming.d.ts +23 -4
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +32 -6
- package/dist/client/streaming.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -0
- package/dist/component/messages.js +1 -1
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +10 -4
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +4 -0
- package/dist/component/schema.js.map +1 -1
- package/dist/component/streams.d.ts +2 -4
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +42 -43
- package/dist/component/streams.js.map +1 -1
- package/dist/deltas.d.ts +48 -0
- package/dist/deltas.d.ts.map +1 -0
- package/dist/deltas.js +468 -0
- package/dist/deltas.js.map +1 -0
- package/dist/react/index.d.ts +4 -77
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +4 -178
- package/dist/react/index.js.map +1 -1
- package/dist/react/optimisticallySendMessage.d.ts +36 -3
- package/dist/react/optimisticallySendMessage.d.ts.map +1 -1
- package/dist/react/optimisticallySendMessage.js +37 -5
- package/dist/react/optimisticallySendMessage.js.map +1 -1
- package/dist/react/types.d.ts +4 -18
- package/dist/react/types.d.ts.map +1 -1
- package/dist/react/useStreamingUIMessages.d.ts +22 -0
- package/dist/react/useStreamingUIMessages.d.ts.map +1 -0
- package/dist/react/useStreamingUIMessages.js +204 -0
- package/dist/react/useStreamingUIMessages.js.map +1 -0
- package/dist/react/useThreadMessages.d.ts +103 -0
- package/dist/react/useThreadMessages.d.ts.map +1 -0
- package/dist/react/useThreadMessages.js +139 -0
- package/dist/react/useThreadMessages.js.map +1 -0
- package/dist/react/useUIMessages.d.ts +91 -0
- package/dist/react/useUIMessages.d.ts.map +1 -0
- package/dist/react/useUIMessages.js +115 -0
- package/dist/react/useUIMessages.js.map +1 -0
- package/dist/validators.d.ts +3 -1
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +1 -0
- package/dist/validators.js.map +1 -1
- package/package.json +1 -1
- package/src/{react/toUIMessages.ts → UIMessages.ts} +132 -15
- package/src/client/index.ts +30 -24
- package/src/client/search.ts +2 -0
- package/src/client/start.ts +0 -1
- package/src/client/streaming.test.ts +186 -0
- package/src/client/streaming.ts +50 -9
- package/src/component/_generated/api.d.ts +2 -0
- package/src/component/messages.ts +1 -1
- package/src/component/schema.ts +6 -0
- package/src/component/streams.ts +49 -66
- package/src/{react/deltas.test.ts → deltas.test.ts} +57 -46
- package/src/deltas.ts +589 -0
- package/src/{react/fromUIMessages.test.ts → fromUIMessages.test.ts} +69 -44
- package/src/react/index.ts +8 -250
- package/src/react/optimisticallySendMessage.ts +57 -8
- package/src/react/types.ts +6 -39
- package/src/react/useStreamingUIMessages.ts +294 -0
- package/src/react/useThreadMessages.ts +251 -0
- package/src/react/useUIMessages.ts +206 -0
- package/src/{react/toUIMessages.test.ts → toUIMessages.test.ts} +3 -3
- package/src/validators.ts +3 -0
- package/dist/client/textStreamParts.d.ts +0 -5
- package/dist/client/textStreamParts.d.ts.map +0 -1
- package/dist/client/textStreamParts.js +0 -44
- package/dist/client/textStreamParts.js.map +0 -1
- package/dist/react/deltas.d.ts +0 -27
- package/dist/react/deltas.d.ts.map +0 -1
- package/dist/react/deltas.js +0 -627
- package/dist/react/deltas.js.map +0 -1
- package/dist/react/fromUIMessages.d.ts +0 -13
- package/dist/react/fromUIMessages.d.ts.map +0 -1
- package/dist/react/fromUIMessages.js +0 -75
- package/dist/react/fromUIMessages.js.map +0 -1
- package/dist/react/toUIMessages.d.ts +0 -19
- package/dist/react/toUIMessages.d.ts.map +0 -1
- package/dist/react/toUIMessages.js.map +0 -1
- package/src/client/textStreamParts.ts +0 -62
- package/src/react/deltas.ts +0 -774
- package/src/react/fromUIMessages.ts +0 -90
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import type { UIMessage } from "./toUIMessages.js";
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { MessageDoc } from "./client/index.js";
|
|
3
|
+
import type { UIMessage } from "./UIMessages.js";
|
|
4
|
+
import { fromUIMessages, toUIMessages } from "./UIMessages.js";
|
|
6
5
|
|
|
7
6
|
// Helper to create a base message doc
|
|
8
|
-
function baseMessageDoc<T = unknown>(
|
|
7
|
+
function baseMessageDoc<T = unknown>(
|
|
8
|
+
overrides: Partial<MessageDoc & { streaming?: boolean; metadata?: T }> = {},
|
|
9
|
+
): MessageDoc & { streaming?: boolean; metadata?: T } {
|
|
9
10
|
return {
|
|
10
11
|
_id: "msg1",
|
|
11
12
|
_creationTime: Date.now(),
|
|
@@ -31,7 +32,9 @@ describe("fromUIMessages round-trip tests", () => {
|
|
|
31
32
|
];
|
|
32
33
|
|
|
33
34
|
const uiMessages = toUIMessages(originalMessages);
|
|
34
|
-
const backToMessageDocs = fromUIMessages(
|
|
35
|
+
const backToMessageDocs = fromUIMessages(uiMessages, {
|
|
36
|
+
threadId: "thread1",
|
|
37
|
+
});
|
|
35
38
|
|
|
36
39
|
expect(uiMessages).toHaveLength(1);
|
|
37
40
|
expect(uiMessages[0].role).toBe("user");
|
|
@@ -63,7 +66,9 @@ describe("fromUIMessages round-trip tests", () => {
|
|
|
63
66
|
];
|
|
64
67
|
|
|
65
68
|
const uiMessages = toUIMessages(originalMessages);
|
|
66
|
-
const backToMessageDocs = fromUIMessages(
|
|
69
|
+
const backToMessageDocs = fromUIMessages(uiMessages, {
|
|
70
|
+
threadId: "thread1",
|
|
71
|
+
});
|
|
67
72
|
|
|
68
73
|
expect(uiMessages).toHaveLength(1);
|
|
69
74
|
expect(uiMessages[0].role).toBe("assistant");
|
|
@@ -85,7 +90,9 @@ describe("fromUIMessages round-trip tests", () => {
|
|
|
85
90
|
];
|
|
86
91
|
|
|
87
92
|
const uiMessages = toUIMessages(originalMessages);
|
|
88
|
-
const backToMessageDocs = fromUIMessages(
|
|
93
|
+
const backToMessageDocs = fromUIMessages(uiMessages, {
|
|
94
|
+
threadId: "thread1",
|
|
95
|
+
});
|
|
89
96
|
|
|
90
97
|
expect(uiMessages).toHaveLength(1);
|
|
91
98
|
expect(uiMessages[0].role).toBe("system");
|
|
@@ -123,7 +130,9 @@ describe("fromUIMessages round-trip tests", () => {
|
|
|
123
130
|
];
|
|
124
131
|
|
|
125
132
|
const uiMessages = toUIMessages(originalMessages);
|
|
126
|
-
const backToMessageDocs = fromUIMessages(
|
|
133
|
+
const backToMessageDocs = fromUIMessages(uiMessages, {
|
|
134
|
+
threadId: "thread1",
|
|
135
|
+
});
|
|
127
136
|
|
|
128
137
|
expect(uiMessages).toHaveLength(1);
|
|
129
138
|
expect(uiMessages[0].text).toBe("Here's my response.");
|
|
@@ -184,35 +193,41 @@ describe("fromUIMessages round-trip tests", () => {
|
|
|
184
193
|
tool: true,
|
|
185
194
|
}),
|
|
186
195
|
];
|
|
196
|
+
const toTest = [originalMessages, [...originalMessages].reverse()];
|
|
197
|
+
for (const messages of toTest) {
|
|
198
|
+
const uiMessages = toUIMessages(messages);
|
|
199
|
+
const backToMessageDocs = fromUIMessages(uiMessages, {
|
|
200
|
+
threadId: "thread1",
|
|
201
|
+
});
|
|
187
202
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
});
|
|
203
|
+
// Should be grouped into single UI message
|
|
204
|
+
expect(uiMessages).toHaveLength(1);
|
|
205
|
+
const uiMessage = uiMessages[0];
|
|
206
|
+
expect(uiMessage.role).toBe("assistant");
|
|
207
|
+
expect(uiMessage.id).toBe("msg1");
|
|
208
|
+
|
|
209
|
+
// Check tool parts exist
|
|
210
|
+
const toolParts = uiMessage.parts.filter(
|
|
211
|
+
(part) => part.type === "tool-calculator",
|
|
212
|
+
);
|
|
213
|
+
expect(toolParts).toHaveLength(1);
|
|
214
|
+
expect(toolParts[0]).toMatchObject({
|
|
215
|
+
type: "tool-calculator",
|
|
216
|
+
toolCallId: "call1",
|
|
217
|
+
state: "output-available",
|
|
218
|
+
input: { operation: "add", a: 2, b: 3 },
|
|
219
|
+
output: { result: 5 },
|
|
220
|
+
});
|
|
207
221
|
|
|
208
|
-
|
|
209
|
-
|
|
222
|
+
// Should expand back to multiple message docs
|
|
223
|
+
expect(backToMessageDocs.length).toBeGreaterThanOrEqual(1);
|
|
210
224
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
225
|
+
// Check that tool information is preserved
|
|
226
|
+
const toolMessages = backToMessageDocs.filter((msg) => msg.tool);
|
|
227
|
+
expect(toolMessages.length).toBeGreaterThan(0);
|
|
228
|
+
expect(toolMessages[0].stepOrder).toBe(1);
|
|
229
|
+
expect(toolMessages[1].stepOrder).toBe(2);
|
|
230
|
+
}
|
|
216
231
|
});
|
|
217
232
|
|
|
218
233
|
it("preserves file attachments in user messages", () => {
|
|
@@ -237,7 +252,9 @@ describe("fromUIMessages round-trip tests", () => {
|
|
|
237
252
|
];
|
|
238
253
|
|
|
239
254
|
const uiMessages = toUIMessages(originalMessages);
|
|
240
|
-
const backToMessageDocs = fromUIMessages(
|
|
255
|
+
const backToMessageDocs = fromUIMessages(uiMessages, {
|
|
256
|
+
threadId: "thread1",
|
|
257
|
+
});
|
|
241
258
|
|
|
242
259
|
expect(uiMessages).toHaveLength(1);
|
|
243
260
|
expect(uiMessages[0].role).toBe("user");
|
|
@@ -298,7 +315,9 @@ describe("fromUIMessages round-trip tests", () => {
|
|
|
298
315
|
];
|
|
299
316
|
|
|
300
317
|
const uiMessages = toUIMessages(originalMessages);
|
|
301
|
-
const backToMessageDocs = fromUIMessages(
|
|
318
|
+
const backToMessageDocs = fromUIMessages(uiMessages, {
|
|
319
|
+
threadId: "thread1",
|
|
320
|
+
});
|
|
302
321
|
|
|
303
322
|
expect(uiMessages).toHaveLength(1);
|
|
304
323
|
|
|
@@ -337,7 +356,9 @@ describe("fromUIMessages round-trip tests", () => {
|
|
|
337
356
|
];
|
|
338
357
|
|
|
339
358
|
const uiMessages = toUIMessages(originalMessages);
|
|
340
|
-
const backToMessageDocs = fromUIMessages(
|
|
359
|
+
const backToMessageDocs = fromUIMessages(uiMessages, {
|
|
360
|
+
threadId: "thread1",
|
|
361
|
+
});
|
|
341
362
|
|
|
342
363
|
expect(uiMessages).toHaveLength(1);
|
|
343
364
|
expect(uiMessages[0].metadata).toEqual(testMetadata);
|
|
@@ -360,7 +381,9 @@ describe("fromUIMessages round-trip tests", () => {
|
|
|
360
381
|
];
|
|
361
382
|
|
|
362
383
|
const uiMessages = toUIMessages(originalMessages);
|
|
363
|
-
const backToMessageDocs = fromUIMessages(
|
|
384
|
+
const backToMessageDocs = fromUIMessages(uiMessages, {
|
|
385
|
+
threadId: "thread1",
|
|
386
|
+
});
|
|
364
387
|
|
|
365
388
|
expect(uiMessages).toHaveLength(1);
|
|
366
389
|
expect(uiMessages[0].status).toBe("streaming");
|
|
@@ -374,7 +397,7 @@ describe("fromUIMessages round-trip tests", () => {
|
|
|
374
397
|
describe("fromUIMessages functionality tests", () => {
|
|
375
398
|
it("handles empty messages array", () => {
|
|
376
399
|
const uiMessages: UIMessage[] = [];
|
|
377
|
-
const result = fromUIMessages("thread1"
|
|
400
|
+
const result = fromUIMessages(uiMessages, { threadId: "thread1" });
|
|
378
401
|
expect(result).toHaveLength(0);
|
|
379
402
|
});
|
|
380
403
|
|
|
@@ -391,7 +414,9 @@ describe("fromUIMessages functionality tests", () => {
|
|
|
391
414
|
parts: [{ type: "text", text: "Hello" }],
|
|
392
415
|
};
|
|
393
416
|
|
|
394
|
-
const result = fromUIMessages(
|
|
417
|
+
const result = fromUIMessages([uiMessage], {
|
|
418
|
+
threadId: "custom-thread-id",
|
|
419
|
+
});
|
|
395
420
|
expect(result).toHaveLength(1);
|
|
396
421
|
expect(result[0].threadId).toBe("custom-thread-id");
|
|
397
422
|
});
|
|
@@ -417,7 +442,7 @@ describe("fromUIMessages functionality tests", () => {
|
|
|
417
442
|
],
|
|
418
443
|
};
|
|
419
444
|
|
|
420
|
-
const result = fromUIMessages("thread1"
|
|
445
|
+
const result = fromUIMessages([toolUIMessage], { threadId: "thread1" });
|
|
421
446
|
expect(result.length).toBeGreaterThan(0);
|
|
422
447
|
|
|
423
448
|
// Should have tool messages
|
|
@@ -446,7 +471,7 @@ describe("fromUIMessages functionality tests", () => {
|
|
|
446
471
|
],
|
|
447
472
|
};
|
|
448
473
|
|
|
449
|
-
const result = fromUIMessages("thread1"
|
|
474
|
+
const result = fromUIMessages([toolUIMessage], { threadId: "thread1" });
|
|
450
475
|
expect(result.length).toBeGreaterThan(0);
|
|
451
476
|
|
|
452
477
|
// Should have tool messages
|
package/src/react/index.ts
CHANGED
|
@@ -1,259 +1,17 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { omit, type ErrorMessage } from "convex-helpers";
|
|
3
|
-
import {
|
|
4
|
-
type PaginatedQueryArgs,
|
|
5
|
-
type UsePaginatedQueryResult,
|
|
6
|
-
useQuery,
|
|
7
|
-
} from "convex/react";
|
|
8
|
-
import { usePaginatedQuery } from "convex-helpers/react";
|
|
9
|
-
import type { FunctionArgs } from "convex/server";
|
|
10
2
|
import { useMemo, useState } from "react";
|
|
11
|
-
|
|
12
|
-
import type { SyncStreamsReturnValue } from "../client/types.js";
|
|
13
|
-
import type { StreamArgs } from "../validators.js";
|
|
14
|
-
import type { UIMessage } from "./toUIMessages.js";
|
|
15
|
-
import { toUIMessages } from "./toUIMessages.js";
|
|
16
|
-
import { mergeDeltas } from "./deltas.js";
|
|
17
|
-
import type {
|
|
18
|
-
ThreadQuery,
|
|
19
|
-
ThreadStreamQuery,
|
|
20
|
-
ThreadMessagesArgs,
|
|
21
|
-
ThreadMessagesResult,
|
|
22
|
-
} from "./types.js";
|
|
3
|
+
export { toUIMessages, type UIMessage } from "../UIMessages.js";
|
|
23
4
|
|
|
24
5
|
export { optimisticallySendMessage } from "./optimisticallySendMessage.js";
|
|
25
6
|
export { useSmoothText } from "./useSmoothText.js";
|
|
26
7
|
export { SmoothText } from "./SmoothText.js";
|
|
27
|
-
export {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*
|
|
35
|
-
* The query must take as arguments `{ threadId, paginationOpts }` and return a
|
|
36
|
-
* pagination result of objects that extend `MessageDoc`.
|
|
37
|
-
*
|
|
38
|
-
* For streaming, it should look like this:
|
|
39
|
-
* ```ts
|
|
40
|
-
* export const listThreadMessages = query({
|
|
41
|
-
* args: {
|
|
42
|
-
* threadId: v.string(),
|
|
43
|
-
* paginationOpts: paginationOptsValidator,
|
|
44
|
-
* streamArgs: vStreamArgs,
|
|
45
|
-
* ... other arguments you want
|
|
46
|
-
* },
|
|
47
|
-
* handler: async (ctx, { threadId, paginationOpts, streamArgs }) => {
|
|
48
|
-
* // await authorizeThreadAccess(ctx, threadId);
|
|
49
|
-
* const paginated = await agent.listMessages(ctx, { threadId, paginationOpts });
|
|
50
|
-
* const streams = await agent.syncStreams(ctx, { threadId, streamArgs });
|
|
51
|
-
* // Here you could filter out / modify the documents & stream deltas.
|
|
52
|
-
* return { ...paginated, streams };
|
|
53
|
-
* },
|
|
54
|
-
* });
|
|
55
|
-
* ```
|
|
56
|
-
*
|
|
57
|
-
* Then the hook can be used like this:
|
|
58
|
-
* ```ts
|
|
59
|
-
* const messages = useThreadMessages(
|
|
60
|
-
* api.myModule.listThreadMessages,
|
|
61
|
-
* { threadId },
|
|
62
|
-
* { initialNumItems: 10, stream: true }
|
|
63
|
-
* );
|
|
64
|
-
* ```
|
|
65
|
-
*
|
|
66
|
-
* @param query The query to use to fetch messages.
|
|
67
|
-
* It must take as arguments `{ threadId, paginationOpts }` and return a
|
|
68
|
-
* pagination result of objects that extend `MessageDoc`.
|
|
69
|
-
* To support streaming, it must also take in `streamArgs: vStreamArgs` and
|
|
70
|
-
* return a `streams` object returned from `agent.syncStreams`.
|
|
71
|
-
* @param args The arguments to pass to the query other than `paginationOpts`
|
|
72
|
-
* and `streamArgs`. So `{ threadId }` at minimum, plus any other arguments that
|
|
73
|
-
* you want to pass to the query.
|
|
74
|
-
* @param options The options for the query. Similar to usePaginatedQuery.
|
|
75
|
-
* To enable streaming, pass `stream: true`.
|
|
76
|
-
* @returns The messages. If stream is true, it will return a list of messages
|
|
77
|
-
* that includes both full messages and streaming messages.
|
|
78
|
-
*/
|
|
79
|
-
export function useThreadMessages<
|
|
80
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
-
Query extends ThreadQuery<any, any>,
|
|
82
|
-
>(
|
|
83
|
-
query: Query,
|
|
84
|
-
args: ThreadMessagesArgs<Query> | "skip",
|
|
85
|
-
options: {
|
|
86
|
-
initialNumItems: number;
|
|
87
|
-
stream?: Query extends ThreadStreamQuery
|
|
88
|
-
? boolean
|
|
89
|
-
: ErrorMessage<"To enable streaming, your query must take in streamArgs: vStreamArgs and return a streams object returned from agent.syncStreams. See docs.">;
|
|
90
|
-
},
|
|
91
|
-
): UsePaginatedQueryResult<
|
|
92
|
-
ThreadMessagesResult<Query> & { streaming?: boolean }
|
|
93
|
-
> {
|
|
94
|
-
// These are full messages
|
|
95
|
-
const paginated = usePaginatedQuery(
|
|
96
|
-
query,
|
|
97
|
-
args as PaginatedQueryArgs<Query> | "skip",
|
|
98
|
-
{ initialNumItems: options.initialNumItems },
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
let startOrder = paginated.results.at(-1)?.order ?? 0;
|
|
102
|
-
for (let i = paginated.results.length - 1; i >= 0; i--) {
|
|
103
|
-
const m = paginated.results[i];
|
|
104
|
-
if (!m.streaming && m.status === "pending") {
|
|
105
|
-
// round down to the nearest 10 for some cache benefits
|
|
106
|
-
startOrder = m.order - (m.order % 10);
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
// These are streaming messages that will not include full messages.
|
|
111
|
-
const streamMessages = useStreamingThreadMessages(
|
|
112
|
-
query as ThreadStreamQuery<
|
|
113
|
-
ThreadMessagesArgs<Query>,
|
|
114
|
-
ThreadMessagesResult<Query>
|
|
115
|
-
>,
|
|
116
|
-
!options.stream ||
|
|
117
|
-
args === "skip" ||
|
|
118
|
-
paginated.status === "LoadingFirstPage"
|
|
119
|
-
? "skip"
|
|
120
|
-
: { ...args, startOrder },
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
const merged = useMemo(() => {
|
|
124
|
-
const streamListMessages =
|
|
125
|
-
streamMessages?.map((m) => ({
|
|
126
|
-
...m,
|
|
127
|
-
streaming: !m.status || m.status === "pending",
|
|
128
|
-
})) ?? [];
|
|
129
|
-
return {
|
|
130
|
-
...paginated,
|
|
131
|
-
results: paginated.results
|
|
132
|
-
.map((m) => ({ ...m, streaming: false }))
|
|
133
|
-
// Note: this is intentionally after paginated results.
|
|
134
|
-
.concat(streamListMessages)
|
|
135
|
-
.sort((a, b) =>
|
|
136
|
-
a.order === b.order ? a.stepOrder - b.stepOrder : a.order - b.order,
|
|
137
|
-
)
|
|
138
|
-
.reduce(
|
|
139
|
-
(msgs, msg) => {
|
|
140
|
-
const last = msgs.at(-1);
|
|
141
|
-
if (!last) {
|
|
142
|
-
return [msg];
|
|
143
|
-
}
|
|
144
|
-
if (last.order !== msg.order || last.stepOrder !== msg.stepOrder) {
|
|
145
|
-
return [...msgs, msg];
|
|
146
|
-
}
|
|
147
|
-
if (
|
|
148
|
-
last.status === "pending" &&
|
|
149
|
-
(msg.streaming || msg.status !== "pending")
|
|
150
|
-
) {
|
|
151
|
-
// Let's prefer a streaming or finalized message over a pending
|
|
152
|
-
// one.
|
|
153
|
-
return [...msgs.slice(0, -1), msg];
|
|
154
|
-
}
|
|
155
|
-
// skip the new one if the previous one (listed) was finalized
|
|
156
|
-
return msgs;
|
|
157
|
-
},
|
|
158
|
-
[] as (ThreadMessagesResult<Query> & { streaming: boolean })[],
|
|
159
|
-
),
|
|
160
|
-
};
|
|
161
|
-
}, [paginated, streamMessages]);
|
|
162
|
-
|
|
163
|
-
return merged as ThreadMessagesResult<Query>;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* A hook that fetches streaming messages from a thread.
|
|
168
|
-
* This ONLY returns streaming messages. To get both, use `useThreadMessages`.
|
|
169
|
-
*
|
|
170
|
-
* @param query The query to use to fetch messages.
|
|
171
|
-
* It must take as arguments `{ threadId, paginationOpts, streamArgs }` and
|
|
172
|
-
* return a `streams` object returned from `agent.syncStreams`.
|
|
173
|
-
* @param args The arguments to pass to the query other than `paginationOpts`
|
|
174
|
-
* and `streamArgs`. So `{ threadId }` at minimum, plus any other arguments that
|
|
175
|
-
* you want to pass to the query.
|
|
176
|
-
* @returns The streaming messages.
|
|
177
|
-
*/
|
|
178
|
-
export function useStreamingThreadMessages<
|
|
179
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
180
|
-
Query extends ThreadStreamQuery<any, any>,
|
|
181
|
-
>(
|
|
182
|
-
query: Query,
|
|
183
|
-
args: (ThreadMessagesArgs<Query> & { startOrder?: number }) | "skip",
|
|
184
|
-
): Array<ThreadMessagesResult<Query>> | undefined {
|
|
185
|
-
// Invariant: streamMessages[streamId] is comprised of all deltas up to the
|
|
186
|
-
// cursor. There can be multiple messages in the same stream, e.g. for tool
|
|
187
|
-
// calls.
|
|
188
|
-
const [streams, setStreams] = useState<
|
|
189
|
-
Array<{ streamId: string; cursor: number; messages: MessageDoc[] }>
|
|
190
|
-
>([]);
|
|
191
|
-
const queryArgs = args === "skip" ? args : omit(args, ["startOrder"]);
|
|
192
|
-
// Get all the active streams
|
|
193
|
-
const streamList = useQuery(
|
|
194
|
-
query,
|
|
195
|
-
queryArgs === "skip"
|
|
196
|
-
? queryArgs
|
|
197
|
-
: ({
|
|
198
|
-
...queryArgs,
|
|
199
|
-
paginationOpts: { cursor: null, numItems: 0 },
|
|
200
|
-
streamArgs: {
|
|
201
|
-
kind: "list",
|
|
202
|
-
startOrder: queryArgs.startOrder ?? 0,
|
|
203
|
-
} as StreamArgs,
|
|
204
|
-
} as FunctionArgs<Query>),
|
|
205
|
-
) as
|
|
206
|
-
| { streams: Extract<SyncStreamsReturnValue, { kind: "list" }> }
|
|
207
|
-
| undefined;
|
|
208
|
-
// Get the cursors for all the active streams
|
|
209
|
-
const cursors = useMemo(() => {
|
|
210
|
-
if (!streamList?.streams) return [];
|
|
211
|
-
if (streamList.streams.kind !== "list") {
|
|
212
|
-
throw new Error("Expected list streams");
|
|
213
|
-
}
|
|
214
|
-
return streamList.streams.messages.map(({ streamId }) => {
|
|
215
|
-
const stream = streams.find((s) => s.streamId === streamId);
|
|
216
|
-
const cursor = stream?.cursor ?? 0;
|
|
217
|
-
return { streamId, cursor };
|
|
218
|
-
});
|
|
219
|
-
}, [streamList, streams]);
|
|
220
|
-
// Get the deltas for all the active streams, if any.
|
|
221
|
-
const cursorQuery = useQuery(
|
|
222
|
-
query,
|
|
223
|
-
queryArgs === "skip" || !streamList
|
|
224
|
-
? ("skip" as const)
|
|
225
|
-
: ({
|
|
226
|
-
...queryArgs,
|
|
227
|
-
paginationOpts: { cursor: null, numItems: 0 },
|
|
228
|
-
streamArgs: { kind: "deltas", cursors } as StreamArgs,
|
|
229
|
-
} as FunctionArgs<Query>),
|
|
230
|
-
) as
|
|
231
|
-
| { streams: Extract<SyncStreamsReturnValue, { kind: "deltas" }> }
|
|
232
|
-
| undefined;
|
|
233
|
-
// Merge any deltas into the streamChunks, keeping it unmodified if unchanged.
|
|
234
|
-
const threadId = args === "skip" ? undefined : args.threadId;
|
|
235
|
-
const [messages, newStreams, changed] = useMemo(() => {
|
|
236
|
-
if (!threadId) return [undefined, [], false];
|
|
237
|
-
if (!streamList) return [undefined, [], false];
|
|
238
|
-
if (cursorQuery && cursorQuery.streams?.kind !== "deltas") {
|
|
239
|
-
throw new Error("Expected deltas streams");
|
|
240
|
-
}
|
|
241
|
-
return mergeDeltas(
|
|
242
|
-
threadId,
|
|
243
|
-
streamList.streams.messages,
|
|
244
|
-
streams,
|
|
245
|
-
cursorQuery?.streams?.deltas ?? [],
|
|
246
|
-
);
|
|
247
|
-
}, [threadId, cursorQuery, streams, streamList]);
|
|
248
|
-
// Now assemble the chunks into messages
|
|
249
|
-
if (!threadId) {
|
|
250
|
-
return undefined;
|
|
251
|
-
}
|
|
252
|
-
if (changed) {
|
|
253
|
-
setStreams(newStreams);
|
|
254
|
-
}
|
|
255
|
-
return messages as ThreadMessagesResult<Query>[] | undefined;
|
|
256
|
-
}
|
|
8
|
+
export {
|
|
9
|
+
type ThreadMessagesQuery,
|
|
10
|
+
useThreadMessages,
|
|
11
|
+
useStreamingThreadMessages,
|
|
12
|
+
} from "./useThreadMessages.js";
|
|
13
|
+
export { type UIMessagesQuery, useUIMessages } from "./useUIMessages.js";
|
|
14
|
+
export { useStreamingUIMessages } from "./useStreamingUIMessages.js";
|
|
257
15
|
|
|
258
16
|
/**
|
|
259
17
|
* @deprecated use useThreadMessages or useStreamingThreadMessages instead
|
|
@@ -1,10 +1,52 @@
|
|
|
1
1
|
import { insertAtTop } from "convex/react";
|
|
2
|
-
import type { MessageDoc } from "../
|
|
2
|
+
import type { MessageDoc, StreamArgs } from "../validators.js";
|
|
3
3
|
import type { OptimisticLocalStore } from "convex/browser";
|
|
4
|
-
import type {
|
|
4
|
+
import type { UIMessage } from "../UIMessages.js";
|
|
5
|
+
import type {
|
|
6
|
+
FunctionReference,
|
|
7
|
+
PaginationOptions,
|
|
8
|
+
PaginationResult,
|
|
9
|
+
} from "convex/server";
|
|
10
|
+
import type { SyncStreamsReturnValue } from "@convex-dev/agent";
|
|
5
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Adds a sent message to the end of a list of messages, so it shows up until
|
|
14
|
+
* the message is saved on the server and arrives in the query.
|
|
15
|
+
* It generates a message with fields that match both MessageDoc and UIMessage,
|
|
16
|
+
* for convenience. It will not include any other fields you might have in your
|
|
17
|
+
* regular query, however.
|
|
18
|
+
*
|
|
19
|
+
* @param query The query used to fetch messages, typically with
|
|
20
|
+
* useThreadMessages or useUIMessages.
|
|
21
|
+
* @returns A function that can be used to optimistically send a message.
|
|
22
|
+
* If your mutation takes different arguments than { threadId, prompt }, you can
|
|
23
|
+
* use it as a helper function in your optimistic update:
|
|
24
|
+
* ```ts
|
|
25
|
+
* const sendMessage = useMutation(
|
|
26
|
+
* api.chatStreaming.streamStoryAsynchronously,
|
|
27
|
+
* ).withOptimisticUpdate(
|
|
28
|
+
* (store, args) => {
|
|
29
|
+
* optimisticallySendMessage(api.chatStreaming.listThreadMessages)(store, {
|
|
30
|
+
* threadId:
|
|
31
|
+
* prompt: whatever you would have passed to the mutation,
|
|
32
|
+
* })
|
|
33
|
+
* }
|
|
34
|
+
* );
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
6
37
|
export function optimisticallySendMessage(
|
|
7
|
-
query:
|
|
38
|
+
query: FunctionReference<
|
|
39
|
+
"query",
|
|
40
|
+
"public",
|
|
41
|
+
{
|
|
42
|
+
threadId: string;
|
|
43
|
+
paginationOpts: PaginationOptions;
|
|
44
|
+
streamArgs?: StreamArgs;
|
|
45
|
+
},
|
|
46
|
+
PaginationResult<MessageDoc | UIMessage> & {
|
|
47
|
+
streams?: SyncStreamsReturnValue;
|
|
48
|
+
}
|
|
49
|
+
>,
|
|
8
50
|
): (
|
|
9
51
|
store: OptimisticLocalStore,
|
|
10
52
|
args: { threadId: string; prompt: string },
|
|
@@ -20,19 +62,26 @@ export function optimisticallySendMessage(
|
|
|
20
62
|
}
|
|
21
63
|
}
|
|
22
64
|
const order = maxOrder + 1;
|
|
65
|
+
const stepOrder = 0;
|
|
66
|
+
const id = randomUUID();
|
|
67
|
+
const { prompt, ...rest } = args;
|
|
23
68
|
insertAtTop({
|
|
24
69
|
paginatedQuery: query,
|
|
25
70
|
argsToMatch: { threadId: args.threadId, streamArgs: undefined },
|
|
26
71
|
item: {
|
|
72
|
+
...rest,
|
|
27
73
|
_creationTime: Date.now(),
|
|
28
|
-
_id:
|
|
74
|
+
_id: id,
|
|
75
|
+
id,
|
|
76
|
+
key: `${args.threadId}-${order}-${stepOrder}`,
|
|
29
77
|
order,
|
|
30
|
-
stepOrder
|
|
78
|
+
stepOrder,
|
|
31
79
|
status: "pending",
|
|
32
|
-
threadId: args.threadId,
|
|
33
80
|
tool: false,
|
|
34
|
-
message: { role: "user", content:
|
|
35
|
-
text:
|
|
81
|
+
message: { role: "user", content: prompt },
|
|
82
|
+
parts: [{ type: "text", text: prompt }],
|
|
83
|
+
role: "user",
|
|
84
|
+
text: prompt,
|
|
36
85
|
},
|
|
37
86
|
localQueryStore: store,
|
|
38
87
|
});
|
package/src/react/types.ts
CHANGED
|
@@ -1,52 +1,19 @@
|
|
|
1
1
|
import type { BetterOmit, Expand } from "convex-helpers";
|
|
2
|
-
import type {
|
|
3
|
-
FunctionArgs,
|
|
4
|
-
FunctionReference,
|
|
5
|
-
PaginationOptions,
|
|
6
|
-
PaginationResult,
|
|
7
|
-
} from "convex/server";
|
|
8
|
-
import type { MessageDoc } from "../client/index.js";
|
|
2
|
+
import type { FunctionArgs, FunctionReference } from "convex/server";
|
|
9
3
|
import type { SyncStreamsReturnValue } from "../client/types.js";
|
|
10
4
|
import type { StreamArgs } from "../validators.js";
|
|
11
5
|
|
|
12
|
-
export type
|
|
13
|
-
Args = unknown,
|
|
14
|
-
M extends MessageDoc = MessageDoc,
|
|
15
|
-
> = FunctionReference<
|
|
6
|
+
export type StreamQuery<Args = Record<string, unknown>> = FunctionReference<
|
|
16
7
|
"query",
|
|
17
8
|
"public",
|
|
18
9
|
{
|
|
19
10
|
threadId: string;
|
|
20
|
-
paginationOpts: PaginationOptions;
|
|
21
|
-
// TODO: will this allow passing a function that doesn't have this param?
|
|
22
|
-
/**
|
|
23
|
-
* If { stream: true } is passed, it will also query for stream deltas.
|
|
24
|
-
* In order for this to work, the query must take as an argument streamArgs.
|
|
25
|
-
*/
|
|
26
|
-
streamArgs?: StreamArgs;
|
|
27
|
-
} & Args,
|
|
28
|
-
PaginationResult<M> & { streams?: SyncStreamsReturnValue }
|
|
29
|
-
>;
|
|
30
|
-
|
|
31
|
-
export type ThreadStreamQuery<
|
|
32
|
-
Args = Record<string, unknown>,
|
|
33
|
-
M extends MessageDoc = MessageDoc,
|
|
34
|
-
> = FunctionReference<
|
|
35
|
-
"query",
|
|
36
|
-
"public",
|
|
37
|
-
{
|
|
38
|
-
threadId: string;
|
|
39
|
-
paginationOpts: PaginationOptions;
|
|
40
11
|
streamArgs?: StreamArgs; // required for stream query
|
|
41
12
|
} & Args,
|
|
42
|
-
|
|
13
|
+
{ streams: SyncStreamsReturnValue }
|
|
43
14
|
>;
|
|
44
15
|
|
|
45
|
-
export type
|
|
46
|
-
Query extends
|
|
47
|
-
? Expand<BetterOmit<FunctionArgs<Query>, "
|
|
16
|
+
export type StreamMessagesArgs<Query extends StreamQuery<unknown>> =
|
|
17
|
+
Query extends StreamQuery<unknown>
|
|
18
|
+
? Expand<BetterOmit<FunctionArgs<Query>, "streamArgs">>
|
|
48
19
|
: never;
|
|
49
|
-
|
|
50
|
-
export type ThreadMessagesResult<
|
|
51
|
-
Query extends ThreadQuery<unknown, MessageDoc>,
|
|
52
|
-
> = Query extends ThreadQuery<unknown, infer M> ? M : never;
|