UNPKG

@convex-dev/agent

Version:

A agent component for Convex.

124 lines 4.36 kB
export const MAX_FILE_SIZE = 1024 * 64; /** * Store a file in the file storage and return the URL and fileId. * @param ctx A ctx object from an action. * @param component The agent component. * @param blob The blob to store. * @param filename The filename to store. * @param sha256 The sha256 hash of the file. If not provided, it will be * computed. However, to ensure no corruption during transfer, you can * calculate this on the client to enforce integrity. * @returns The URL, fileId, and storageId of the stored file. */ export async function storeFile(ctx, component, blob, filename, sha256) { if (!("runAction" in ctx)) { throw new Error("You're trying to save a file that's too large in a mutation. " + "You can store the file in file storage from an action first, then pass a URL instead. " + "To have the agent component track the file, you can use `saveFile` from an action then use the fileId with getFile in the mutation. " + "Read more in the docs."); } const hash = sha256 || Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", await blob.arrayBuffer()))) .map((b) => b.toString(16).padStart(2, "0")) .join(""); const reused = await ctx.runMutation(component.files.useExistingFile, { hash, filename, }); if (reused) { const url = (await ctx.storage.getUrl(reused.storageId)); return { ...getParts(url, blob.type, filename), file: { url, fileId: reused.fileId, storageId: reused.storageId, hash, filename, }, }; } const newStorageId = await ctx.storage.store(blob); if (sha256) { const metadata = await ctx.storage.getMetadata(newStorageId); if (metadata?.sha256 !== sha256) { throw new Error("Hash mismatch: " + metadata?.sha256 + " != " + sha256); } } const { fileId, storageId } = await ctx.runMutation(component.files.addFile, { storageId: newStorageId, hash, filename, mimeType: blob.type, }); const url = (await ctx.storage.getUrl(storageId)); if (storageId !== newStorageId) { // We're re-using another file's storageId // Because we try to reuse the file above, this should be very very rare // and only in the case of racing to check then store the file. await ctx.storage.delete(newStorageId); } return { ...getParts(url, blob.type, filename), file: { url, fileId, storageId: storageId, hash, filename, }, }; } /** * Get file metadata from the component. * This also returns filePart (and imagePart if the file is an image), * which are useful to construct a CoreMessage like * ```ts * const { filePart, imagePart } = await getFile(ctx, components.agent, fileId); * const message: UserMessage = { * role: "user", * content: [imagePart ?? filePart], * }; * ``` * @param ctx A ctx object from an action or query. * @param component The agent component, usually `components.agent`. * @param fileId The fileId of the file to get. * @returns The file metadata and content parts. */ export async function getFile(ctx, component, fileId) { const file = await ctx.runQuery(component.files.get, { fileId }); if (!file) { throw new Error(`File not found in component: ${fileId}`); } const url = await ctx.storage.getUrl(file.storageId); if (!url) { throw new Error(`File not found in storage: ${file.storageId}`); } return { ...getParts(url, file.mimeType, file.filename), file: { fileId, url, storageId: file.storageId, hash: file.hash, filename: file.filename, }, }; } function getParts(url, mimeType, filename) { const filePart = { type: "file", data: new URL(url), mimeType, filename, }; const imagePart = mimeType.startsWith("image/") ? { type: "image", image: new URL(url), mimeType, } : undefined; return { filePart, imagePart }; } //# sourceMappingURL=files.js.map