import { generateText, type EmbeddingModel, type EmbeddingModelUsage, type ModelMessage } from "ai"; import { FunctionReference, type FunctionArgs, type FunctionReturnType, type GenericActionCtx, type GenericDataModel, type GenericMutationCtx, type PaginationOptions, type PaginationResult, type RegisteredAction, type RegisteredMutation } from "convex/server"; import { type Value } from "convex/values"; import { ComponentApi } from "../component/_generated/component.js"; import { OnCompleteArgs, vEntryId, vNamespaceId, vSearchType, type Chunk, type ChunkerAction, type Entry, type EntryFilter, type EntryId, type Namespace, type NamespaceId, type OnComplete, type OnCompleteNamespace, type SearchEntry, type SearchResult, type SearchType, type Status } from "../shared.js"; import { defaultChunker } from "./defaultChunker.js"; export { hybridRank } from "./hybridRank.js"; export { defaultChunker, vEntryId, vNamespaceId, vSearchType }; export type { ChunkerAction, Entry, EntryId, NamespaceId, OnComplete, OnCompleteNamespace, SearchEntry, SearchResult, SearchType, Status, }; export { vEntry, vOnCompleteArgs, vSearchEntry, vSearchResult, type EntryFilter, type VEntry, type VSearchEntry, } from "../shared.js"; export { contentHashFromArrayBuffer, guessMimeTypeFromContents, guessMimeTypeFromExtension, } from "./fileUtils.js"; type Importance = number; export declare class RAG = Record, EntryMetadata extends Record = Record> { component: ComponentApi; options: { embeddingDimension: number; textEmbeddingModel: EmbeddingModel; filterNames?: FilterNames; }; /** * A component to use for Retrieval-Augmented Generation. * Create one for each model and embedding dimension you want to use. * When migrating between models / embedding lengths, create multiple * instances and do your `add`s with the appropriate instance, and searches * against the appropriate instance to get results with those parameters. * * The filterNames need to match the names of the filters you provide when * adding and when searching. Use the type parameter to make this type safe. * * The second type parameter makes the entry metadata type safe. E.g. you can * do rag.add(ctx, { * namespace: "my-namespace", * metadata: { * source: "website" as const, * }, * }) * and then entry results will have the metadata type `{ source: "website" }`. */ constructor(component: ComponentApi, options: { embeddingDimension: number; textEmbeddingModel: EmbeddingModel; filterNames?: FilterNames; }); /** * Add an entry to the store. It will chunk the text with the `defaultChunker` * if you don't provide chunks, and embed the chunks with the default model * if you don't provide chunk embeddings. * * If you provide a key, it will replace an existing entry with the same key. * If you don't provide a key, it will always create a new entry. * If you provide a contentHash, it will deduplicate the entry if it already exists. * The filterValues you provide can be used later to search for it. */ add(ctx: CtxWith<"runMutation">, args: NamespaceSelection & EntryArgs & ({ /** * You can provide your own chunks to finely control the splitting. * These can also include your own provided embeddings, so you can * control what content is embedded, which can differ from the content * in the chunks. */ chunks: Iterable | AsyncIterable; /** @deprecated You cannot specify both chunks and text currently. */ text?: undefined; } | { /** * If you don't provide chunks, we will split the text into chunks * using the default chunker and embed them with the default model. */ text: string; /** @deprecated You cannot specify both chunks and text currently. */ chunks?: undefined; })): Promise<{ entryId: EntryId; status: Status; created: boolean; replacedEntry: Entry | null; usage: EmbeddingModelUsage; }>; /** * Add an entry to the store asynchronously. * * This is useful if you want to chunk the entry in a separate process, * or if you want to chunk the entry in a separate process. * * The chunkerAction is a function that splits the entry into chunks and * embeds them. It should be passed as internal.foo.myChunkerAction * e.g. * ```ts * export const myChunkerAction = rag.defineChunkerAction(async (ctx, args) => { * // ... * return { chunks: [chunk1, chunk2, chunk3] }; * }); * * // in your mutation * const entryId = await rag.addAsync(ctx, { * key: "myfile.txt", * namespace: "my-namespace", * chunkerAction: internal.foo.myChunkerAction, * }); * ``` */ addAsync(ctx: CtxWith<"runMutation">, args: NamespaceSelection & EntryArgs & { /** * A function that splits the entry into chunks and embeds them. * This should be passed as internal.foo.myChunkerAction * e.g. * ```ts * export const myChunkerAction = rag.defineChunkerAction(); * * // in your mutation * const entryId = await rag.addAsync(ctx, { * key: "myfile.txt", * namespace: "my-namespace", * chunker: internal.foo.myChunkerAction, * }); */ chunkerAction: ChunkerAction; }): Promise<{ entryId: EntryId; status: "ready" | "pending"; }>; /** * Search for entries in a namespace with configurable filters. * You can provide a query string or target embedding, as well as search * parameters to filter and constrain the results. */ search(ctx: CtxWith<"runAction">, args: { /** * The namespace to search in. e.g. a userId if entries are per-user. * Note: it will only match entries in the namespace that match the * modelId, embedding dimension, and filterNames of the RAG instance. */ namespace: string; /** * The query to search for. Optional if embedding is provided. */ query: string | Array; } & SearchOptions): Promise<{ results: SearchResult[]; text: string; entries: SearchEntry[]; usage: EmbeddingModelUsage; }>; /** * Generate text based on Retrieval-Augmented Generation. * * This will search for entries in the namespace based on the prompt and use * the results as context to generate text, using the search options args. * You can override the default "instructions" to provide guidance on * using the context and answering in the appropriate style. * You can provide "messages" in addition to the prompt to provide * extra context / conversation history. */ generateText(ctx: CtxWith<"runAction">, args: { /** * The search options to use for context search, including the namespace. */ search: SearchOptions & { /** * The namespace to search in. e.g. a userId if entries are per-user. */ namespace: string; /** * The text or embedding to search for. If provided, it will be used * instead of the prompt for vector search. */ query?: string | Array; }; /** * Required. The prompt to use for context search, as well as the final * message to the LLM when generating text. * Can be used along with "messages" */ prompt: string; /** * Additional messages to add to the context. Can be provided in addition * to the prompt, in which case it will precede the prompt. */ messages?: ModelMessage[]; } & Parameters[0]): Promise> & { context: { results: SearchResult[]; text: string; entries: SearchEntry[]; }; }>; /** * List all entries in a namespace. */ list(ctx: CtxWith<"runQuery">, args: { namespaceId?: NamespaceId; order?: "desc" | "asc"; status?: Status; } & ({ paginationOpts: PaginationOptions; } | { limit: number; })): Promise>>; /** * Get entry metadata by its id. */ getEntry(ctx: CtxWith<"runQuery">, args: { entryId: EntryId; }): Promise | null>; /** * Find an existing entry by its content hash, which you can use to copy * new results into a new entry when migrating, or avoiding duplicating work * when updating content. */ findEntryByContentHash(ctx: CtxWith<"runQuery">, args: { namespace: string; key: string; /** The hash of the entry contents to try to match. */ contentHash: string; }): Promise | null>; /** * Get a namespace that matches the modelId, embedding dimension, and * filterNames of the RAG instance. If it doesn't exist, it will be created. */ getOrCreateNamespace(ctx: CtxWith<"runMutation">, args: { /** * The namespace to get or create. e.g. a userId if entries are per-user. */ namespace: string; /** * If it isn't in existence, what the new namespace status should be. */ status?: "pending" | "ready"; /** * This will be called when then namespace leaves the "pending" state. * Either if the namespace is created or if the namespace is replaced * along the way. */ onComplete?: OnCompleteNamespace; }): Promise<{ namespaceId: NamespaceId; status: "pending" | "ready"; }>; /** * Get a namespace that matches the modelId, embedding dimension, and * filterNames of the RAG instance. If it doesn't exist, it will return null. */ getNamespace(ctx: CtxWith<"runQuery">, args: { namespace: string; }): Promise; /** * List all chunks for an entry, paginated. */ listChunks(ctx: CtxWith<"runQuery">, args: { paginationOpts: PaginationOptions; entryId: EntryId; order?: "desc" | "asc"; }): Promise>; /** * Delete an entry and all its chunks in the background using a workpool. */ deleteAsync(ctx: CtxWith<"runMutation">, args: { entryId: EntryId; }): Promise; /** * Delete an entry and all its chunks (synchronously). * If you are getting warnings about `ctx` not being compatible, * you're likely running this in a mutation. * Use `deleteAsync` or run `delete` in an action. */ delete(ctx: CtxWith<"runAction">, args: { entryId: EntryId; }): Promise; /** @deprecated Use `deleteAsync` in mutations. */ delete(ctx: CtxWith<"runMutation">, args: { entryId: EntryId; }): Promise; /** * Delete all entries with a given key (asynchrounously). */ deleteByKeyAsync(ctx: CtxWith<"runMutation">, args: { namespaceId: NamespaceId; key: string; beforeVersion?: number; }): Promise; /** * Delete all entries with a given key (synchronously). * If you are getting warnings about `ctx` not being compatible, * you're likely running this in a mutation. * Use `deleteByKeyAsync` or run `delete` in an action. */ deleteByKey(ctx: CtxWith<"runAction">, args: { namespaceId: NamespaceId; key: string; beforeVersion?: number; }): Promise; /** * Define a function that can be provided to the `onComplete` parameter of * `add` or `addAsync` like: * ```ts * const onComplete = rag.defineOnComplete(async (ctx, args) => { * // ... * }); * * // in your mutation * await rag.add(ctx, { * namespace: "my-namespace", * onComplete: internal.foo.onComplete, * }); * ``` * It will be called when the entry is no longer "pending". * This is usually when it's "ready" but it can be "replaced" if a newer * entry is ready before this one. */ defineOnComplete(fn: (ctx: GenericMutationCtx, args: OnCompleteArgs) => Promise): RegisteredMutation<"internal", FunctionArgs, null>; /** * Define a function that can be provided to the `chunkerAction` parameter of * `addAsync` like: * ```ts * const chunkerAction = rag.defineChunkerAction(async (ctx, args) => { * // ... * }); * * // in your mutation * const entryId = await rag.addAsync(ctx, { * key: "myfile.txt", * namespace: "my-namespace", * chunkerAction: internal.foo.myChunkerAction, * }); * ``` * It will be called when the entry is added, or when the entry is replaced * along the way. */ defineChunkerAction(fn: (ctx: GenericActionCtx, args: { namespace: Namespace; entry: Entry; }) => AsyncIterable | Promise<{ chunks: InputChunk[]; }>): RegisteredAction<"internal", FunctionArgs, FunctionReturnType>; } type MastraChunk = { text: string; metadata?: Record; embedding?: Array; }; type LangChainChunk = { id?: string; pageContent: string; metadata?: Record; embedding?: Array; }; export type InputChunk = string | ((MastraChunk | LangChainChunk) & { /** * Text to use for full-text search. Defaults to the chunk's text content. * Provide a custom value to control what text is searchable. */ keywords?: string; }); type FilterNames> = (keyof FiltersSchemas & string)[]; type NamespaceSelection = { /** * A namespace is an isolated search space - no search can access entities * in other namespaces. Often this is used to segment user documents from * each other, but can be an arbitrary delineation. All filters apply * within a namespace. */ namespace: string; } | { /** * The namespaceId, which is returned when creating a namespace * or looking it up. * There can be multiple namespaceIds for the same namespace, e.g. * one for each modelId, embedding dimension, and filterNames. * Each of them have a separate "status" and only one is ever "ready" for * any given "namespace" (e.g. a userId). */ namespaceId: NamespaceId; }; type EntryArgs, EntryMetadata extends Record> = { /** * This key allows replacing an existing entry by key. * Within a namespace, there will only be one "ready" entry per key. * When adding a new one, it will start as "pending" and after all * chunks are added, it will be promoted to "ready". */ key?: string | undefined; /** * The title of the entry. Used for default prompting to contextualize * the entry results. Also may be used for keyword search in the future. */ title?: string; /** * Metadata about the entry that is not indexed or filtered or searched. * Provided as a convenience to store associated information, such as * the storageId or url to the source material. */ metadata?: EntryMetadata; /** * Filters to apply to the entry. These can be OR'd together in search. * To represent AND logic, your filter can be an object or array with * multiple values. e.g. saving the result with: * `{ name: "categoryAndPriority", value: ["articles", "high"] }` * and searching with the same value will return entries that match that * value exactly. */ filterValues?: EntryFilter[]; /** * The importance of the entry. This is used to scale the vector search * score of each chunk. */ importance?: Importance; /** * The hash of the entry contents. This is used to deduplicate entries. * You can look up existing entries by content hash within a namespace. * It will also return an existing entry if you add an entry with the * same content hash. */ contentHash?: string; /** * A function that is called when the entry is added. */ onComplete?: OnComplete; }; type SearchOptions> = { /** * Filters to apply to the search. These are OR'd together. To represent * AND logic, your filter can be an object or array with multiple values. * e.g. `[{ category: "articles" }, { priority: "high" }]` will return * entries that have "articles" category OR "high" priority. * `[{ category_priority: ["articles", "high"] }]` will return * entries that have "articles" category AND "high" priority. * This requires inserting the entries with these filter values exactly. * e.g. if you insert a entry with * `{ team_user: { team: "team1", user: "user1" } }`, it will not match * `{ team_user: { team: "team1" } }` but it will match */ filters?: EntryFilter[]; /** * The maximum number of messages to fetch. Default is 10. * This is the number *before* the chunkContext is applied. * e.g. { before: 2, after: 1 } means 4x the limit is returned. */ limit?: number; /** * What chunks around the search results to include. * Default: { before: 0, after: 0 } * e.g. { before: 2, after: 1 } means 2 chunks before + 1 chunk after. * If `chunk4` was the only result, the results returned would be: * `[{ content: [chunk2, chunk3, chunk4, chunk5], score, ... }]` * The results don't overlap, and bias toward giving "before" context. * So if `chunk7` was also a result, the results returned would be: * `[ * { content: [chunk2, chunk3, chunk4], score, ... } * { content: [chunk5, chunk6, chunk7, chunk8], score, ... }, * ]` */ chunkContext?: { before: number; after: number; }; /** * The minimum score to return a result. */ vectorScoreThreshold?: number; /** * The search mode to use. * - "vector": Vector similarity search only (default). Returns cosine * similarity scores. * - "text": Full-text search only. No embedding is computed. Returns * position-based scores. * - "hybrid": Combines vector and full-text search using Reciprocal Rank * Fusion. Returns position-based scores (1.0 for top result, decreasing * linearly). * * Text and hybrid modes require the query to be a string (not an embedding * array). */ searchType?: SearchType; /** * Weight for text search results in hybrid ranking (RRF). * Higher values give more influence to text search matches. * Only used when searchType is "hybrid". * Default: 1 */ textWeight?: number; /** * Weight for vector search results in hybrid ranking (RRF). * Higher values give more influence to vector search matches. * Only used when searchType is "hybrid". * Default: 1 */ vectorWeight?: number; }; export type ModelOrMetadata = string | ({ provider: string; } & ({ modelId: string; } | { model: string; })); export declare function getModelId(embeddingModel: ModelOrMetadata): string; export declare function getProviderName(embeddingModel: ModelOrMetadata): string; type CtxWith = Pick<{ runQuery: >(query: Query, args: FunctionArgs) => Promise>; runMutation: >(mutation: Mutation, args: FunctionArgs) => Promise>; runAction: >(action: Action, args: FunctionArgs) => Promise>; }, T>; //# sourceMappingURL=index.d.ts.map