UNPKG

@convex-dev/agent

Version:

A agent component for Convex.

110 lines 4.66 kB
"use client"; import {} from "convex-helpers"; import { usePaginatedQuery } from "convex-helpers/react"; import {} from "convex/react"; import { useMemo } from "react"; import {} from "../UIMessages.js"; import { sorted } from "../shared.js"; import { useStreamingUIMessages } from "./useStreamingUIMessages.js"; import { combineUIMessages } from "../deltas.js"; /** * A hook that fetches UIMessages from a thread. * * It's similar to useThreadMessages, for endpoints that return UIMessages. * The streaming messages are materialized as UIMessages. The rest are passed * through from the query. * * This hook is a wrapper around `usePaginatedQuery` and `useStreamingUIMessages`. * It will fetch both full messages and streaming messages, and merge them together. * * The query must take as arguments `{ threadId, paginationOpts }` and return a * pagination result of objects similar to UIMessage: * * For streaming, it should look like this: * ```ts * export const listThreadMessages = query({ * args: { * threadId: v.string(), * paginationOpts: paginationOptsValidator, * streamArgs: vStreamArgs, * ... other arguments you want * }, * handler: async (ctx, args) => { * // await authorizeThreadAccess(ctx, threadId); * // NOTE: listUIMessages returns UIMessages, not MessageDocs. * const paginated = await listUIMessages(ctx, components.agent, args); * const streams = await syncStreams(ctx, components.agent, args); * // Here you could filter out / modify the documents & stream deltas. * return { ...paginated, streams }; * }, * }); * ``` * * Then the hook can be used like this: * ```ts * const { results, status, loadMore } = useUIMessages( * api.myModule.listThreadMessages, * { threadId }, * { initialNumItems: 10, stream: true } * ); * ``` * * @param query The query to use to fetch messages. * It must take as arguments `{ threadId, paginationOpts }` and return a * pagination result of objects similar to UIMessage: * Required fields: (role, parts, status, order, stepOrder). * To support streaming, it must also take in `streamArgs: vStreamArgs` and * return a `streams` object returned from `syncStreams`. * @param args The arguments to pass to the query other than `paginationOpts` * and `streamArgs`. So `{ threadId }` at minimum, plus any other arguments that * you want to pass to the query. * @param options The options for the query. Similar to usePaginatedQuery. * To enable streaming, pass `stream: true`. * @returns The messages. If stream is true, it will return a list of messages * that includes both full messages and streaming messages. * The streaming messages are materialized as UIMessages. The rest are passed * through from the query. */ export function useUIMessages(query, args, options) { // These are full messages const paginated = usePaginatedQuery(query, args, { initialNumItems: options.initialNumItems }); const startOrder = paginated.results.length ? Math.min(...paginated.results.map((m) => m.order)) : 0; // These are streaming messages that will not include full messages. const streamMessages = useStreamingUIMessages(query, !options.stream || args === "skip" || paginated.status === "LoadingFirstPage" ? "skip" : // eslint-disable-next-line @typescript-eslint/no-explicit-any { ...args, paginationOpts: { cursor: null, numItems: 0 } }, { startOrder, skipStreamIds: options.skipStreamIds }); const merged = useMemo(() => { // Messages may have been split by pagination. Re-combine them here. const combined = combineUIMessages(sorted(paginated.results)); return { ...paginated, results: dedupeMessages(combined, streamMessages ?? []), }; }, [paginated, streamMessages]); return merged; } export function dedupeMessages(messages, streamMessages) { return sorted(messages.concat(streamMessages)).reduce((msgs, msg) => { const last = msgs.at(-1); if (!last) { return [msg]; } if (last.order !== msg.order || last.stepOrder !== msg.stepOrder) { return [...msgs, msg]; } if ((last.status === "pending" || last.status === "streaming") && msg.status !== "pending") { // Let's prefer a streaming or finalized message over a pending // one. return [...msgs.slice(0, -1), msg]; } // skip the new one if the previous one (listed) was finalized return msgs; }, []); } //# sourceMappingURL=useUIMessages.js.map