UNPKG

@convex-dev/agent

Version:

A agent component for Convex.

263 lines (247 loc) 7.6 kB
import { paginator } from "convex-helpers/server/pagination"; import { stream } from "convex-helpers/server/stream"; import { nullable } from "convex-helpers/validators"; import { paginationOptsValidator } from "convex/server"; import type { ObjectType } from "convex/values"; import { vPaginationResult } from "../validators.js"; import { internal } from "./_generated/api.js"; import type { Id } from "./_generated/dataModel.js"; import { action, internalMutation, internalQuery, mutation, type MutationCtx, query, } from "./_generated/server.js"; import { deleteMessage } from "./messages.js"; import { deleteStreamsPageForThreadId } from "./streams.js"; import { schema, v } from "./schema.js"; // Note: it only searches for users with threads export const listUsersWithThreads = query({ args: { paginationOpts: paginationOptsValidator, }, handler: async (ctx, args) => { const results = await stream(ctx.db, schema) .query("threads") .withIndex("userId", (q) => q.gt("userId", "")) .distinct(["userId"]) .paginate(args.paginationOpts); return { ...results, page: results.page.map((t) => t.userId).filter((t): t is string => !!t), }; }, returns: vPaginationResult(v.string()), }); export const deleteAllForUserId = action({ args: { userId: v.string() }, handler: async (ctx, args) => { let threadsCursor = null; let threadInProgress = null; let messagesCursor = null; let streamsInProgress = false; let streamOrder = undefined; let deltaCursor = undefined; let isDone = false; while (!isDone) { const result: DeleteAllReturns = await ctx.runMutation( internal.users._deletePageForUserId, { userId: args.userId, messagesCursor, threadInProgress, threadsCursor, streamsInProgress, streamOrder, deltaCursor, }, ); messagesCursor = result.messagesCursor; threadInProgress = result.threadInProgress; threadsCursor = result.threadsCursor; streamsInProgress = result.streamsInProgress ?? false; streamOrder = result.streamOrder; deltaCursor = result.deltaCursor; isDone = result.isDone; } }, returns: v.null(), }); export const deleteAllForUserIdAsync = mutation({ args: { userId: v.string(), }, handler: async (ctx, args) => { const isDone = await deleteAllForUserIdAsyncHandler(ctx, { userId: args.userId, messagesCursor: null, threadsCursor: null, threadInProgress: null, streamsInProgress: false, streamOrder: undefined, deltaCursor: undefined, }); return isDone; }, returns: v.boolean(), }); const deleteAllArgs = { userId: v.string(), messagesCursor: nullable(v.string()), threadsCursor: nullable(v.string()), threadInProgress: nullable(v.id("threads")), streamsInProgress: v.optional(v.boolean()), streamOrder: v.optional(v.number()), deltaCursor: v.optional(v.string()), }; type DeleteAllArgs = ObjectType<typeof deleteAllArgs>; const deleteAllReturns = { threadsCursor: v.string(), threadInProgress: nullable(v.id("threads")), messagesCursor: nullable(v.string()), streamsInProgress: v.optional(v.boolean()), streamOrder: v.optional(v.number()), deltaCursor: v.optional(v.string()), isDone: v.boolean(), }; type DeleteAllReturns = ObjectType<typeof deleteAllReturns>; export const _deleteAllForUserIdAsync = internalMutation({ args: deleteAllArgs, handler: deleteAllForUserIdAsyncHandler, returns: v.boolean(), }); async function deleteAllForUserIdAsyncHandler( ctx: MutationCtx, args: DeleteAllArgs, ): Promise<boolean> { const result = await deletePageForUserId(ctx, args); if (!result.isDone) { await ctx.scheduler.runAfter(0, internal.users._deleteAllForUserIdAsync, { userId: args.userId, messagesCursor: result.messagesCursor, threadsCursor: result.threadsCursor, threadInProgress: result.threadInProgress, streamsInProgress: result.streamsInProgress ?? false, streamOrder: result.streamOrder, deltaCursor: result.deltaCursor, }); } return result.isDone; } export const _deletePageForUserId = internalMutation({ args: deleteAllArgs, handler: deletePageForUserId, returns: deleteAllReturns, }); async function deletePageForUserId( ctx: MutationCtx, args: DeleteAllArgs, ): Promise<DeleteAllReturns> { let threadInProgress: Id<"threads"> | null = args.threadInProgress; let threadsCursor: string | null = args.threadsCursor; let messagesCursor: string | null = args.messagesCursor; let streamsInProgress: boolean = args.streamsInProgress ?? false; let streamOrder: number | undefined = args.streamOrder; let deltaCursor: string | undefined = args.deltaCursor; // Phase 1: Get a thread to work on if we don't have one if (!threadsCursor || !threadInProgress) { const threads = await paginator(ctx.db, schema) .query("threads") .withIndex("userId", (q) => q.eq("userId", args.userId)) .order("desc") .paginate({ numItems: 1, cursor: args.threadsCursor ?? null, }); threadsCursor = threads.continueCursor; if (threads.page.length > 0) { threadInProgress = threads.page[0]._id; messagesCursor = null; streamsInProgress = false; streamOrder = undefined; deltaCursor = undefined; } else { return { isDone: true, threadsCursor, threadInProgress, messagesCursor, streamsInProgress, streamOrder, deltaCursor, }; } } // Phase 2: Delete messages for the current thread if (!streamsInProgress) { const messages = await paginator(ctx.db, schema) .query("messages") .withIndex("threadId_status_tool_order_stepOrder", (q) => q.eq("threadId", threadInProgress!), ) .order("desc") .paginate({ numItems: 100, cursor: args.messagesCursor, }); await Promise.all(messages.page.map((m) => deleteMessage(ctx, m))); if (messages.isDone) { // Messages are done, move to streams deletion phase streamsInProgress = true; messagesCursor = null; streamOrder = undefined; deltaCursor = undefined; } else { messagesCursor = messages.continueCursor; } return { messagesCursor, threadsCursor, threadInProgress, streamsInProgress, streamOrder, deltaCursor, isDone: false, }; } // Phase 3: Delete streams for the current thread const streamResult = await deleteStreamsPageForThreadId(ctx, { threadId: threadInProgress!, streamOrder, deltaCursor, }); if (streamResult.isDone) { // Streams are done, delete the thread and reset for next thread await ctx.db.delete(threadInProgress); threadInProgress = null; messagesCursor = null; streamsInProgress = false; streamOrder = undefined; deltaCursor = undefined; } else { // Continue with streams deletion streamOrder = streamResult.streamOrder; deltaCursor = streamResult.deltaCursor; } return { messagesCursor, threadsCursor, threadInProgress, streamsInProgress, streamOrder, deltaCursor, isDone: false, }; } export const getThreadUserId = internalQuery({ args: { threadId: v.id("threads"), }, returns: v.union(v.string(), v.null()), handler: async (ctx, args) => { const thread = await ctx.db.get(args.threadId); return thread?.userId ?? null; }, });