@convex-dev/agent
Version:
A agent component for Convex.
785 lines • 35.1 kB
JavaScript
import { generateObject, generateText, stepCountIs, streamObject } from "ai";
import { assert, omit, pick } from "convex-helpers";
import { internalActionGeneric, internalMutationGeneric, } from "convex/server";
import { convexToJson, v } from "convex/values";
import {} from "../component/vector/tables.js";
import { toModelMessage, serializeMessage, serializeNewMessagesInStep, serializeObjectResult, } from "../mapping.js";
import { getModelName, getProviderName } from "../shared.js";
import { vMessageEmbeddings, vMessageWithMetadata, vSafeObjectArgs, vTextArgs, } from "../validators.js";
import { listMessages, saveMessages, } from "./messages.js";
import { embedMany, embedMessages, fetchContextMessages, generateAndSaveEmbeddings, } from "./search.js";
import { startGeneration } from "./start.js";
import { syncStreams } from "./streaming.js";
import { createThread, getThreadMetadata } from "./threads.js";
import { streamText } from "./streamText.js";
import { errorToString, willContinue } from "./utils.js";
export { stepCountIs } from "ai";
export { docsToModelMessages, toModelMessage,
//** @deprecated use toModelMessage instead */
toModelMessage as deserializeMessage, guessMimeType, serializeDataOrUrl, serializeMessage, toUIFilePart, } from "../mapping.js";
// NOTE: these are also exported via @convex-dev/agent/validators
// a future version may put them all here or move these over there
export { extractText, isTool, sorted } from "../shared.js";
export { vAssistantMessage, vContent, vContextOptions, vMessage, vMessageDoc, vPaginationResult, vProviderMetadata, vSource, vStorageOptions, vStreamArgs, vSystemMessage, vThreadDoc, vToolMessage, vUsage, vUserMessage, } from "../validators.js";
export { createTool } from "./createTool.js";
export { definePlaygroundAPI, } from "./definePlaygroundAPI.js";
export { getFile, storeFile } from "./files.js";
export { listMessages, listUIMessages, saveMessage, saveMessages, } from "./messages.js";
export { mockModel } from "./mockModel.js";
export { fetchContextMessages, filterOutOrphanedToolMessages, fetchContextWithPrompt, generateAndSaveEmbeddings, embedMessages, embedMany, } from "./search.js";
export { startGeneration } from "./start.js";
export { DEFAULT_STREAMING_OPTIONS, DeltaStreamer, abortStream, compressUIMessageChunks, listStreams, syncStreams, vStreamMessagesReturnValue, } from "./streaming.js";
export { createThread, getThreadMetadata, searchThreadTitles, updateThreadMetadata, } from "./threads.js";
export { toUIMessages, fromUIMessages } from "../UIMessages.js";
export class Agent {
component;
options;
constructor(component, options) {
this.component = component;
this.options = options;
}
async createThread(ctx, args) {
const threadId = await createThread(ctx, this.component, args);
if (!("runAction" in ctx) || "workflowId" in ctx) {
return { threadId };
}
const { thread } = await this.continueThread(ctx, {
threadId,
userId: args?.userId,
});
return { threadId, thread };
}
/**
* Continues a thread using this agent. Note: threads can be continued
* by different agents. This is a convenience around calling the various
* generate and stream functions with explicit userId and threadId parameters.
* @param ctx The ctx object passed to the action handler
* @param { threadId, userId }: the thread and user to associate the messages with.
* @returns Functions bound to the userId and threadId on a `{thread}` object.
*/
async continueThread(ctx, args) {
return {
thread: {
threadId: args.threadId,
getMetadata: this.getThreadMetadata.bind(this, ctx, {
threadId: args.threadId,
}),
updateMetadata: (patch) => ctx.runMutation(this.component.threads.updateThread, {
threadId: args.threadId,
patch,
}),
generateText: this.generateText.bind(this, ctx, args),
streamText: this.streamText.bind(this, ctx, args),
generateObject: this.generateObject.bind(this, ctx, args),
streamObject: this.streamObject.bind(this, ctx, args),
},
};
}
async start(ctx,
/**
* These are the arguments you'll pass to the LLM call such as
* `generateText` or `streamText`. This function will look up the context
* and provide functions to save the steps, abort the generation, and more.
* The type of the arguments returned infers from the type of the arguments
* you pass here.
*/
args, options) {
return startGeneration(ctx, this.component, {
...args,
tools: (args.tools ?? this.options.tools),
system: args.system ?? this.options.instructions,
stopWhen: (args.stopWhen ?? this.options.stopWhen),
}, {
...this.options,
...options,
agentName: this.options.name,
agentForToolCtx: this,
});
}
/**
* This behaves like {@link generateText} from the "ai" package except that
* it add context based on the userId and threadId and saves the input and
* resulting messages to the thread, if specified.
* Use {@link continueThread} to get a version of this function already scoped
* to a thread (and optionally userId).
* @param ctx The context passed from the action function calling this.
* @param scope: The user and thread to associate the message with
* @param generateTextArgs The arguments to the generateText function, along
* with {@link AgentPrompt} options, such as promptMessageId.
* @param options Extra controls for the {@link ContextOptions} and {@link StorageOptions}.
* @returns The result of the generateText function.
*/
async generateText(ctx, threadOpts,
/**
* The arguments to the generateText function, similar to the ai sdk's
* {@link generateText} function, along with Agent prompt options.
*/
generateTextArgs, options) {
const { args, promptMessageId, order, ...call } = await this.start(ctx, generateTextArgs, { ...threadOpts, ...options });
const steps = [];
try {
const result = (await generateText({
...args,
prepareStep: async (options) => {
const result = await generateTextArgs.prepareStep?.(options);
call.updateModel(result?.model ?? options.model);
return result;
},
onStepFinish: async (step) => {
steps.push(step);
await call.save({ step }, await willContinue(steps, args.stopWhen));
return generateTextArgs.onStepFinish?.(step);
},
}));
const metadata = {
promptMessageId,
order,
savedMessages: call.getSavedMessages(),
messageId: promptMessageId,
};
return Object.assign(result, metadata);
}
catch (error) {
await call.fail(errorToString(error));
throw error;
}
}
/**
* This behaves like {@link streamText} from the "ai" package except that
* it add context based on the userId and threadId and saves the input and
* resulting messages to the thread, if specified.
* Use {@link continueThread} to get a version of this function already scoped
* to a thread (and optionally userId).
*/
async streamText(ctx, threadOpts,
/**
* The arguments to the streamText function, similar to the ai sdk's
* {@link streamText} function, along with Agent prompt options.
*/
streamTextArgs,
/**
* The {@link ContextOptions} and {@link StorageOptions}
* options to use for fetching contextual messages and saving input/output messages.
*/
options) {
return streamText(ctx, this.component, {
...streamTextArgs,
model: streamTextArgs.model ?? this.options.languageModel,
tools: (streamTextArgs.tools ?? this.options.tools),
system: streamTextArgs.system ?? this.options.instructions,
stopWhen: (streamTextArgs.stopWhen ?? this.options.stopWhen),
}, {
...threadOpts,
...this.options,
agentName: this.options.name,
agentForToolCtx: this,
...options,
});
}
/**
* This behaves like {@link generateObject} from the "ai" package except that
* it add context based on the userId and threadId and saves the input and
* resulting messages to the thread, if specified.
* Use {@link continueThread} to get a version of this function already scoped
* to a thread (and optionally userId).
*/
async generateObject(ctx, threadOpts,
/**
* The arguments to the generateObject function, similar to the ai sdk's
* {@link generateObject} function, along with Agent prompt options.
*/
generateObjectArgs,
/**
* The {@link ContextOptions} and {@link StorageOptions}
* options to use for fetching contextual messages and saving input/output messages.
*/
options) {
const { args, promptMessageId, order, fail, save, getSavedMessages } = await this.start(ctx, generateObjectArgs, { ...threadOpts, ...options });
try {
const result = (await generateObject(args));
await save({ object: result });
const metadata = {
promptMessageId,
order,
savedMessages: getSavedMessages(),
messageId: promptMessageId,
};
return Object.assign(result, metadata);
}
catch (error) {
await fail(errorToString(error));
throw error;
}
}
/**
* This behaves like `streamObject` from the "ai" package except that
* it add context based on the userId and threadId and saves the input and
* resulting messages to the thread, if specified.
* Use {@link continueThread} to get a version of this function already scoped
* to a thread (and optionally userId).
*/
async streamObject(ctx, threadOpts,
/**
* The arguments to the streamObject function, similar to the ai sdk's
* {@link streamObject} function, along with Agent prompt options.
*/
streamObjectArgs,
/**
* The {@link ContextOptions} and {@link StorageOptions}
* options to use for fetching contextual messages and saving input/output messages.
*/
options) {
const { args, promptMessageId, order, fail, save, getSavedMessages } = await this.start(ctx, streamObjectArgs, { ...threadOpts, ...options });
const stream = streamObject({
...args,
onError: async (error) => {
console.error(" streamObject onError", error);
// TODO: content that we have so far
// content: stream.fullStream.
await fail(errorToString(error.error));
return args.onError?.(error);
},
onFinish: async (result) => {
await save({
object: {
object: result.object,
finishReason: result.error ? "error" : "stop",
usage: result.usage,
warnings: result.warnings,
request: await stream.request,
response: result.response,
providerMetadata: result.providerMetadata,
toJsonResponse: stream.toTextStreamResponse,
reasoning: undefined,
},
});
return args.onFinish?.(result);
},
});
const metadata = {
promptMessageId,
order,
savedMessages: getSavedMessages(),
messageId: promptMessageId,
};
return Object.assign(stream, metadata);
}
/**
* Save a message to the thread.
* @param ctx A ctx object from a mutation or action.
* @param args The message and what to associate it with (user / thread)
* You can pass extra metadata alongside the message, e.g. associated fileIds.
* @returns The messageId of the saved message.
*/
async saveMessage(ctx, args) {
const { messages } = await this.saveMessages(ctx, {
threadId: args.threadId,
userId: args.userId,
embeddings: args.embedding
? { model: args.embedding.model, vectors: [args.embedding.vector] }
: undefined,
messages: args.prompt !== undefined
? [{ role: "user", content: args.prompt }]
: [args.message],
metadata: args.metadata ? [args.metadata] : undefined,
skipEmbeddings: args.skipEmbeddings,
promptMessageId: args.promptMessageId,
pendingMessageId: args.pendingMessageId,
});
const message = messages.at(-1);
return { messageId: message._id, message };
}
/**
* Explicitly save messages associated with the thread (& user if provided)
* If you have an embedding model set, it will also generate embeddings for
* the messages.
* @param ctx The ctx parameter to a mutation or action.
* @param args The messages and context to save
* @returns
*/
async saveMessages(ctx, args) {
let embeddings;
const { skipEmbeddings, ...rest } = args;
if (args.embeddings) {
embeddings = args.embeddings;
}
else if (!skipEmbeddings && this.options.textEmbeddingModel) {
if (!("runAction" in ctx)) {
console.warn("You're trying to save messages and generate embeddings, but you're in a mutation. " +
"Pass `skipEmbeddings: true` to skip generating embeddings in the mutation and skip this warning. " +
"They will be generated lazily when you generate or stream text / objects. " +
"You can explicitly generate them asynchronously by using the scheduler to run an action later that calls `agent.generateAndSaveEmbeddings`.");
}
else if ("workflowId" in ctx) {
console.warn("You're trying to save messages and generate embeddings, but you're in a workflow. " +
"Pass `skipEmbeddings: true` to skip generating embeddings in the workflow and skip this warning. " +
"They will be generated lazily when you generate or stream text / objects. " +
"You can explicitly generate them asynchronously by using the scheduler to run an action later that calls `agent.generateAndSaveEmbeddings`.");
}
else {
embeddings = await this.generateEmbeddings(ctx, { userId: args.userId ?? undefined, threadId: args.threadId }, args.messages);
}
}
return saveMessages(ctx, this.component, {
...rest,
agentName: this.options.name,
embeddings,
});
}
/**
* List messages from a thread.
* @param ctx A ctx object from a query, mutation, or action.
* @param args.threadId The thread to list messages from.
* @param args.paginationOpts Pagination options (e.g. via usePaginatedQuery).
* @param args.excludeToolMessages Whether to exclude tool messages.
* False by default.
* @param args.statuses What statuses to include. All by default.
* @returns The MessageDoc's in a format compatible with usePaginatedQuery.
*/
async listMessages(ctx, args) {
return listMessages(ctx, this.component, args);
}
/**
* A function that handles fetching stream deltas, used with the React hooks
* `useThreadMessages` or `useStreamingThreadMessages`.
* @param ctx A ctx object from a query, mutation, or action.
* @param args.threadId The thread to sync streams for.
* @param args.streamArgs The stream arguments with per-stream cursors.
* @returns The deltas for each stream from their existing cursor.
*/
async syncStreams(ctx, args) {
return syncStreams(ctx, this.component, args);
}
/**
* Fetch the context messages for a thread.
* @param ctx Either a query, mutation, or action ctx.
* If it is not an action context, you can't do text or
* vector search.
* @param args The associated thread, user, message
* @returns
*/
async fetchContextMessages(ctx, args) {
assert(args.userId || args.threadId, "Specify userId or threadId");
const contextOptions = {
...this.options.contextOptions,
...args.contextOptions,
};
return fetchContextMessages(ctx, this.component, {
...args,
contextOptions,
getEmbedding: async (text) => {
assert("runAction" in ctx);
assert(this.options.textEmbeddingModel, "A textEmbeddingModel is required to be set on the Agent that you're doing vector search with");
return {
embedding: (await embedMany(ctx, {
...this.options,
agentName: this.options.name,
userId: args.userId,
threadId: args.threadId,
values: [text],
})).embeddings[0],
textEmbeddingModel: this.options.textEmbeddingModel,
};
},
});
}
/**
* Get the metadata for a thread.
* @param ctx A ctx object from a query, mutation, or action.
* @param args.threadId The thread to get the metadata for.
* @returns The metadata for the thread.
*/
async getThreadMetadata(ctx, args) {
return getThreadMetadata(ctx, this.component, args);
}
/**
* Update the metadata for a thread.
* @param ctx A ctx object from a mutation or action.
* @param args.threadId The thread to update the metadata for.
* @param args.patch The patch to apply to the thread.
* @returns The updated thread metadata.
*/
async updateThreadMetadata(ctx, args) {
const thread = await ctx.runMutation(this.component.threads.updateThread, args);
return thread;
}
/**
* Get the embeddings for a set of messages.
* @param messages The messages to get the embeddings for.
* @returns The embeddings for the messages.
*/
async generateEmbeddings(ctx, args, messages) {
return embedMessages(ctx, { ...args, ...this.options, agentName: this.options.name }, messages);
}
/**
* Generate embeddings for a set of messages, and save them to the database.
* It will not generate or save embeddings for messages that already have an
* embedding.
* @param ctx The ctx parameter to an action.
* @param args The messageIds to generate embeddings for.
*/
async generateAndSaveEmbeddings(ctx, args) {
const messages = (await ctx.runQuery(this.component.messages.getMessagesByIds, {
messageIds: args.messageIds,
})).filter((m) => m !== null);
if (messages.length !== args.messageIds.length) {
throw new Error("Some messages were not found: " +
args.messageIds
.filter((id) => !messages.some((m) => m?._id === id))
.join(", "));
}
if (messages.some((m) => !m.message)) {
throw new Error("Some messages don't have a message: " +
messages
.filter((m) => !m.message)
.map((m) => m._id)
.join(", "));
}
const { textEmbeddingModel } = this.options;
if (!textEmbeddingModel) {
throw new Error("No embeddings were generated for the messages. You must pass a textEmbeddingModel to the agent constructor.");
}
await generateAndSaveEmbeddings(ctx, this.component, {
...this.options,
agentName: this.options.name,
threadId: messages[0].threadId,
userId: messages[0].userId,
textEmbeddingModel,
}, messages);
}
/**
* Explicitly save a "step" created by the AI SDK.
* @param ctx The ctx argument to a mutation or action.
* @param args The Step generated by the AI SDK.
*/
async saveStep(ctx, args) {
const { messages } = await serializeNewMessagesInStep(ctx, this.component, args.step, {
provider: args.provider ?? getProviderName(this.options.languageModel),
model: args.model ?? getModelName(this.options.languageModel),
});
const embeddings = await this.generateEmbeddings(ctx, { userId: args.userId, threadId: args.threadId }, messages.map((m) => m.message));
return ctx.runMutation(this.component.messages.addMessages, {
userId: args.userId,
threadId: args.threadId,
agentName: this.options.name,
promptMessageId: args.promptMessageId,
messages,
embeddings,
failPendingSteps: false,
});
}
/**
* Manually save the result of a generateObject call to the thread.
* This happens automatically when using {@link generateObject} or {@link streamObject}
* from the `thread` object created by {@link continueThread} or {@link createThread}.
* @param ctx The context passed from the mutation or action function calling this.
* @param args The arguments to the saveObject function.
*/
async saveObject(ctx, args) {
const { messages } = await serializeObjectResult(ctx, this.component, args.result, {
model: args.model ??
args.metadata?.model ??
getModelName(this.options.languageModel),
provider: args.provider ??
args.metadata?.provider ??
getProviderName(this.options.languageModel),
});
const embeddings = await this.generateEmbeddings(ctx, { userId: args.userId, threadId: args.threadId }, messages.map((m) => m.message));
return ctx.runMutation(this.component.messages.addMessages, {
userId: args.userId,
threadId: args.threadId,
promptMessageId: args.promptMessageId,
failPendingSteps: false,
messages,
embeddings,
agentName: this.options.name,
});
}
/**
* Commit or rollback a message that was pending.
* This is done automatically when saving messages by default.
* If creating pending messages, you can call this when the full "transaction" is done.
* @param ctx The ctx argument to your mutation or action.
* @param args What message to save. Generally the parent message sent into
* the generateText call.
*/
async finalizeMessage(ctx, args) {
await ctx.runMutation(this.component.messages.finalizeMessage, {
messageId: args.messageId,
result: args.result,
});
}
/**
* Update a message by its id.
* @param ctx The ctx argument to your mutation or action.
* @param args The message fields to update.
*/
async updateMessage(ctx, args) {
const { message, fileIds } = await serializeMessage(ctx, this.component, args.patch.message);
await ctx.runMutation(this.component.messages.updateMessage, {
messageId: args.messageId,
patch: {
message,
fileIds: args.patch.fileIds
? [...args.patch.fileIds, ...(fileIds ?? [])]
: fileIds,
status: args.patch.status === "success" ? "success" : "failed",
error: args.patch.error,
},
});
}
/**
* Delete multiple messages by their ids, including their embeddings
* and reduce the refcount of any files they reference.
* @param ctx The ctx argument to your mutation or action.
* @param args The ids of the messages to delete.
*/
async deleteMessages(ctx, args) {
await ctx.runMutation(this.component.messages.deleteByIds, args);
}
/**
* Delete a single message by its id, including its embedding
* and reduce the refcount of any files it references.
* @param ctx The ctx argument to your mutation or action.
* @param args The id of the message to delete.
*/
async deleteMessage(ctx, args) {
await ctx.runMutation(this.component.messages.deleteByIds, {
messageIds: [args.messageId],
});
}
/**
* Delete a range of messages by their order and step order.
* Each "order" is a set of associated messages in response to the message
* at stepOrder 0.
* The (startOrder, startStepOrder) is inclusive
* and the (endOrder, endStepOrder) is exclusive.
* To delete all messages at "order" 1, you can pass:
* `{ startOrder: 1, endOrder: 2 }`
* To delete a message at step (order=1, stepOrder=1), you can pass:
* `{ startOrder: 1, startStepOrder: 1, endOrder: 1, endStepOrder: 2 }`
* To delete all messages between (1, 1) up to and including (3, 5), you can pass:
* `{ startOrder: 1, startStepOrder: 1, endOrder: 3, endStepOrder: 6 }`
*
* If it cannot do it in one transaction, it returns information you can use
* to resume the deletion.
* e.g.
* ```ts
* let isDone = false;
* let lastOrder = args.startOrder;
* let lastStepOrder = args.startStepOrder ?? 0;
* while (!isDone) {
* // eslint-disable-next-line @typescript-eslint/no-explicit-any
* ({ isDone, lastOrder, lastStepOrder } = await agent.deleteMessageRange(
* ctx,
* {
* threadId: args.threadId,
* startOrder: lastOrder,
* startStepOrder: lastStepOrder,
* endOrder: args.endOrder,
* endStepOrder: args.endStepOrder,
* }
* ));
* }
* ```
* @param ctx The ctx argument to your mutation or action.
* @param args The range of messages to delete.
*/
async deleteMessageRange(ctx, args) {
return ctx.runMutation(this.component.messages.deleteByOrder, {
threadId: args.threadId,
startOrder: args.startOrder,
startStepOrder: args.startStepOrder,
endOrder: args.endOrder,
endStepOrder: args.endStepOrder,
});
}
/**
* Delete a thread and all its messages and streams asynchronously (in batches)
* This uses a mutation to that processes one page and recursively queues the
* next page for deletion.
* @param ctx The ctx argument to your mutation or action.
* @param args The id of the thread to delete and optionally the page size to use for the delete.
*/
async deleteThreadAsync(ctx, args) {
await ctx.runMutation(this.component.threads.deleteAllForThreadIdAsync, {
threadId: args.threadId,
limit: args.pageSize,
});
}
/**
* Delete a thread and all its messages and streams synchronously.
* This uses an action to iterate through all pages. If the action fails
* partway, it will not automatically restart.
* @param ctx The ctx argument to your action.
* @param args The id of the thread to delete and optionally the page size to use for the delete.
*/
async deleteThreadSync(ctx, args) {
await ctx.runAction(this.component.threads.deleteAllForThreadIdSync, {
threadId: args.threadId,
limit: args.pageSize,
});
}
/**
* WORKFLOW UTILITIES
*/
/**
* Create a mutation that creates a thread so you can call it from a Workflow.
* e.g.
* ```ts
* // in convex/foo.ts
* export const createThread = weatherAgent.createThreadMutation();
*
* const workflow = new WorkflowManager(components.workflow);
* export const myWorkflow = workflow.define({
* args: {},
* handler: async (step) => {
* const { threadId } = await step.runMutation(internal.foo.createThread);
* // use the threadId to generate text, object, etc.
* },
* });
* ```
* @returns A mutation that creates a thread.
*/
createThreadMutation() {
return internalMutationGeneric({
args: {
userId: v.optional(v.string()),
title: v.optional(v.string()),
summary: v.optional(v.string()),
},
handler: async (ctx, args) => {
const { threadId } = await this.createThread(ctx, args);
return { threadId };
},
});
}
/**
* Create an action out of this agent so you can call it from workflows or other actions
* without a wrapping function.
* @param spec Configuration for the agent acting as an action, including
* {@link ContextOptions}, {@link StorageOptions}, and {@link stopWhen}.
*/
asTextAction(spec, overrides) {
return internalActionGeneric({
args: vTextArgs,
handler: async (ctx_, args) => {
const stream = args.stream === true ? spec?.stream || true : (spec?.stream ?? false);
const { userId, threadId, prompt, messages, maxSteps, ...rest } = args;
const targetArgs = { userId, threadId };
const llmArgs = {
stopWhen: spec?.stopWhen,
...overrides,
...omit(rest, ["storageOptions", "contextOptions", "stream"]),
messages: messages?.map(toModelMessage),
prompt: Array.isArray(prompt) ? prompt.map(toModelMessage) : prompt,
toolChoice: args.toolChoice,
};
if (maxSteps) {
llmArgs.stopWhen = stepCountIs(maxSteps);
}
const opts = {
...pick(spec, ["contextOptions", "storageOptions"]),
...pick(args, ["contextOptions", "storageOptions"]),
saveStreamDeltas: stream,
};
const ctx = (spec?.customCtx
? { ...ctx_, ...spec.customCtx(ctx_, targetArgs, llmArgs) }
: ctx_);
if (stream) {
const result = await this.streamText(ctx, targetArgs, llmArgs, opts);
await result.consumeStream();
return {
text: await result.text,
promptMessageId: result.promptMessageId,
order: result.order,
finishReason: await result.finishReason,
warnings: await result.warnings,
savedMessageIds: result.savedMessages?.map((m) => m._id) ?? [],
};
}
else {
const res = await this.generateText(ctx, targetArgs, llmArgs, opts);
return {
text: res.text,
promptMessageId: res.promptMessageId,
order: res.order,
finishReason: res.finishReason,
warnings: res.warnings,
savedMessageIds: res.savedMessages?.map((m) => m._id) ?? [],
};
}
},
});
}
/**
* Create an action that generates an object out of this agent so you can call
* it from workflows or other actions without a wrapping function.
* @param spec Configuration for the agent acting as an action, including
* the normal parameters to {@link generateObject}, plus {@link ContextOptions}
* and stopWhen.
*/
asObjectAction(objectArgs, options) {
return internalActionGeneric({
args: vSafeObjectArgs,
handler: async (ctx_, args) => {
const { userId, threadId, callSettings, ...rest } = args;
const overrides = pick(rest, ["contextOptions", "storageOptions"]);
const targetArgs = { userId, threadId };
const llmArgs = {
...objectArgs,
...callSettings,
...omit(rest, ["storageOptions", "contextOptions"]),
messages: args.messages?.map(toModelMessage),
prompt: Array.isArray(args.prompt)
? args.prompt.map(toModelMessage)
: args.prompt,
};
const ctx = (options?.customCtx
? { ...ctx_, ...options.customCtx(ctx_, targetArgs, llmArgs) }
: ctx_);
const value = await this.generateObject(ctx, targetArgs, llmArgs, {
...this.options,
...options,
...overrides,
});
return {
object: convexToJson(value.object),
promptMessageId: value.promptMessageId,
order: value.order,
finishReason: value.finishReason,
warnings: value.warnings,
savedMessageIds: value.savedMessages?.map((m) => m._id) ?? [],
};
},
});
}
/**
* @deprecated Use {@link saveMessages} directly instead.
*/
asSaveMessagesMutation() {
return internalMutationGeneric({
args: {
threadId: v.string(),
userId: v.optional(v.string()),
promptMessageId: v.optional(v.string()),
messages: v.array(vMessageWithMetadata),
failPendingSteps: v.optional(v.boolean()),
embeddings: v.optional(vMessageEmbeddings),
},
handler: async (ctx, args) => {
const { messages } = await this.saveMessages(ctx, {
...args,
messages: args.messages.map((m) => toModelMessage(m.message)),
metadata: args.messages.map(({ message: _, ...m }) => m),
skipEmbeddings: true,
});
return {
lastMessageId: messages.at(-1)._id,
messages: messages.map((m) => pick(m, ["_id", "order", "stepOrder"])),
};
},
});
}
}
//# sourceMappingURL=index.js.map