@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
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { omit } from "convex-helpers";
|
|
3
|
+
import { useQuery } from "convex/react";
|
|
4
|
+
import type { FunctionArgs } from "convex/server";
|
|
5
|
+
import { useMemo, useState, useEffect, useRef } from "react";
|
|
6
|
+
import {
|
|
7
|
+
readUIMessageStream,
|
|
8
|
+
type TextUIPart,
|
|
9
|
+
type UIDataTypes,
|
|
10
|
+
type UIMessageChunk,
|
|
11
|
+
type UITools,
|
|
12
|
+
} from "ai";
|
|
13
|
+
import type { SyncStreamsReturnValue } from "../client/types.js";
|
|
14
|
+
import type { StreamArgs } from "../validators.js";
|
|
15
|
+
import type { StreamQuery, StreamMessagesArgs } from "./types.js";
|
|
16
|
+
import { type UIMessage } from "../UIMessages.js";
|
|
17
|
+
import {
|
|
18
|
+
blankUIMessage,
|
|
19
|
+
getParts,
|
|
20
|
+
deriveUIMessagesFromTextStreamParts,
|
|
21
|
+
statusFromStreamStatus,
|
|
22
|
+
} from "../deltas.js";
|
|
23
|
+
import { sorted } from "../shared.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A hook that fetches streaming messages from a thread and converts them to UIMessages
|
|
27
|
+
* using AI SDK's readUIMessageStream.
|
|
28
|
+
* This ONLY returns streaming UIMessages. To get both full and streaming messages,
|
|
29
|
+
* use `useUIMessages`.
|
|
30
|
+
*
|
|
31
|
+
* @param query The query to use to fetch messages.
|
|
32
|
+
* It must take as arguments `{ threadId, paginationOpts, streamArgs }` and
|
|
33
|
+
* return a `streams` object returned from `agent.syncStreams`.
|
|
34
|
+
* @param args The arguments to pass to the query other than `paginationOpts`
|
|
35
|
+
* and `streamArgs`. So `{ threadId }` at minimum, plus any other arguments that
|
|
36
|
+
* you want to pass to the query.
|
|
37
|
+
* @returns The streaming UIMessages.
|
|
38
|
+
*/
|
|
39
|
+
export function useStreamingUIMessages<
|
|
40
|
+
METADATA = unknown,
|
|
41
|
+
DATA_PARTS extends UIDataTypes = UIDataTypes,
|
|
42
|
+
TOOLS extends UITools = UITools,
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
+
Query extends StreamQuery<any> = StreamQuery<object>,
|
|
45
|
+
>(
|
|
46
|
+
query: Query,
|
|
47
|
+
args: StreamMessagesArgs<Query> | "skip",
|
|
48
|
+
options?: {
|
|
49
|
+
startOrder?: number;
|
|
50
|
+
skipStreamIds?: string[];
|
|
51
|
+
},
|
|
52
|
+
// TODO: make generic on metadata, etc.
|
|
53
|
+
): UIMessage<METADATA, DATA_PARTS, TOOLS>[] | undefined {
|
|
54
|
+
const [uiMessages, setUIMessages] = useState<
|
|
55
|
+
Record<string, UIMessage<METADATA, DATA_PARTS, TOOLS>>
|
|
56
|
+
>({});
|
|
57
|
+
const [streamCursors, setStreamCursors] = useState<Record<string, number>>(
|
|
58
|
+
{},
|
|
59
|
+
);
|
|
60
|
+
const uiMessageStreamControllers = useRef<
|
|
61
|
+
Map<string, ReadableStreamDefaultController<UIMessageChunk>>
|
|
62
|
+
>(new Map());
|
|
63
|
+
|
|
64
|
+
// Get all the active streams
|
|
65
|
+
const streamList = useQuery(
|
|
66
|
+
query,
|
|
67
|
+
args === "skip"
|
|
68
|
+
? args
|
|
69
|
+
: ({
|
|
70
|
+
...args,
|
|
71
|
+
streamArgs: {
|
|
72
|
+
kind: "list",
|
|
73
|
+
startOrder: options?.startOrder ?? 0,
|
|
74
|
+
} as StreamArgs,
|
|
75
|
+
} as FunctionArgs<Query>),
|
|
76
|
+
) as
|
|
77
|
+
| { streams: Extract<SyncStreamsReturnValue, { kind: "list" }> }
|
|
78
|
+
| undefined;
|
|
79
|
+
|
|
80
|
+
// Get the cursors for all the active streams
|
|
81
|
+
const cursors = useMemo(() => {
|
|
82
|
+
if (!streamList?.streams) return [];
|
|
83
|
+
if (streamList.streams.kind !== "list") {
|
|
84
|
+
throw new Error("Expected list streams");
|
|
85
|
+
}
|
|
86
|
+
return streamList.streams.messages
|
|
87
|
+
.filter(({ streamId }) => !options?.skipStreamIds?.includes(streamId))
|
|
88
|
+
.map(({ streamId }) => {
|
|
89
|
+
const cursor = streamCursors[streamId] ?? 0;
|
|
90
|
+
return { streamId, cursor };
|
|
91
|
+
});
|
|
92
|
+
}, [streamList, streamCursors, options?.skipStreamIds]);
|
|
93
|
+
|
|
94
|
+
// Get the deltas for all the active streams, if any.
|
|
95
|
+
const cursorQuery = useQuery(
|
|
96
|
+
query,
|
|
97
|
+
args === "skip" || !streamList
|
|
98
|
+
? ("skip" as const)
|
|
99
|
+
: ({
|
|
100
|
+
...args,
|
|
101
|
+
paginationOpts: { cursor: null, numItems: 0 },
|
|
102
|
+
streamArgs: { kind: "deltas", cursors } as StreamArgs,
|
|
103
|
+
} as FunctionArgs<Query>),
|
|
104
|
+
) as
|
|
105
|
+
| { streams: Extract<SyncStreamsReturnValue, { kind: "deltas" }> }
|
|
106
|
+
| undefined;
|
|
107
|
+
|
|
108
|
+
const threadId = args === "skip" ? undefined : args.threadId;
|
|
109
|
+
|
|
110
|
+
// Process new deltas and convert to UIMessageChunks, then use readUIMessageStream
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!cursorQuery?.streams?.deltas || !threadId) return;
|
|
113
|
+
|
|
114
|
+
const deltasByStream = new Map<string, typeof cursorQuery.streams.deltas>();
|
|
115
|
+
|
|
116
|
+
// Group deltas by streamId
|
|
117
|
+
for (const delta of cursorQuery.streams.deltas) {
|
|
118
|
+
if (!deltasByStream.has(delta.streamId)) {
|
|
119
|
+
deltasByStream.set(delta.streamId, []);
|
|
120
|
+
}
|
|
121
|
+
deltasByStream.get(delta.streamId)!.push(delta);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Process each stream's deltas
|
|
125
|
+
for (const [streamId, deltas] of deltasByStream.entries()) {
|
|
126
|
+
const streamMessage = streamList?.streams?.messages.find(
|
|
127
|
+
(m) => m.streamId === streamId,
|
|
128
|
+
);
|
|
129
|
+
if (!streamMessage) continue;
|
|
130
|
+
if (streamMessage.format === "UIMessageChunk") {
|
|
131
|
+
setStreamCursors((streamCursors) => {
|
|
132
|
+
const currentCursor = streamCursors[streamId] ?? 0;
|
|
133
|
+
|
|
134
|
+
const { parts, cursor } = getParts<UIMessageChunk>(
|
|
135
|
+
deltas,
|
|
136
|
+
currentCursor,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const newStreamCursors =
|
|
140
|
+
cursor === currentCursor
|
|
141
|
+
? streamCursors
|
|
142
|
+
: { ...streamCursors, [streamId]: cursor };
|
|
143
|
+
|
|
144
|
+
if (parts.length === 0) return newStreamCursors;
|
|
145
|
+
|
|
146
|
+
// Get existing message for this stream as starting point
|
|
147
|
+
const existingStream =
|
|
148
|
+
uiMessageStreamControllers.current.get(streamId);
|
|
149
|
+
if (existingStream) {
|
|
150
|
+
for (const part of parts) {
|
|
151
|
+
existingStream.enqueue(part);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
const stream = new ReadableStream<UIMessageChunk>({
|
|
155
|
+
start(controller) {
|
|
156
|
+
uiMessageStreamControllers.current.set(streamId, controller);
|
|
157
|
+
for (const part of parts) {
|
|
158
|
+
controller.enqueue(part);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
const initialMessage = blankUIMessage(streamMessage, threadId);
|
|
163
|
+
setUIMessages((prev) => ({
|
|
164
|
+
...prev,
|
|
165
|
+
[streamId]: initialMessage as UIMessage<
|
|
166
|
+
METADATA,
|
|
167
|
+
DATA_PARTS,
|
|
168
|
+
TOOLS
|
|
169
|
+
>,
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
const messageStream = readUIMessageStream({
|
|
173
|
+
message: initialMessage,
|
|
174
|
+
stream,
|
|
175
|
+
onError: (error) => {
|
|
176
|
+
setUIMessages((prev) => ({
|
|
177
|
+
...prev,
|
|
178
|
+
[streamId]: {
|
|
179
|
+
...prev[streamId],
|
|
180
|
+
status: "failed",
|
|
181
|
+
},
|
|
182
|
+
}));
|
|
183
|
+
console.error(`Error in stream ${streamId}:`, error);
|
|
184
|
+
},
|
|
185
|
+
terminateOnError: false,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Process the async iterator
|
|
189
|
+
void (async () => {
|
|
190
|
+
for await (const message of messageStream) {
|
|
191
|
+
message.text = message.parts
|
|
192
|
+
.filter((p) => p.type === "text")
|
|
193
|
+
.map((p) => p.text)
|
|
194
|
+
.join("");
|
|
195
|
+
setUIMessages((prev) =>
|
|
196
|
+
// If we don't have a ui message assume we've aborted.
|
|
197
|
+
prev[streamId]
|
|
198
|
+
? {
|
|
199
|
+
...prev,
|
|
200
|
+
[streamId]: message as UIMessage<
|
|
201
|
+
METADATA,
|
|
202
|
+
DATA_PARTS,
|
|
203
|
+
TOOLS
|
|
204
|
+
>,
|
|
205
|
+
}
|
|
206
|
+
: prev,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
})().catch((error) => {
|
|
210
|
+
console.error(`Error processing stream ${streamId}:`, error);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return newStreamCursors;
|
|
214
|
+
});
|
|
215
|
+
} else {
|
|
216
|
+
setUIMessages((uiMessages) => {
|
|
217
|
+
const existingUIMessage = uiMessages[streamId];
|
|
218
|
+
const [[uiMessage], [streamMetadata], changed] =
|
|
219
|
+
deriveUIMessagesFromTextStreamParts(
|
|
220
|
+
threadId,
|
|
221
|
+
[streamMessage],
|
|
222
|
+
existingUIMessage
|
|
223
|
+
? [
|
|
224
|
+
{
|
|
225
|
+
streamId,
|
|
226
|
+
cursor: streamCursors[streamId] ?? 0,
|
|
227
|
+
message: existingUIMessage,
|
|
228
|
+
},
|
|
229
|
+
]
|
|
230
|
+
: [],
|
|
231
|
+
deltas,
|
|
232
|
+
);
|
|
233
|
+
if (changed) {
|
|
234
|
+
setStreamCursors((prev) => ({
|
|
235
|
+
...prev,
|
|
236
|
+
[streamId]: streamMetadata.cursor,
|
|
237
|
+
}));
|
|
238
|
+
return {
|
|
239
|
+
...uiMessages,
|
|
240
|
+
[streamId]: uiMessage as UIMessage<METADATA, DATA_PARTS, TOOLS>,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return uiMessages;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}, [cursorQuery, streamCursors, threadId, streamList?.streams?.messages]);
|
|
248
|
+
|
|
249
|
+
// Clean up finished streams
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
const activeStreamIds = new Set(
|
|
252
|
+
streamList?.streams.messages.map((m) => m.streamId) ?? [],
|
|
253
|
+
);
|
|
254
|
+
setUIMessages((prev) => {
|
|
255
|
+
const toRemove = Object.keys(prev).filter(
|
|
256
|
+
(streamId) => !activeStreamIds.has(streamId),
|
|
257
|
+
);
|
|
258
|
+
return toRemove.length ? omit(prev, toRemove) : prev;
|
|
259
|
+
});
|
|
260
|
+
for (const [
|
|
261
|
+
streamId,
|
|
262
|
+
controller,
|
|
263
|
+
] of uiMessageStreamControllers.current.entries()) {
|
|
264
|
+
if (!activeStreamIds.has(streamId)) {
|
|
265
|
+
controller.close();
|
|
266
|
+
uiMessageStreamControllers.current.delete(streamId);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}, [streamList]);
|
|
270
|
+
|
|
271
|
+
return useMemo(() => {
|
|
272
|
+
if (!streamList) return undefined;
|
|
273
|
+
return sorted(
|
|
274
|
+
Array.from(
|
|
275
|
+
Object.entries(uiMessages)
|
|
276
|
+
.map(([streamId, uiMessage]) => {
|
|
277
|
+
const streamMessage = streamList?.streams.messages.find(
|
|
278
|
+
(m) => m.streamId === streamId,
|
|
279
|
+
);
|
|
280
|
+
if (!streamMessage) return undefined;
|
|
281
|
+
uiMessage.status = statusFromStreamStatus(
|
|
282
|
+
streamMessage?.status ?? "finished",
|
|
283
|
+
);
|
|
284
|
+
uiMessage.text = uiMessage.parts
|
|
285
|
+
.filter((p): p is TextUIPart => p.type === "text")
|
|
286
|
+
.map((p) => p.text)
|
|
287
|
+
.join("");
|
|
288
|
+
return uiMessage;
|
|
289
|
+
})
|
|
290
|
+
.filter((uiMessage) => uiMessage !== undefined),
|
|
291
|
+
),
|
|
292
|
+
);
|
|
293
|
+
}, [uiMessages, streamList]);
|
|
294
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
omit,
|
|
4
|
+
type BetterOmit,
|
|
5
|
+
type ErrorMessage,
|
|
6
|
+
type Expand,
|
|
7
|
+
} from "convex-helpers";
|
|
8
|
+
import { usePaginatedQuery } from "convex-helpers/react";
|
|
9
|
+
import {
|
|
10
|
+
type PaginatedQueryArgs,
|
|
11
|
+
type UsePaginatedQueryResult,
|
|
12
|
+
} from "convex/react";
|
|
13
|
+
import type {
|
|
14
|
+
FunctionArgs,
|
|
15
|
+
FunctionReference,
|
|
16
|
+
PaginationOptions,
|
|
17
|
+
PaginationResult,
|
|
18
|
+
} from "convex/server";
|
|
19
|
+
import { useMemo } from "react";
|
|
20
|
+
import type { SyncStreamsReturnValue } from "../client/types.js";
|
|
21
|
+
import { sorted } from "../shared.js";
|
|
22
|
+
import { fromUIMessages } from "../UIMessages.js";
|
|
23
|
+
import type {
|
|
24
|
+
Message,
|
|
25
|
+
MessageDoc,
|
|
26
|
+
MessageStatus,
|
|
27
|
+
StreamArgs,
|
|
28
|
+
} from "../validators.js";
|
|
29
|
+
import type { StreamMessagesArgs, StreamQuery } from "./types.js";
|
|
30
|
+
import { useStreamingUIMessages } from "./useStreamingUIMessages.js";
|
|
31
|
+
|
|
32
|
+
export type MessageDocLike = {
|
|
33
|
+
order: number;
|
|
34
|
+
stepOrder: number;
|
|
35
|
+
status: MessageStatus | "streaming";
|
|
36
|
+
message?: Message;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type ThreadMessagesQuery<
|
|
40
|
+
Args = unknown,
|
|
41
|
+
M extends MessageDocLike = MessageDocLike,
|
|
42
|
+
> = FunctionReference<
|
|
43
|
+
"query",
|
|
44
|
+
"public",
|
|
45
|
+
{
|
|
46
|
+
threadId: string;
|
|
47
|
+
paginationOpts: PaginationOptions;
|
|
48
|
+
/**
|
|
49
|
+
* If { stream: true } is passed, it will also query for stream deltas.
|
|
50
|
+
* In order for this to work, the query must take as an argument streamArgs.
|
|
51
|
+
*/
|
|
52
|
+
streamArgs?: StreamArgs;
|
|
53
|
+
} & Args,
|
|
54
|
+
PaginationResult<M> & { streams?: SyncStreamsReturnValue }
|
|
55
|
+
>;
|
|
56
|
+
|
|
57
|
+
export type ThreadMessagesArgs<
|
|
58
|
+
Query extends ThreadMessagesQuery<unknown, MessageDocLike>,
|
|
59
|
+
> =
|
|
60
|
+
Query extends ThreadMessagesQuery<unknown, MessageDocLike>
|
|
61
|
+
? Expand<BetterOmit<FunctionArgs<Query>, "paginationOpts" | "streamArgs">>
|
|
62
|
+
: never;
|
|
63
|
+
|
|
64
|
+
export type ThreadMessagesResult<
|
|
65
|
+
Query extends ThreadMessagesQuery<unknown, MessageDocLike>,
|
|
66
|
+
> = Query extends ThreadMessagesQuery<unknown, infer M> ? M : never;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A hook that fetches messages from a thread.
|
|
70
|
+
*
|
|
71
|
+
* This hook is a wrapper around `usePaginatedQuery` and `useStreamingThreadMessages`.
|
|
72
|
+
* It will fetch both full messages and streaming messages, and merge them together.
|
|
73
|
+
*
|
|
74
|
+
* The query must take as arguments `{ threadId, paginationOpts }` and return a
|
|
75
|
+
* pagination result of objects that extend `MessageDoc`.
|
|
76
|
+
*
|
|
77
|
+
* For streaming, it should look like this:
|
|
78
|
+
* ```ts
|
|
79
|
+
* export const listThreadMessages = query({
|
|
80
|
+
* args: {
|
|
81
|
+
* threadId: v.string(),
|
|
82
|
+
* paginationOpts: paginationOptsValidator,
|
|
83
|
+
* streamArgs: vStreamArgs,
|
|
84
|
+
* ... other arguments you want
|
|
85
|
+
* },
|
|
86
|
+
* handler: async (ctx, args) => {
|
|
87
|
+
* // await authorizeThreadAccess(ctx, threadId);
|
|
88
|
+
* const paginated = await listMessages(ctx, components.agent, args);
|
|
89
|
+
* const streams = await syncStreams(ctx, components.agent, args);
|
|
90
|
+
* // Here you could filter out / modify the documents & stream deltas.
|
|
91
|
+
* return { ...paginated, streams };
|
|
92
|
+
* },
|
|
93
|
+
* });
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* Then the hook can be used like this:
|
|
97
|
+
* ```ts
|
|
98
|
+
* const { results, status, loadMore } = useThreadMessages(
|
|
99
|
+
* api.myModule.listThreadMessages,
|
|
100
|
+
* { threadId },
|
|
101
|
+
* { initialNumItems: 10, stream: true }
|
|
102
|
+
* );
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @param query The query to use to fetch messages.
|
|
106
|
+
* It must take as arguments `{ threadId, paginationOpts }` and return a
|
|
107
|
+
* pagination result of objects that extend `MessageDoc`.
|
|
108
|
+
* To support streaming, it must also take in `streamArgs: vStreamArgs` and
|
|
109
|
+
* return a `streams` object returned from `agent.syncStreams`.
|
|
110
|
+
* @param args The arguments to pass to the query other than `paginationOpts`
|
|
111
|
+
* and `streamArgs`. So `{ threadId }` at minimum, plus any other arguments that
|
|
112
|
+
* you want to pass to the query.
|
|
113
|
+
* @param options The options for the query. Similar to usePaginatedQuery.
|
|
114
|
+
* To enable streaming, pass `stream: true`.
|
|
115
|
+
* @returns The messages. If stream is true, it will return a list of messages
|
|
116
|
+
* that includes both full messages and streaming messages.
|
|
117
|
+
*/
|
|
118
|
+
export function useThreadMessages<
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
120
|
+
Query extends ThreadMessagesQuery<any, any>,
|
|
121
|
+
>(
|
|
122
|
+
query: Query,
|
|
123
|
+
args: ThreadMessagesArgs<Query> | "skip",
|
|
124
|
+
options: {
|
|
125
|
+
initialNumItems: number;
|
|
126
|
+
stream?: Query extends StreamQuery
|
|
127
|
+
? boolean
|
|
128
|
+
: ErrorMessage<"To enable streaming, your query must take in streamArgs: vStreamArgs and return a streams object returned from syncStreams. See docs.">;
|
|
129
|
+
},
|
|
130
|
+
): UsePaginatedQueryResult<
|
|
131
|
+
ThreadMessagesResult<Query> & { streaming: boolean; key: string }
|
|
132
|
+
> {
|
|
133
|
+
// These are full messages
|
|
134
|
+
const paginated = usePaginatedQuery(
|
|
135
|
+
query,
|
|
136
|
+
args as PaginatedQueryArgs<Query> | "skip",
|
|
137
|
+
{ initialNumItems: options.initialNumItems },
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
let startOrder = paginated.results.at(-1)?.order ?? 0;
|
|
141
|
+
for (let i = paginated.results.length - 1; i >= 0; i--) {
|
|
142
|
+
const m = paginated.results[i];
|
|
143
|
+
if (!m.streaming && m.status === "pending") {
|
|
144
|
+
// round down to the nearest 10 for some cache benefits
|
|
145
|
+
startOrder = m.order - (m.order % 10);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// These are streaming messages that will not include full messages.
|
|
150
|
+
const streamMessages = useStreamingThreadMessages(
|
|
151
|
+
query as StreamQuery<ThreadMessagesArgs<Query>>,
|
|
152
|
+
!options.stream ||
|
|
153
|
+
args === "skip" ||
|
|
154
|
+
paginated.status === "LoadingFirstPage"
|
|
155
|
+
? "skip"
|
|
156
|
+
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
157
|
+
({ ...args, paginationOpts: { cursor: null, numItems: 0 } } as any),
|
|
158
|
+
{ startOrder },
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const merged = useMemo(() => {
|
|
162
|
+
const streamListMessages =
|
|
163
|
+
streamMessages?.map((m) => ({
|
|
164
|
+
...m,
|
|
165
|
+
streaming: !m.status || m.status === "pending",
|
|
166
|
+
})) ?? [];
|
|
167
|
+
return {
|
|
168
|
+
...paginated,
|
|
169
|
+
results: sorted(
|
|
170
|
+
paginated.results
|
|
171
|
+
.map((m) => ({ ...m, streaming: false }))
|
|
172
|
+
// Note: this is intentionally after paginated results.
|
|
173
|
+
.concat(streamListMessages),
|
|
174
|
+
).reduce(
|
|
175
|
+
(msgs, msg) => {
|
|
176
|
+
msg.key = `${msg.threadId}-${msg.order}-${msg.stepOrder}`;
|
|
177
|
+
const last = msgs.at(-1);
|
|
178
|
+
if (!last) {
|
|
179
|
+
return [msg];
|
|
180
|
+
}
|
|
181
|
+
if (last.order !== msg.order || last.stepOrder !== msg.stepOrder) {
|
|
182
|
+
return [...msgs, msg];
|
|
183
|
+
}
|
|
184
|
+
if (
|
|
185
|
+
last.status === "pending" &&
|
|
186
|
+
(msg.streaming || msg.status !== "pending")
|
|
187
|
+
) {
|
|
188
|
+
// Let's prefer a streaming or finalized message over a pending
|
|
189
|
+
// one.
|
|
190
|
+
return [...msgs.slice(0, -1), msg];
|
|
191
|
+
}
|
|
192
|
+
// skip the new one if the previous one (listed) was finalized
|
|
193
|
+
return msgs;
|
|
194
|
+
},
|
|
195
|
+
[] as (ThreadMessagesResult<Query> & { streaming: boolean })[],
|
|
196
|
+
),
|
|
197
|
+
};
|
|
198
|
+
}, [paginated, streamMessages]);
|
|
199
|
+
|
|
200
|
+
return merged as ThreadMessagesResult<Query> & {
|
|
201
|
+
key: string;
|
|
202
|
+
streaming: boolean;
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @deprecated FYI `useStreamingUIMessages` is likely better for you.
|
|
208
|
+
* A hook that fetches streaming messages from a thread.
|
|
209
|
+
* This ONLY returns streaming messages. To get both, use `useThreadMessages`.
|
|
210
|
+
*
|
|
211
|
+
* @param query The query to use to fetch messages.
|
|
212
|
+
* It must take as arguments `{ threadId, paginationOpts, streamArgs }` and
|
|
213
|
+
* return a `streams` object returned from `agent.syncStreams`.
|
|
214
|
+
* @param args The arguments to pass to the query other than `paginationOpts`
|
|
215
|
+
* and `streamArgs`. So `{ threadId }` at minimum, plus any other arguments that
|
|
216
|
+
* you want to pass to the query.
|
|
217
|
+
* @returns The streaming messages.
|
|
218
|
+
*/
|
|
219
|
+
export function useStreamingThreadMessages<
|
|
220
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
221
|
+
Query extends StreamQuery<any>,
|
|
222
|
+
>(
|
|
223
|
+
query: Query,
|
|
224
|
+
args:
|
|
225
|
+
| (StreamMessagesArgs<Query> & {
|
|
226
|
+
/** @deprecated Pass startOrder to the next argument (third argument). */
|
|
227
|
+
startOrder?: number;
|
|
228
|
+
})
|
|
229
|
+
| "skip",
|
|
230
|
+
options?: {
|
|
231
|
+
startOrder?: number;
|
|
232
|
+
skipStreamIds?: string[];
|
|
233
|
+
},
|
|
234
|
+
): Array<MessageDoc> | undefined {
|
|
235
|
+
const queryArgs =
|
|
236
|
+
args === "skip"
|
|
237
|
+
? args
|
|
238
|
+
: (omit(args, ["startOrder"]) as unknown as StreamMessagesArgs<Query>);
|
|
239
|
+
const startOrder =
|
|
240
|
+
args === "skip" ? undefined : (args.startOrder ?? undefined);
|
|
241
|
+
const queryOptions = { startOrder, ...options };
|
|
242
|
+
const uiMessages = useStreamingUIMessages(query, queryArgs, queryOptions);
|
|
243
|
+
if (args === "skip") {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
// TODO: we aren't passing through as much metadata as we could here.
|
|
247
|
+
// We could share the stream metadata logic with useStreamingUIMessages.
|
|
248
|
+
return uiMessages
|
|
249
|
+
?.map((m) => fromUIMessages([m], { threadId: args.threadId }))
|
|
250
|
+
.flat();
|
|
251
|
+
}
|