@lunora/ai 1.0.0-alpha.2 → 1.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +6 -0
- package/README.md +2 -0
- package/dist/index.d.mts +26 -28
- package/dist/index.d.ts +26 -28
- package/dist/index.mjs +2 -2
- package/dist/packem_shared/bm25LexicalStore-BQzWLMqX.mjs +117 -0
- package/dist/packem_shared/contentHash-Cgz5KRGD.mjs +92 -0
- package/dist/packem_shared/{createAi-CaLhvwAd.mjs → createAi-CYxQuvEL.mjs} +14 -8
- package/dist/packem_shared/defineRag-CQ3pnKUh.mjs +471 -0
- package/dist/packem_shared/fixedWindowChunks-J-WfQDw9.mjs +26 -0
- package/dist/packem_shared/hybridRank-DPC9c2ON.mjs +20 -0
- package/dist/packem_shared/types.d-BXCiRv1x.d.mts +109 -0
- package/dist/packem_shared/types.d-BXCiRv1x.d.ts +109 -0
- package/dist/rag/index.d.mts +550 -0
- package/dist/rag/index.d.ts +550 -0
- package/dist/rag/index.mjs +5 -0
- package/package.json +12 -5
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { EmbeddingModel, LanguageModel } from 'ai';
|
|
2
|
+
/**
|
|
3
|
+
* Structural projection of the Cloudflare Workers `AI` binding (`env.AI`).
|
|
4
|
+
* Declared locally so unit tests can pass a plain-object double and the real
|
|
5
|
+
* binding satisfies the same shape without importing `@cloudflare/workers-types`
|
|
6
|
+
* into the public surface. Mirrors the `run` method documented at
|
|
7
|
+
* https://developers.cloudflare.com/workers-ai/.
|
|
8
|
+
* @experimental
|
|
9
|
+
*/
|
|
10
|
+
interface AiBindingLike {
|
|
11
|
+
run: (model: string, inputs: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A Workers AI provider instance — the value returned by `createWorkersAI(...)`.
|
|
15
|
+
* Calling it with a model id yields an AI SDK {@link LanguageModel}; the
|
|
16
|
+
* optional `textEmbeddingModel` factory yields an {@link EmbeddingModel}.
|
|
17
|
+
* Typed structurally so `@lunora/ai` neither re-declares the provider's full
|
|
18
|
+
* surface nor hard-pins its exact type across minor releases.
|
|
19
|
+
* @experimental
|
|
20
|
+
*/
|
|
21
|
+
interface WorkersAiProviderLike {
|
|
22
|
+
(modelId: string, settings?: Record<string, unknown>): LanguageModel;
|
|
23
|
+
textEmbeddingModel?: (modelId: string) => EmbeddingModel;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* AI Gateway options forwarded to `createWorkersAI`. Lets inference route
|
|
27
|
+
* through a Cloudflare AI Gateway for caching, rate-limiting, and observability.
|
|
28
|
+
* @experimental
|
|
29
|
+
*/
|
|
30
|
+
interface AiGatewayOptions {
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
id: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* `LunoraAiOptions` is part of the experimental `@lunora/ai` API and may change without a major version bump.
|
|
36
|
+
* @experimental
|
|
37
|
+
*/
|
|
38
|
+
interface LunoraAiOptions {
|
|
39
|
+
/**
|
|
40
|
+
* The Workers `AI` binding (`env.AI`). Required for the zero-config Workers
|
|
41
|
+
* AI default and for the raw `ai.run(...)` passthrough. May be omitted when
|
|
42
|
+
* a pre-built `provider` is supplied (e.g. in tests or a custom setup).
|
|
43
|
+
*/
|
|
44
|
+
binding?: AiBindingLike;
|
|
45
|
+
/**
|
|
46
|
+
* Default Workers AI **embedding** model id used by `embeddingModel()` when no
|
|
47
|
+
* explicit model is passed (e.g. `@cf/baai/bge-base-en-v1.5`). Kept separate
|
|
48
|
+
* from `defaultModel` because a language-model id and an embedding-model
|
|
49
|
+
* id belong to different Workers AI families and are never interchangeable —
|
|
50
|
+
* reusing the language-model default here would defer a wrong-family error to
|
|
51
|
+
* inference time. Has no effect on bring-your-own providers.
|
|
52
|
+
*/
|
|
53
|
+
defaultEmbeddingModel?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Default Workers AI **language** model id used by `model()` when no explicit
|
|
56
|
+
* model is passed. For embeddings, set `defaultEmbeddingModel` instead.
|
|
57
|
+
* Has no effect on bring-your-own providers.
|
|
58
|
+
*/
|
|
59
|
+
defaultModel?: string;
|
|
60
|
+
/** Route Workers AI inference through a Cloudflare AI Gateway. */
|
|
61
|
+
gateway?: AiGatewayOptions;
|
|
62
|
+
/**
|
|
63
|
+
* Pre-built Workers AI provider. When omitted, one is constructed from
|
|
64
|
+
* `binding` via `createWorkersAI`. Supplying it directly is the seam used by
|
|
65
|
+
* tests and advanced setups; it also lets callers configure the provider
|
|
66
|
+
* (e.g. `safePrompt`) before handing it to `@lunora/ai`.
|
|
67
|
+
*/
|
|
68
|
+
provider?: WorkersAiProviderLike;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A model to run against. The AI SDK's {@link LanguageModel} already admits a
|
|
72
|
+
* bare `string`, so this alias covers both arms of the provider-agnostic seam:
|
|
73
|
+
* a string id is the Workers AI convenience path (resolved by `ctx.ai.model`),
|
|
74
|
+
* a built model object is bring-your-own (`@ai-sdk/openai`, `@ai-sdk/anthropic`,
|
|
75
|
+
* `@ai-sdk/google`, OpenRouter, …).
|
|
76
|
+
* @experimental
|
|
77
|
+
*/
|
|
78
|
+
type ModelInput = LanguageModel;
|
|
79
|
+
/**
|
|
80
|
+
* Likewise for embeddings: a Workers AI embedding model id (e.g.
|
|
81
|
+
* `@cf/baai/bge-base-en-v1.5`) or any AI SDK {@link EmbeddingModel}.
|
|
82
|
+
* @experimental
|
|
83
|
+
*/
|
|
84
|
+
type EmbeddingModelInput = EmbeddingModel | string;
|
|
85
|
+
/**
|
|
86
|
+
* The `ctx.ai` surface. `model`/`embeddingModel` resolve a Workers AI model from
|
|
87
|
+
* a string (the default provider) and pass any non-string model straight through,
|
|
88
|
+
* so both accept Workers AI and bring-your-own providers. Feed the resolved model
|
|
89
|
+
* to the AI SDK functions re-exported from `@lunora/ai` (`generateText`,
|
|
90
|
+
* `streamText`, `generateObject`, `embed`, …); `run` is the raw binding escape
|
|
91
|
+
* hatch, and `workersai` is the underlying provider for direct model access.
|
|
92
|
+
* @experimental
|
|
93
|
+
*/
|
|
94
|
+
interface LunoraAi {
|
|
95
|
+
/** Resolve an {@link EmbeddingModel}: a string → Workers AI, an object → passthrough. */
|
|
96
|
+
embeddingModel: (model?: EmbeddingModelInput) => EmbeddingModel;
|
|
97
|
+
/** Resolve a {@link LanguageModel}: a string → Workers AI, an object → passthrough. */
|
|
98
|
+
model: (model?: ModelInput) => LanguageModel;
|
|
99
|
+
/**
|
|
100
|
+
* Raw Workers AI binding passthrough (void-style `ai.run`). Bypasses the AI
|
|
101
|
+
* SDK entirely — useful for Workers-AI-only model families (image, ASR,
|
|
102
|
+
* translation) not surfaced through the provider. Throws if no binding was
|
|
103
|
+
* supplied.
|
|
104
|
+
*/
|
|
105
|
+
run: (model: string, inputs: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
|
|
106
|
+
/** The underlying Workers AI provider — `ai.workersai("@cf/...")` for a raw model. */
|
|
107
|
+
workersai: WorkersAiProviderLike;
|
|
108
|
+
}
|
|
109
|
+
export { AiBindingLike as A, EmbeddingModelInput as E, LunoraAiOptions as L, ModelInput as M, WorkersAiProviderLike as W, LunoraAi as a, AiGatewayOptions as b };
|
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import { Tool } from 'ai';
|
|
2
|
+
import { E as EmbeddingModelInput, a as LunoraAi } from "../packem_shared/types.d-BXCiRv1x.mjs";
|
|
3
|
+
/**
|
|
4
|
+
* Built-in fixed-window chunker: split into `size`-char windows overlapping by
|
|
5
|
+
* `overlap` chars. Deliberately simple and deterministic — the zero-config
|
|
6
|
+
* default. Token-aware / sentence / semantic strategies plug in via
|
|
7
|
+
* `RagConfig.chunk`.
|
|
8
|
+
* @experimental
|
|
9
|
+
*/
|
|
10
|
+
declare const fixedWindowChunks: (text: string, size: number, overlap: number) => ReadonlyArray<string>;
|
|
11
|
+
/**
|
|
12
|
+
* `(text) => vector` — the embedder shape `ctx.vectors` accepts on both its
|
|
13
|
+
* write (`upsert`) and read (`query`) inputs. Matches `@lunora/server`'s
|
|
14
|
+
* `VectorEmbedder` and `@lunora/bindings/vectors`' `EmbedFunction<string>`.
|
|
15
|
+
* @experimental
|
|
16
|
+
*/
|
|
17
|
+
type RagEmbedder = (input: string) => Promise<ReadonlyArray<number>> | ReadonlyArray<number>;
|
|
18
|
+
/**
|
|
19
|
+
* `RagVectorMatch` is part of the experimental `@lunora/ai` API and may change without a major version bump.
|
|
20
|
+
* @experimental
|
|
21
|
+
*/
|
|
22
|
+
interface RagVectorMatch {
|
|
23
|
+
id: string;
|
|
24
|
+
metadata?: Record<string, unknown>;
|
|
25
|
+
score: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* `RagVectorMatches` is part of the experimental `@lunora/ai` API and may change without a major version bump.
|
|
29
|
+
* @experimental
|
|
30
|
+
*/
|
|
31
|
+
interface RagVectorMatches {
|
|
32
|
+
count: number;
|
|
33
|
+
matches: ReadonlyArray<RagVectorMatch>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `RagVectorQueryInput` is part of the experimental `@lunora/ai` API and may change without a major version bump.
|
|
37
|
+
* @experimental
|
|
38
|
+
*/
|
|
39
|
+
interface RagVectorQueryInput {
|
|
40
|
+
/** Embedder used to vectorize `input`. */
|
|
41
|
+
embed?: RagEmbedder;
|
|
42
|
+
filter?: Record<string, unknown>;
|
|
43
|
+
/** Natural-language query text, embedded via `embed`. */
|
|
44
|
+
input?: string;
|
|
45
|
+
namespace?: string;
|
|
46
|
+
/**
|
|
47
|
+
* How much stored metadata to return on matches. The runtime honours it even
|
|
48
|
+
* though `@lunora/server`'s ctx type does not declare it — the helper relies
|
|
49
|
+
* on it to read chunk text back in metadata mode.
|
|
50
|
+
*/
|
|
51
|
+
returnMetadata?: "all" | "indexed" | "none";
|
|
52
|
+
topK?: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* `RagVectorRecord` is part of the experimental `@lunora/ai` API and may change without a major version bump.
|
|
56
|
+
* @experimental
|
|
57
|
+
*/
|
|
58
|
+
interface RagVectorRecord {
|
|
59
|
+
id: string;
|
|
60
|
+
metadata?: Record<string, unknown>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* `RagVectorUpsertInput` is part of the experimental `@lunora/ai` API and may change without a major version bump.
|
|
64
|
+
* @experimental
|
|
65
|
+
*/
|
|
66
|
+
interface RagVectorUpsertInput {
|
|
67
|
+
/** Embedder used to vectorize `input`. Optional — omitted for text-search indexes. */
|
|
68
|
+
embed?: RagEmbedder;
|
|
69
|
+
id: string;
|
|
70
|
+
input: string;
|
|
71
|
+
metadata?: Record<string, unknown>;
|
|
72
|
+
namespace?: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Structural subset of the vector surface the RAG helper needs. Both the
|
|
76
|
+
* `ctx.vectors` facade on Mutation/Action ctx (`@lunora/server`'s
|
|
77
|
+
* `VectorSearch`) and the raw `@lunora/bindings/vectors` `LunoraVectors`
|
|
78
|
+
* satisfy it — declared here so `@lunora/ai` depends on neither package.
|
|
79
|
+
* @experimental
|
|
80
|
+
*/
|
|
81
|
+
interface RagVectors {
|
|
82
|
+
deleteByIds: (indexName: string, ids: ReadonlyArray<string>, namespace?: string) => Promise<unknown>;
|
|
83
|
+
getByIds: (indexName: string, ids: ReadonlyArray<string>, namespace?: string) => Promise<ReadonlyArray<RagVectorRecord>>;
|
|
84
|
+
query: (indexName: string, input: RagVectorQueryInput) => Promise<RagVectorMatches>;
|
|
85
|
+
upsert: (indexName: string, input: RagVectorUpsertInput) => Promise<unknown>;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The two facades `defineRag` binds. An `ActionCtx` satisfies this directly
|
|
89
|
+
* (`ctx.ai` is action-only, so RAG methods run inside actions); any object
|
|
90
|
+
* carrying the two facades works in tests.
|
|
91
|
+
* @experimental
|
|
92
|
+
*/
|
|
93
|
+
interface RagContext {
|
|
94
|
+
/**
|
|
95
|
+
* Resolves a Workers AI embedding-model id (or the omitted default) — an
|
|
96
|
+
* `ActionCtx`'s `ctx.ai` satisfies it. OPTIONAL: when
|
|
97
|
+
* {@link RagConfig.embeddingModel} is a direct AI SDK `EmbeddingModel` object
|
|
98
|
+
* (bring-your-own embeddings, e.g. `@ai-sdk/openai`), the helper uses that
|
|
99
|
+
* object as-is and never reads `ai`, so a hand-built context may omit it and
|
|
100
|
+
* no `env.AI` binding is needed. A model-id string (or an omitted model) with
|
|
101
|
+
* no `ai` present throws a directed error.
|
|
102
|
+
*/
|
|
103
|
+
ai?: Pick<LunoraAi, "embeddingModel">;
|
|
104
|
+
/**
|
|
105
|
+
* The verified retrieval identity, read by {@link RagConfig.rlsFilter} to
|
|
106
|
+
* derive a per-request row filter. An `ActionCtx` carrying `ctx.auth`
|
|
107
|
+
* satisfies this structurally, so `docs(ctx)` picks the identity up
|
|
108
|
+
* automatically; tests pass any value. `unknown` on purpose — `@lunora/ai`
|
|
109
|
+
* stays decoupled from `@lunora/server`'s identity type; `rlsFilter` narrows.
|
|
110
|
+
*/
|
|
111
|
+
auth?: unknown;
|
|
112
|
+
/**
|
|
113
|
+
* Optional `ctx.trace` span factory — an `ActionCtx`'s `ctx.trace` satisfies
|
|
114
|
+
* it structurally. When present, `defineRag` wraps each embedding-model
|
|
115
|
+
* call in a `generation` span carrying `gen_ai.operation.name: "embeddings"`
|
|
116
|
+
* and `gen_ai.request.model` up front, plus — attached post-hoc through the
|
|
117
|
+
* span handle the tracer hands the body — `gen_ai.usage.input_tokens` (from
|
|
118
|
+
* the embed result's token usage) and `gen_ai.usage.cost` (probed from the
|
|
119
|
+
* embed result's provider metadata, e.g. AI Gateway) when those are present.
|
|
120
|
+
* So the embed shows up on the trace waterfall with its usage like any other
|
|
121
|
+
* instrumented model call. `unknown` on purpose — the same decoupling
|
|
122
|
+
* rationale as `auth`: `defineRag` narrows it to a callable and runs embeds
|
|
123
|
+
* untraced when it is absent (a hand-built context / test).
|
|
124
|
+
*/
|
|
125
|
+
trace?: unknown;
|
|
126
|
+
vectors: RagVectors;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Pluggable chunk-text storage. By default chunk text is stored in vector
|
|
130
|
+
* metadata (`__ragText`), which forces `returnMetadata: "all"` on retrieval and
|
|
131
|
+
* caps `topK` at 20 (the Vectorize full-metadata ceiling) — and each vector's
|
|
132
|
+
* metadata must stay under the ~10 KiB Vectorize cap. Supplying a text store
|
|
133
|
+
* (a DO table, KV, …) moves the text out of metadata: retrieval queries with
|
|
134
|
+
* `returnMetadata: "indexed"` (topK up to 100) and hydrates text by chunk id.
|
|
135
|
+
* @experimental
|
|
136
|
+
*/
|
|
137
|
+
interface RagTextStore {
|
|
138
|
+
/** Fetch chunk texts by id, aligned with the input order; `undefined` for misses. */
|
|
139
|
+
getMany: (ids: ReadonlyArray<string>, options: {
|
|
140
|
+
namespace?: string;
|
|
141
|
+
}) => Promise<ReadonlyArray<string | undefined>>;
|
|
142
|
+
/** Persist chunk texts. Must be idempotent by chunk `id` (re-index re-puts). */
|
|
143
|
+
put: (chunks: ReadonlyArray<StoredRagChunk>, options: {
|
|
144
|
+
namespace?: string;
|
|
145
|
+
}) => Promise<void>;
|
|
146
|
+
/** Optional cleanup hook, invoked when a source's chunks are deleted. */
|
|
147
|
+
remove?: (ids: ReadonlyArray<string>, options: {
|
|
148
|
+
namespace?: string;
|
|
149
|
+
}) => Promise<void>;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A chunk handed to {@link RagTextStore.put} / {@link RagLexicalStore.index}.
|
|
153
|
+
* @experimental
|
|
154
|
+
*/
|
|
155
|
+
interface StoredRagChunk {
|
|
156
|
+
chunkIndex: number;
|
|
157
|
+
id: string;
|
|
158
|
+
sourceId: string;
|
|
159
|
+
text: string;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* One lexical (BM25) hit returned by {@link RagLexicalStore.search}.
|
|
163
|
+
* @experimental
|
|
164
|
+
*/
|
|
165
|
+
interface LexicalMatch {
|
|
166
|
+
/** The chunk vector id — the same id scheme the vector leg uses, so RRF can fuse the two. */
|
|
167
|
+
id: string;
|
|
168
|
+
/** BM25 relevance score (higher = better). Used only for the leg's internal ranking; RRF fuses by rank. */
|
|
169
|
+
score: number;
|
|
170
|
+
/** The chunk text, returned so a fused lexical-only hit needs no extra hydration round-trip. */
|
|
171
|
+
text: string;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Pluggable lexical (BM25 / keyword) store — the production seam for hybrid
|
|
175
|
+
* retrieval. When {@link RagConfig.lexicalStore} is set, `index()` mirrors each
|
|
176
|
+
* chunk's text here and `retrieve()` fuses this store's keyword ranking with the
|
|
177
|
+
* vector store's semantic ranking via Reciprocal Rank Fusion. Mirrors the
|
|
178
|
+
* {@link RagTextStore} shape (idempotent by chunk `id`, namespace-partitioned).
|
|
179
|
+
*
|
|
180
|
+
* `@lunora/ai/rag` ships `bm25LexicalStore()`, an in-memory reference adapter;
|
|
181
|
+
* production deployments plug a durable one (DO SQLite inverted index, D1,
|
|
182
|
+
* Vectorize-adjacent search service, …) behind this same interface.
|
|
183
|
+
* @experimental
|
|
184
|
+
*/
|
|
185
|
+
interface RagLexicalStore {
|
|
186
|
+
/** Index chunk texts for keyword search. Must be idempotent by chunk `id` (re-index re-puts). */
|
|
187
|
+
index: (chunks: ReadonlyArray<StoredRagChunk>, options: {
|
|
188
|
+
namespace?: string;
|
|
189
|
+
}) => Promise<void>;
|
|
190
|
+
/** Optional cleanup hook, invoked when a source's chunks are deleted or a re-index shrinks it. */
|
|
191
|
+
remove?: (ids: ReadonlyArray<string>, options: {
|
|
192
|
+
namespace?: string;
|
|
193
|
+
}) => Promise<void>;
|
|
194
|
+
/**
|
|
195
|
+
* Rank chunks by lexical relevance to `query`. `filter` carries the same
|
|
196
|
+
* (RLS-merged) metadata predicate handed to the vector leg — a store that
|
|
197
|
+
* indexes metadata MUST honour it so hybrid retrieval can't surface a row
|
|
198
|
+
* the RLS filter would exclude; a namespace-only store (the reference
|
|
199
|
+
* adapter) isolates by `namespace` and documents that it ignores `filter`.
|
|
200
|
+
*/
|
|
201
|
+
search: (query: string, options: {
|
|
202
|
+
filter?: Record<string, unknown>;
|
|
203
|
+
namespace?: string;
|
|
204
|
+
topK: number;
|
|
205
|
+
}) => Promise<ReadonlyArray<LexicalMatch>>;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* A pre-defined, reusable filter expression. Declared on `RagConfig.filters`
|
|
209
|
+
* (keyed by name) and referenced by name from `RetrieveOptions.filter` — avoids
|
|
210
|
+
* repeating the same tenant/RBAC filter shape across every retrieval site.
|
|
211
|
+
* @example
|
|
212
|
+
* ```ts
|
|
213
|
+
* const docs = defineRag({
|
|
214
|
+
* index: "docs",
|
|
215
|
+
* filters: {
|
|
216
|
+
* published: { filter: { status: "published", deleted: false }, description: "Only published content" },
|
|
217
|
+
* },
|
|
218
|
+
* });
|
|
219
|
+
* // Later — reference by name:
|
|
220
|
+
* docs(ctx).retrieve("query", { filter: "published" });
|
|
221
|
+
* ```
|
|
222
|
+
* @experimental
|
|
223
|
+
*/
|
|
224
|
+
interface RagNamedFilter {
|
|
225
|
+
/** Optional human-readable description for observability / Studio display. */
|
|
226
|
+
description?: string;
|
|
227
|
+
/** The filter expression passed verbatim to Vectorize's `filter` parameter. */
|
|
228
|
+
filter: Record<string, unknown>;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* `RagConfig` is part of the experimental `@lunora/ai` API and may change without a major version bump.
|
|
232
|
+
* @experimental
|
|
233
|
+
*/
|
|
234
|
+
interface RagConfig {
|
|
235
|
+
/**
|
|
236
|
+
* Suppress the one-time dev warning emitted when `index`/`retrieve` run
|
|
237
|
+
* without a `namespace`. Only appropriate for genuinely single-tenant apps —
|
|
238
|
+
* Vectorize indexes are account-global, so a namespace-less index shares
|
|
239
|
+
* vectors across every tenant.
|
|
240
|
+
*/
|
|
241
|
+
allowSharedNamespace?: boolean;
|
|
242
|
+
/** Custom chunker; overrides the built-in fixed-window splitter. */
|
|
243
|
+
chunk?: (text: string) => ReadonlyArray<string>;
|
|
244
|
+
/** Overlap (chars) between adjacent chunks. Default 200. Must be < `chunkSize`. */
|
|
245
|
+
chunkOverlap?: number;
|
|
246
|
+
/** Target chunk size (chars). Default 1000. */
|
|
247
|
+
chunkSize?: number;
|
|
248
|
+
/**
|
|
249
|
+
* Embedding model, declared once so index + retrieve embed identically: a
|
|
250
|
+
* Workers AI id (e.g. `@cf/baai/bge-base-en-v1.5`) or any AI SDK
|
|
251
|
+
* `EmbeddingModel`. Falls back to `createAi`'s `defaultModel` when omitted.
|
|
252
|
+
*/
|
|
253
|
+
embeddingModel?: EmbeddingModelInput;
|
|
254
|
+
/**
|
|
255
|
+
* Embedding-model version tag — an opt-in discriminator that partitions the
|
|
256
|
+
* vector space so a model swap can never silently return garbage. Vectors
|
|
257
|
+
* embedded by one model live in a different space from another's, and
|
|
258
|
+
* querying across the two returns meaningless neighbours. When set, the tag
|
|
259
|
+
* is folded into the effective Vectorize namespace (and chunk-id prefix) of
|
|
260
|
+
* every index/retrieve/remove, so bumping it re-partitions cleanly: old
|
|
261
|
+
* vectors become unreachable to new queries (empty ≫ wrong) until sources
|
|
262
|
+
* are re-indexed under the new tag.
|
|
263
|
+
*
|
|
264
|
+
* Set + bump this whenever you change {@link RagConfig.embeddingModel} (or
|
|
265
|
+
* its dimensions). Opt-in and non-breaking — omitting it keeps the exact
|
|
266
|
+
* chunk-id/namespace scheme of un-versioned indexes. Must match
|
|
267
|
+
* `^[A-Za-z0-9._-]{1,40}$` (e.g. `"bge-v1.5"`, `"v2"`).
|
|
268
|
+
*/
|
|
269
|
+
embeddingModelVersion?: string;
|
|
270
|
+
/**
|
|
271
|
+
* Pre-defined named filter expressions. Each key is a filter name users
|
|
272
|
+
* pass through `RetrieveOptions.filter`. Throws at retrieve-time if the
|
|
273
|
+
* name is not found here — catches spelling mistakes early.
|
|
274
|
+
*/
|
|
275
|
+
filters?: Record<string, RagNamedFilter>;
|
|
276
|
+
/** The Vectorize index name (a `ctx.vectors` index binding key). */
|
|
277
|
+
index: string;
|
|
278
|
+
/**
|
|
279
|
+
* Pluggable lexical (BM25) store for hybrid retrieval. When set, `index()`
|
|
280
|
+
* mirrors chunk text into it and `retrieve()` fuses the vector (semantic)
|
|
281
|
+
* and lexical (keyword) rankings via Reciprocal Rank Fusion — recovering the
|
|
282
|
+
* exact-term / rare-token matches a pure-embedding search misses. Use the
|
|
283
|
+
* shipped `bm25LexicalStore()` reference adapter or plug your own durable
|
|
284
|
+
* one. See {@link RagLexicalStore}.
|
|
285
|
+
*/
|
|
286
|
+
lexicalStore?: RagLexicalStore;
|
|
287
|
+
/** Retrieval depth for the lexical leg of hybrid search. Defaults to the effective `topK`. */
|
|
288
|
+
lexicalTopK?: number;
|
|
289
|
+
/**
|
|
290
|
+
* Enforce tenant isolation: throw (instead of the one-time dev warning)
|
|
291
|
+
* when `index`/`retrieve`/`remove` run without a `namespace`. Recommended
|
|
292
|
+
* for every multi-tenant app — Vectorize indexes are account-global, and
|
|
293
|
+
* in metadata mode the leaked payload includes raw chunk text.
|
|
294
|
+
*/
|
|
295
|
+
requireNamespace?: boolean;
|
|
296
|
+
/**
|
|
297
|
+
* Row-level-security filter derived from the retrieval identity. Called once
|
|
298
|
+
* per `retrieve()` with {@link RagContext.auth} (the bound ctx's `auth`); the
|
|
299
|
+
* returned Vectorize metadata filter is merged over the caller's `filter`
|
|
300
|
+
* with **RLS keys winning** (a caller can never widen past what RLS allows),
|
|
301
|
+
* then applied to both the vector and the lexical legs. Return `undefined` to
|
|
302
|
+
* add no constraint (e.g. an admin identity). Runs on retrieval only —
|
|
303
|
+
* indexing is a trusted server path.
|
|
304
|
+
* @example
|
|
305
|
+
* ```ts
|
|
306
|
+
* const docs = defineRag({
|
|
307
|
+
* index: "docs",
|
|
308
|
+
* // only ever return the caller's own org, whatever else they ask for:
|
|
309
|
+
* rlsFilter: (auth) => ({ orgId: (auth as { orgId: string }).orgId }),
|
|
310
|
+
* });
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
313
|
+
rlsFilter?: (auth: unknown) => Promise<Record<string, unknown> | undefined> | Record<string, unknown> | undefined;
|
|
314
|
+
/** Chunk-text storage override — see {@link RagTextStore}. */
|
|
315
|
+
textStore?: RagTextStore;
|
|
316
|
+
/** Default retrieval depth. Default 5. Capped at 20 (metadata mode) / 100 (text-store mode). */
|
|
317
|
+
topK?: number;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* `IndexInput` is part of the experimental `@lunora/ai` API and may change without a major version bump.
|
|
321
|
+
* @experimental
|
|
322
|
+
*/
|
|
323
|
+
interface IndexInput {
|
|
324
|
+
/**
|
|
325
|
+
* When `false`, throws if the source text produces zero chunks (e.g. empty
|
|
326
|
+
* or whitespace-only text). Default `true` (silently produces zero chunks).
|
|
327
|
+
*/
|
|
328
|
+
allowEmptySources?: boolean;
|
|
329
|
+
/** Source document id — chunk ids derive from it as `${id}#${chunkIndex}`. */
|
|
330
|
+
id: string;
|
|
331
|
+
/**
|
|
332
|
+
* Relative weight in `[0, 1]` multiplied into this source's match scores at
|
|
333
|
+
* retrieval time (default 1). Lets canonical docs outrank incidental ones.
|
|
334
|
+
*/
|
|
335
|
+
importance?: number;
|
|
336
|
+
/** Source metadata copied onto every chunk vector (e.g. title, url). */
|
|
337
|
+
metadata?: Record<string, unknown>;
|
|
338
|
+
/** Tenant/shard key. Required for multi-tenant apps — Vectorize is account-global. */
|
|
339
|
+
namespace?: string;
|
|
340
|
+
/**
|
|
341
|
+
* Called after each chunk is successfully upserted. Useful for progress
|
|
342
|
+
* tracking during large indexing operations — e.g. updating a UI progress
|
|
343
|
+
* bar or logging per-chunk status.
|
|
344
|
+
*/
|
|
345
|
+
onChunk?: (info: {
|
|
346
|
+
chunkIndex: number;
|
|
347
|
+
id: string;
|
|
348
|
+
text: string;
|
|
349
|
+
total: number;
|
|
350
|
+
}) => void;
|
|
351
|
+
/** The document body to chunk + embed + upsert. */
|
|
352
|
+
text: string;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* `IndexResult` is part of the experimental `@lunora/ai` API and may change without a major version bump.
|
|
356
|
+
* @experimental
|
|
357
|
+
*/
|
|
358
|
+
interface IndexResult {
|
|
359
|
+
/** Number of chunks the source is indexed into. */
|
|
360
|
+
chunks: number;
|
|
361
|
+
/** The deterministic chunk vector ids, in chunk order. */
|
|
362
|
+
ids: ReadonlyArray<string>;
|
|
363
|
+
/**
|
|
364
|
+
* True when the source's content hash matched the previously indexed hash —
|
|
365
|
+
* chunking/embedding/upserts were skipped entirely (a no-op re-sync).
|
|
366
|
+
*/
|
|
367
|
+
unchanged: boolean;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* `RemoveInput` is part of the experimental `@lunora/ai` API and may change without a major version bump.
|
|
371
|
+
* @experimental
|
|
372
|
+
*/
|
|
373
|
+
interface RemoveInput {
|
|
374
|
+
/** The source document id whose chunks are removed. */
|
|
375
|
+
id: string;
|
|
376
|
+
namespace?: string;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* `RetrieveOptions` is part of the experimental `@lunora/ai` API and may change without a major version bump.
|
|
380
|
+
* @experimental
|
|
381
|
+
*/
|
|
382
|
+
interface RetrieveOptions {
|
|
383
|
+
/**
|
|
384
|
+
* Also return this many neighbouring chunks around each match (fetched by
|
|
385
|
+
* deterministic id, not re-queried) — "embed small, retrieve big". Neighbour
|
|
386
|
+
* text is stitched into the chunk's `text` in document order. Best combined
|
|
387
|
+
* with `chunkOverlap: 0`, since overlapping windows repeat boundary text.
|
|
388
|
+
*/
|
|
389
|
+
chunkContext?: {
|
|
390
|
+
after?: number;
|
|
391
|
+
before?: number;
|
|
392
|
+
};
|
|
393
|
+
/**
|
|
394
|
+
* Vectorize filter expression — or the name of a pre-defined filter declared
|
|
395
|
+
* in `RagConfig.filters`. Passing a name that is not registered throws at
|
|
396
|
+
* call time, catching spelling mistakes early.
|
|
397
|
+
*/
|
|
398
|
+
filter?: Record<string, unknown> | string;
|
|
399
|
+
/** Drop matches whose (importance-adjusted) score falls below this threshold. */
|
|
400
|
+
minScore?: number;
|
|
401
|
+
namespace?: string;
|
|
402
|
+
/**
|
|
403
|
+
* Fires after retrieval completes, before chunk expansion. Useful for
|
|
404
|
+
* observability — logging query latency, hit counts, etc.
|
|
405
|
+
*/
|
|
406
|
+
onRetrieve?: (info: {
|
|
407
|
+
matches: number;
|
|
408
|
+
query: string;
|
|
409
|
+
}) => void;
|
|
410
|
+
topK?: number;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* `RetrievedChunk` is part of the experimental `@lunora/ai` API and may change without a major version bump.
|
|
414
|
+
* @experimental
|
|
415
|
+
*/
|
|
416
|
+
interface RetrievedChunk {
|
|
417
|
+
chunkIndex: number;
|
|
418
|
+
id: string;
|
|
419
|
+
/**
|
|
420
|
+
* The source-level importance weight that was multiplied into this chunk's
|
|
421
|
+
* score. `1` when no importance was set at index time.
|
|
422
|
+
*/
|
|
423
|
+
importance: number;
|
|
424
|
+
/** Caller metadata stored on the vector (internal `__rag*` keys stripped). */
|
|
425
|
+
metadata?: Record<string, unknown>;
|
|
426
|
+
/** Cosine similarity, multiplied by the source's `importance` when one was set. */
|
|
427
|
+
score: number;
|
|
428
|
+
sourceId: string;
|
|
429
|
+
text: string;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* `RagSource` is part of the experimental `@lunora/ai` API and may change without a major version bump.
|
|
433
|
+
* @experimental
|
|
434
|
+
*/
|
|
435
|
+
interface RagSource {
|
|
436
|
+
id: string;
|
|
437
|
+
/** Caller metadata from the source's first-seen chunk (internal keys stripped). */
|
|
438
|
+
metadata?: Record<string, unknown>;
|
|
439
|
+
/**
|
|
440
|
+
* The source's importance weight (the `importance` value passed at index
|
|
441
|
+
* time, default 1), propagated so downstream consumers can factor it into
|
|
442
|
+
* their own ranking or UI.
|
|
443
|
+
*/
|
|
444
|
+
weight?: number;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* The retrieve return shape — designed so an agent memory step consumes it directly.
|
|
448
|
+
* @experimental
|
|
449
|
+
*/
|
|
450
|
+
interface RetrieveResult {
|
|
451
|
+
/** Ranked chunks (best first). */
|
|
452
|
+
chunks: ReadonlyArray<RetrievedChunk>;
|
|
453
|
+
/** Ready-to-inject prompt context: chunks joined under `[source:<id>#<n>]` headers. */
|
|
454
|
+
context: string;
|
|
455
|
+
/** Deduped source references, in first-seen (best) order. */
|
|
456
|
+
sources: ReadonlyArray<RagSource>;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* `RagToolOptions` is part of the experimental `@lunora/ai` API and may change without a major version bump.
|
|
460
|
+
* @experimental
|
|
461
|
+
*/
|
|
462
|
+
interface RagToolOptions {
|
|
463
|
+
/** Tool description shown to the model. Defaults to a search description naming the index. */
|
|
464
|
+
description?: string;
|
|
465
|
+
/** Namespace applied to every tool-invoked retrieval (the tenant key). */
|
|
466
|
+
namespace?: string;
|
|
467
|
+
/** Retrieval depth for tool-invoked retrievals. */
|
|
468
|
+
topK?: number;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* The per-request RAG surface returned by binding a ctx: `docs(ctx)`.
|
|
472
|
+
* @experimental
|
|
473
|
+
*/
|
|
474
|
+
interface Rag {
|
|
475
|
+
/**
|
|
476
|
+
* Expose `retrieve` as an AI SDK tool (for `generateText`/`streamText`
|
|
477
|
+
* `tools:` maps), so a model can decide to search the index itself.
|
|
478
|
+
*/
|
|
479
|
+
asTool: (options?: RagToolOptions) => Tool<{
|
|
480
|
+
query: string;
|
|
481
|
+
}, RetrieveResult>;
|
|
482
|
+
/**
|
|
483
|
+
* Chunk + embed + upsert one source document. Re-indexing the same `id` is
|
|
484
|
+
* an atomic-enough replace: unchanged content short-circuits via content
|
|
485
|
+
* hash, and stale chunks beyond the new count are deleted automatically.
|
|
486
|
+
*/
|
|
487
|
+
index: (input: IndexInput) => Promise<IndexResult>;
|
|
488
|
+
/** Delete every chunk of a previously indexed source. */
|
|
489
|
+
remove: (input: RemoveInput) => Promise<void>;
|
|
490
|
+
/** Embed the query and return ranked chunks + prompt-ready context. */
|
|
491
|
+
retrieve: (query: string, options?: RetrieveOptions) => Promise<RetrieveResult>;
|
|
492
|
+
}
|
|
493
|
+
declare const defineRag: (config: RagConfig) => ((context: RagContext) => Rag);
|
|
494
|
+
/**
|
|
495
|
+
* Guess a MIME type from a file extension. Lowercases and strips a leading `.`
|
|
496
|
+
* from `extension`; returns `"application/octet-stream"` for unknown extensions.
|
|
497
|
+
*
|
|
498
|
+
* Covers the broad set of extensions users are likely to encounter in a web /
|
|
499
|
+
* document-processing context — images, video, audio, office docs, PDF, text,
|
|
500
|
+
* archives, and source code. Follows the same approach as Convex's
|
|
501
|
+
* `guessMimeType` helper.
|
|
502
|
+
* @experimental
|
|
503
|
+
*/
|
|
504
|
+
declare const guessMimeTypeFromExtension: (extension: string) => string;
|
|
505
|
+
/**
|
|
506
|
+
* SHA-256 hex digest of binary data. Accepts a `BufferSource` (`ArrayBuffer` or
|
|
507
|
+
* `ArrayBufferView` such as `Uint8Array`). Useful for content-addressable
|
|
508
|
+
* storage — pair with `IndexInput.text` to detect duplicates across re-indexes.
|
|
509
|
+
* @experimental
|
|
510
|
+
*/
|
|
511
|
+
declare const contentHash: (data: BufferSource) => Promise<string>;
|
|
512
|
+
/**
|
|
513
|
+
* Reciprocal Rank Fusion (RRF): merge two ranked lists of chunks by their
|
|
514
|
+
* _rank position_ rather than their absolute scores, which are not comparable
|
|
515
|
+
* across different search methods (cosine vs BM25).
|
|
516
|
+
*
|
|
517
|
+
* Each result set contributes `1 / (k + rank)` to each chunk's fused score,
|
|
518
|
+
* where `rank` is 0-based position in the list. The constant `k` (default 60)
|
|
519
|
+
* dampens the influence of high ranks — the standard value from the RRF
|
|
520
|
+
* literature that works well across domains.
|
|
521
|
+
*
|
|
522
|
+
* The fused list is sorted descending by fused score. Ties are broken by
|
|
523
|
+
* preferring the chunk ranked higher in the vector search result (typically
|
|
524
|
+
* the more semantically accurate of the two methods).
|
|
525
|
+
*
|
|
526
|
+
* Callers MUST ensure every chunk in both lists carries a unique, comparable
|
|
527
|
+
* `id` — this is guaranteed by the chunk-id scheme `${sourceId}#${chunkIndex}`.
|
|
528
|
+
* @experimental
|
|
529
|
+
*/
|
|
530
|
+
declare const hybridRank: (vectorResults: ReadonlyArray<RetrievedChunk>, textResults: ReadonlyArray<RetrievedChunk>, k?: number) => ReadonlyArray<RetrievedChunk>;
|
|
531
|
+
/**
|
|
532
|
+
* An **in-memory** Okapi BM25 lexical store — the reference adapter behind
|
|
533
|
+
* `RagConfig.lexicalStore`, giving hybrid retrieval its keyword leg with zero
|
|
534
|
+
* infrastructure. State lives in the worker isolate: it is **not durable and not
|
|
535
|
+
* shared across isolates**, so it is intended for tests, local development, and
|
|
536
|
+
* single-isolate workloads. Production deployments plug a durable
|
|
537
|
+
* {@link RagLexicalStore} (a DO-SQLite inverted index, D1, or an external search
|
|
538
|
+
* service) behind the same seam.
|
|
539
|
+
*
|
|
540
|
+
* Tenant isolation is by `namespace` (each namespace keeps its own index). This
|
|
541
|
+
* store holds **no metadata**, so it cannot evaluate a metadata `filter`
|
|
542
|
+
* (including an `rlsFilter` result): when `search` is called with a non-empty
|
|
543
|
+
* filter it **fails closed** — returns no lexical hits and warns once — rather
|
|
544
|
+
* than risk surfacing a row the filter would exclude. If your RLS is
|
|
545
|
+
* metadata-based (not namespace-based) and you want a lexical leg, fold the RLS
|
|
546
|
+
* dimension into the `namespace` or plug a filter-aware store.
|
|
547
|
+
* @experimental
|
|
548
|
+
*/
|
|
549
|
+
declare const bm25LexicalStore: () => RagLexicalStore;
|
|
550
|
+
export { type IndexInput, type IndexResult, type LexicalMatch, type Rag, type RagConfig, type RagContext, type RagEmbedder, type RagLexicalStore, type RagNamedFilter, type RagSource, type RagTextStore, type RagToolOptions, type RagVectorMatch, type RagVectorMatches, type RagVectorQueryInput, type RagVectorRecord, type RagVectorUpsertInput, type RagVectors, type RemoveInput, type RetrieveOptions, type RetrieveResult, type RetrievedChunk, type StoredRagChunk, bm25LexicalStore, contentHash, defineRag, fixedWindowChunks, guessMimeTypeFromExtension, hybridRank };
|