@lunora/ai 1.0.0-alpha.13 → 1.0.0-alpha.14

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/dist/index.d.mts CHANGED
@@ -1,31 +1,6 @@
1
- import { EmbeddingModel, LanguageModel } from 'ai';
2
- export { type EmbeddingModel, type LanguageModel, embed, embedMany, generateObject, generateText, streamObject, streamText, tool } from 'ai';
1
+ import { L as LunoraAiOptions, a as LunoraAi } from "./packem_shared/types.d-C6dA8LCy.mjs";
2
+ export type { A as AiBindingLike, b as AiGatewayOptions, E as EmbeddingModelInput, M as ModelInput, W as WorkersAiProviderLike } from "./packem_shared/types.d-C6dA8LCy.mjs";
3
+ export { type EmbeddingModel, type LanguageModel, embed, embedMany, generateObject, generateText, hasToolCall, jsonSchema, streamObject, streamText, tool } from 'ai';
3
4
  export { createWorkersAI } from 'workers-ai-provider';
4
- interface AiBindingLike {
5
- run: (model: string, inputs: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
6
- }
7
- interface WorkersAiProviderLike {
8
- (modelId: string, settings?: Record<string, unknown>): LanguageModel;
9
- textEmbeddingModel?: (modelId: string) => EmbeddingModel;
10
- }
11
- interface AiGatewayOptions {
12
- [key: string]: unknown;
13
- id: string;
14
- }
15
- interface LunoraAiOptions {
16
- binding?: AiBindingLike;
17
- defaultEmbeddingModel?: string;
18
- defaultModel?: string;
19
- gateway?: AiGatewayOptions;
20
- provider?: WorkersAiProviderLike;
21
- }
22
- type ModelInput = LanguageModel;
23
- type EmbeddingModelInput = EmbeddingModel | string;
24
- interface LunoraAi {
25
- embeddingModel: (model?: EmbeddingModelInput) => EmbeddingModel;
26
- model: (model?: ModelInput) => LanguageModel;
27
- run: (model: string, inputs: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
28
- workersai: WorkersAiProviderLike;
29
- }
30
5
  declare const createAi: (options: LunoraAiOptions) => LunoraAi;
31
- export { type AiBindingLike, type AiGatewayOptions, type EmbeddingModelInput, type LunoraAi, type LunoraAiOptions, type ModelInput, type WorkersAiProviderLike, createAi };
6
+ export { type LunoraAi, type LunoraAiOptions, createAi };
package/dist/index.d.ts CHANGED
@@ -1,31 +1,6 @@
1
- import { EmbeddingModel, LanguageModel } from 'ai';
2
- export { type EmbeddingModel, type LanguageModel, embed, embedMany, generateObject, generateText, streamObject, streamText, tool } from 'ai';
1
+ import { L as LunoraAiOptions, a as LunoraAi } from "./packem_shared/types.d-C6dA8LCy.js";
2
+ export type { A as AiBindingLike, b as AiGatewayOptions, E as EmbeddingModelInput, M as ModelInput, W as WorkersAiProviderLike } from "./packem_shared/types.d-C6dA8LCy.js";
3
+ export { type EmbeddingModel, type LanguageModel, embed, embedMany, generateObject, generateText, hasToolCall, jsonSchema, streamObject, streamText, tool } from 'ai';
3
4
  export { createWorkersAI } from 'workers-ai-provider';
4
- interface AiBindingLike {
5
- run: (model: string, inputs: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
6
- }
7
- interface WorkersAiProviderLike {
8
- (modelId: string, settings?: Record<string, unknown>): LanguageModel;
9
- textEmbeddingModel?: (modelId: string) => EmbeddingModel;
10
- }
11
- interface AiGatewayOptions {
12
- [key: string]: unknown;
13
- id: string;
14
- }
15
- interface LunoraAiOptions {
16
- binding?: AiBindingLike;
17
- defaultEmbeddingModel?: string;
18
- defaultModel?: string;
19
- gateway?: AiGatewayOptions;
20
- provider?: WorkersAiProviderLike;
21
- }
22
- type ModelInput = LanguageModel;
23
- type EmbeddingModelInput = EmbeddingModel | string;
24
- interface LunoraAi {
25
- embeddingModel: (model?: EmbeddingModelInput) => EmbeddingModel;
26
- model: (model?: ModelInput) => LanguageModel;
27
- run: (model: string, inputs: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
28
- workersai: WorkersAiProviderLike;
29
- }
30
5
  declare const createAi: (options: LunoraAiOptions) => LunoraAi;
31
- export { type AiBindingLike, type AiGatewayOptions, type EmbeddingModelInput, type LunoraAi, type LunoraAiOptions, type ModelInput, type WorkersAiProviderLike, createAi };
6
+ export { type LunoraAi, type LunoraAiOptions, createAi };
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  export { default as createAi } from './packem_shared/createAi-CYxQuvEL.mjs';
2
- export { embed, embedMany, generateObject, generateText, streamObject, streamText, tool } from 'ai';
2
+ export { embed, embedMany, generateObject, generateText, hasToolCall, jsonSchema, streamObject, streamText, tool } from 'ai';
3
3
  export { createWorkersAI } from 'workers-ai-provider';
@@ -0,0 +1,118 @@
1
+ const BM25_K1 = 1.5;
2
+ const BM25_B = 0.75;
3
+ const TOKEN_PATTERN = /[a-z0-9]+/g;
4
+ const tokenize = (text) => text.toLowerCase().match(TOKEN_PATTERN) ?? [];
5
+ const isPrimitiveFilter = (filter) => Object.values(filter).every((value) => value === null || typeof value !== "object");
6
+ const filterWarned = /* @__PURE__ */ new WeakSet();
7
+ const bm25LexicalStore = () => {
8
+ const namespaces = /* @__PURE__ */ new Map();
9
+ const stateFor = (namespace = "") => {
10
+ let state = namespaces.get(namespace);
11
+ if (!state) {
12
+ state = { documents: /* @__PURE__ */ new Map(), postings: /* @__PURE__ */ new Map(), totalLength: 0 };
13
+ namespaces.set(namespace, state);
14
+ }
15
+ return state;
16
+ };
17
+ const removeDocument = (namespace, id) => {
18
+ const state = stateFor(namespace);
19
+ const existing = state.documents.get(id);
20
+ if (!existing) {
21
+ return;
22
+ }
23
+ for (const term of existing.termFrequency.keys()) {
24
+ const posting = state.postings.get(term);
25
+ if (posting) {
26
+ posting.delete(id);
27
+ if (posting.size === 0) {
28
+ state.postings.delete(term);
29
+ }
30
+ }
31
+ }
32
+ state.totalLength -= existing.length;
33
+ state.documents.delete(id);
34
+ };
35
+ const store = {
36
+ index: (chunks, options) => {
37
+ const state = stateFor(options.namespace);
38
+ for (const chunk of chunks) {
39
+ removeDocument(options.namespace, chunk.id);
40
+ const tokens = tokenize(chunk.text);
41
+ if (tokens.length === 0) {
42
+ continue;
43
+ }
44
+ const termFrequency = /* @__PURE__ */ new Map();
45
+ for (const token of tokens) {
46
+ termFrequency.set(token, (termFrequency.get(token) ?? 0) + 1);
47
+ }
48
+ for (const [term, frequency] of termFrequency) {
49
+ let posting = state.postings.get(term);
50
+ if (!posting) {
51
+ posting = /* @__PURE__ */ new Map();
52
+ state.postings.set(term, posting);
53
+ }
54
+ posting.set(chunk.id, frequency);
55
+ }
56
+ state.documents.set(chunk.id, { length: tokens.length, termFrequency, text: chunk.text });
57
+ state.totalLength += tokens.length;
58
+ }
59
+ return Promise.resolve();
60
+ },
61
+ remove: (ids, options) => {
62
+ for (const id of ids) {
63
+ removeDocument(options.namespace, id);
64
+ }
65
+ return Promise.resolve();
66
+ },
67
+ search: (query, options) => {
68
+ if (options.filter && Object.keys(options.filter).length > 0 && !isPrimitiveFilter(options.filter)) {
69
+ if (!filterWarned.has(store)) {
70
+ filterWarned.add(store);
71
+ console.warn(
72
+ "[@lunora/ai/rag] bm25LexicalStore cannot evaluate a metadata filter (it stores no metadata);\nthe lexical leg is skipped for filtered queries. Fold the RLS dimension into `namespace`,\nor plug a filter-aware RagLexicalStore, to keep a lexical leg under metadata-based RLS."
73
+ );
74
+ }
75
+ return Promise.resolve([]);
76
+ }
77
+ const state = stateFor(options.namespace);
78
+ const documentCount = state.documents.size;
79
+ if (documentCount === 0) {
80
+ return Promise.resolve([]);
81
+ }
82
+ const queryTerms = [...new Set(tokenize(query))];
83
+ if (queryTerms.length === 0) {
84
+ return Promise.resolve([]);
85
+ }
86
+ const averageLength = state.totalLength / documentCount;
87
+ const scores = /* @__PURE__ */ new Map();
88
+ for (const term of queryTerms) {
89
+ const posting = state.postings.get(term);
90
+ if (!posting) {
91
+ continue;
92
+ }
93
+ const documentFrequency = posting.size;
94
+ const idf = Math.log(1 + (documentCount - documentFrequency + 0.5) / (documentFrequency + 0.5));
95
+ for (const [id, frequency] of posting) {
96
+ const document = state.documents.get(id);
97
+ if (!document) {
98
+ continue;
99
+ }
100
+ const denominator = frequency + BM25_K1 * (1 - BM25_B + BM25_B * document.length / averageLength);
101
+ const contribution = idf * (frequency * (BM25_K1 + 1) / denominator);
102
+ scores.set(id, (scores.get(id) ?? 0) + contribution);
103
+ }
104
+ }
105
+ const matches = [...scores.entries()].map(([id, score]) => {
106
+ return {
107
+ id,
108
+ score,
109
+ text: state.documents.get(id)?.text ?? ""
110
+ };
111
+ });
112
+ return Promise.resolve(matches.toSorted((a, b) => b.score - a.score).slice(0, options.topK));
113
+ }
114
+ };
115
+ return store;
116
+ };
117
+
118
+ export { bm25LexicalStore as default };
@@ -0,0 +1,92 @@
1
+ const DOT_PREFIX_RE = /^\.+/u;
2
+ const EXTENSION_MIME_MAP = {
3
+ // Images
4
+ avif: "image/avif",
5
+ bmp: "image/bmp",
6
+ gif: "image/gif",
7
+ ico: "image/x-icon",
8
+ jpeg: "image/jpeg",
9
+ jpg: "image/jpeg",
10
+ png: "image/png",
11
+ svg: "image/svg+xml",
12
+ tiff: "image/tiff",
13
+ tif: "image/tiff",
14
+ webp: "image/webp",
15
+ // Video
16
+ avi: "video/x-msvideo",
17
+ mkv: "video/x-matroska",
18
+ mov: "video/quicktime",
19
+ mp4: "video/mp4",
20
+ mpeg: "video/mpeg",
21
+ mpg: "video/mpeg",
22
+ webm: "video/webm",
23
+ wmv: "video/x-ms-wmv",
24
+ // Audio
25
+ aac: "audio/aac",
26
+ flac: "audio/flac",
27
+ m4a: "audio/mp4",
28
+ mp3: "audio/mpeg",
29
+ ogg: "audio/ogg",
30
+ opus: "audio/opus",
31
+ wav: "audio/wav",
32
+ wma: "audio/x-ms-wma",
33
+ // Documents (office / PDF)
34
+ csv: "text/csv",
35
+ doc: "application/msword",
36
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
37
+ odp: "application/vnd.oasis.opendocument.presentation",
38
+ ods: "application/vnd.oasis.opendocument.spreadsheet",
39
+ odt: "application/vnd.oasis.opendocument.text",
40
+ pdf: "application/pdf",
41
+ ppt: "application/vnd.ms-powerpoint",
42
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
43
+ rtf: "application/rtf",
44
+ xls: "application/vnd.ms-excel",
45
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
46
+ // Text / markup
47
+ css: "text/css",
48
+ html: "text/html",
49
+ htm: "text/html",
50
+ ini: "text/plain",
51
+ json: "application/json",
52
+ js: "text/javascript",
53
+ mjs: "text/javascript",
54
+ md: "text/markdown",
55
+ jsx: "text/javascript",
56
+ ts: "text/typescript",
57
+ tsx: "text/typescript",
58
+ txt: "text/plain",
59
+ xml: "application/xml",
60
+ yaml: "application/x-yaml",
61
+ yml: "application/x-yaml",
62
+ // Archives / binaries
63
+ "7z": "application/x-7z-compressed",
64
+ bz2: "application/x-bzip2",
65
+ gz: "application/gzip",
66
+ jar: "application/java-archive",
67
+ rar: "application/vnd.rar",
68
+ tar: "application/x-tar",
69
+ zip: "application/zip",
70
+ // Fonts
71
+ otf: "font/otf",
72
+ ttf: "font/ttf",
73
+ woff: "font/woff",
74
+ woff2: "font/woff2",
75
+ // Other common
76
+ bin: "application/octet-stream",
77
+ epub: "application/epub+zip",
78
+ exe: "application/vnd.microsoft.portable-executable",
79
+ iso: "application/x-iso9660-image",
80
+ sql: "application/sql",
81
+ toml: "application/toml"
82
+ };
83
+ const guessMimeTypeFromExtension = (extension) => {
84
+ const normalizedExtension = extension.replace(DOT_PREFIX_RE, "").toLowerCase();
85
+ return EXTENSION_MIME_MAP[normalizedExtension] ?? "application/octet-stream";
86
+ };
87
+ const contentHash = async (data) => {
88
+ const digest = await crypto.subtle.digest("SHA-256", data);
89
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
90
+ };
91
+
92
+ export { contentHash, guessMimeTypeFromExtension };
@@ -0,0 +1,402 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { tool, jsonSchema, embed } from 'ai';
3
+ import fixedWindowChunks from './fixedWindowChunks-J-WfQDw9.mjs';
4
+ import hybridRank from './hybridRank-DPC9c2ON.mjs';
5
+
6
+ const INDEX_CONCURRENCY = 8;
7
+ const concurrentMap = async (items, limit, function_) => {
8
+ if (!Number.isInteger(limit) || limit < 1) {
9
+ throw new RangeError("concurrentMap: `limit` must be a positive integer");
10
+ }
11
+ const results = Array.from({ length: items.length });
12
+ let cursor = 0;
13
+ const worker = async () => {
14
+ while (cursor < items.length) {
15
+ const index = cursor;
16
+ cursor += 1;
17
+ results[index] = await function_(items[index], index);
18
+ }
19
+ };
20
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
21
+ await Promise.all(workers);
22
+ return results;
23
+ };
24
+
25
+ const DEFAULT_CHUNK_SIZE = 1e3;
26
+ const DEFAULT_CHUNK_OVERLAP = 200;
27
+ const DEFAULT_TOP_K = 5;
28
+ const MAX_TOP_K_FULL_METADATA = 20;
29
+ const MAX_TOP_K = 100;
30
+ const CHUNK_INDEX_KEY = "__ragChunk";
31
+ const SOURCE_KEY = "__ragSource";
32
+ const TEXT_KEY = "__ragText";
33
+ const HASH_KEY = "__ragHash";
34
+ const COUNT_KEY = "__ragChunks";
35
+ const IMPORTANCE_KEY = "__ragImportance";
36
+ const MODEL_KEY = "__ragModel";
37
+ const INTERNAL_KEYS = /* @__PURE__ */ new Set([CHUNK_INDEX_KEY, COUNT_KEY, HASH_KEY, IMPORTANCE_KEY, MODEL_KEY, SOURCE_KEY, TEXT_KEY]);
38
+ const MODEL_VERSION_PATTERN = /^[\w.-]{1,40}$/;
39
+ const namespacePrefix = (namespace) => namespace === void 0 ? "" : `${encodeURIComponent(namespace)}#`;
40
+ const chunkVectorId = (namespace, sourceId, chunkIndex) => `${namespacePrefix(namespace)}${sourceId}#${String(chunkIndex)}`;
41
+ const parseChunkVectorId = (id, namespace) => {
42
+ const prefix = namespacePrefix(namespace);
43
+ const body = prefix !== "" && id.startsWith(prefix) ? id.slice(prefix.length) : id;
44
+ const separator = body.lastIndexOf("#");
45
+ const chunkIndex = separator === -1 ? Number.NaN : Number(body.slice(separator + 1));
46
+ if (separator === -1 || !Number.isInteger(chunkIndex) || chunkIndex < 0) {
47
+ return { chunkIndex: 0, sourceId: body };
48
+ }
49
+ return { chunkIndex, sourceId: body.slice(0, separator) };
50
+ };
51
+ const sha256Hex = async (text) => {
52
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
53
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
54
+ };
55
+ const userMetadataOf = (metadata) => {
56
+ if (!metadata) {
57
+ return void 0;
58
+ }
59
+ const entries = Object.entries(metadata).filter(([key]) => !INTERNAL_KEYS.has(key));
60
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
61
+ };
62
+ const sharedNamespaceWarned = /* @__PURE__ */ new Set();
63
+ const warnSharedNamespace = (indexName) => {
64
+ if (sharedNamespaceWarned.has(indexName)) {
65
+ return;
66
+ }
67
+ sharedNamespaceWarned.add(indexName);
68
+ console.warn(
69
+ `[@lunora/ai/rag] index "${indexName}" is used without a namespace — in a multi-tenant/sharded
70
+ app this shares one tenant's chunks (text included) with every other tenant, since
71
+ Vectorize indexes are account-global. Pass \`namespace\` (the shard/tenant key) on both
72
+ index() and retrieve(). Single-tenant apps suppress this via { allowSharedNamespace: true }.`
73
+ );
74
+ };
75
+ const assembleContext = (chunks) => chunks.map((chunk) => `[source:${chunk.sourceId}#${String(chunk.chunkIndex)}]
76
+ ${chunk.text}`).join("\n\n");
77
+ const resolveEmbeddingModel = (input, ai) => {
78
+ if (typeof input === "object") {
79
+ return input;
80
+ }
81
+ if (ai === void 0) {
82
+ throw new LunoraError(
83
+ "INTERNAL",
84
+ "@lunora/ai/rag: `embeddingModel` is a Workers AI model id (or omitted) but the bound context has no `ai` (env.AI). Pass an AI SDK EmbeddingModel object to embed without Workers AI, or bind a context whose `ctx.ai` is wired."
85
+ );
86
+ }
87
+ return ai.embeddingModel(input);
88
+ };
89
+ const defineRag = (config) => {
90
+ if (typeof config.index !== "string" || config.index.length === 0) {
91
+ throw new LunoraError("BAD_REQUEST", "@lunora/ai/rag: `index` must be a non-empty Vectorize index name");
92
+ }
93
+ const chunkSize = config.chunkSize ?? DEFAULT_CHUNK_SIZE;
94
+ const chunkOverlap = config.chunkOverlap ?? DEFAULT_CHUNK_OVERLAP;
95
+ if (!Number.isInteger(chunkSize) || chunkSize < 1) {
96
+ throw new LunoraError("BAD_REQUEST", "@lunora/ai/rag: `chunkSize` must be a positive integer");
97
+ }
98
+ if (!Number.isInteger(chunkOverlap) || chunkOverlap < 0 || chunkOverlap >= chunkSize) {
99
+ throw new LunoraError("BAD_REQUEST", "@lunora/ai/rag: `chunkOverlap` must be a non-negative integer smaller than `chunkSize`");
100
+ }
101
+ const defaultTopK = config.topK ?? DEFAULT_TOP_K;
102
+ if (!Number.isInteger(defaultTopK) || defaultTopK < 1) {
103
+ throw new LunoraError("BAD_REQUEST", "@lunora/ai/rag: `topK` must be a positive integer");
104
+ }
105
+ if (config.embeddingModelVersion !== void 0 && !MODEL_VERSION_PATTERN.test(config.embeddingModelVersion)) {
106
+ throw new LunoraError(
107
+ "BAD_REQUEST",
108
+ '@lunora/ai/rag: `embeddingModelVersion` must match /^[A-Za-z0-9._-]{1,40}$/ (a short, stable tag like "bge-v1.5")'
109
+ );
110
+ }
111
+ const splitter = config.chunk ?? ((text) => fixedWindowChunks(text, chunkSize, chunkOverlap));
112
+ const { textStore } = config;
113
+ const topKCeiling = textStore ? MAX_TOP_K : MAX_TOP_K_FULL_METADATA;
114
+ const modelTag = config.embeddingModelVersion;
115
+ const withModelTag = (namespace) => {
116
+ if (modelTag === void 0) {
117
+ return namespace;
118
+ }
119
+ return namespace === void 0 ? modelTag : `${modelTag}::${namespace}`;
120
+ };
121
+ return (context) => {
122
+ let model;
123
+ const embedText = async (text) => {
124
+ model ??= resolveEmbeddingModel(config.embeddingModel, context.ai);
125
+ const { embedding } = await embed({ model, value: text });
126
+ return embedding;
127
+ };
128
+ const checkNamespace = (namespace) => {
129
+ if (namespace !== void 0) {
130
+ return;
131
+ }
132
+ if (config.requireNamespace) {
133
+ throw new LunoraError(
134
+ "BAD_REQUEST",
135
+ `@lunora/ai/rag: index "${config.index}" requires a namespace (requireNamespace is set) — pass the tenant/shard key on index()/retrieve()/remove()`
136
+ );
137
+ }
138
+ if (!config.allowSharedNamespace) {
139
+ warnSharedNamespace(config.index);
140
+ }
141
+ };
142
+ const readHead = async (sourceId, namespace) => {
143
+ const [head] = await context.vectors.getByIds(config.index, [chunkVectorId(namespace, sourceId, 0)], namespace);
144
+ const hash = head?.metadata?.[HASH_KEY];
145
+ const chunks = head?.metadata?.[COUNT_KEY];
146
+ return {
147
+ chunks: typeof chunks === "number" && Number.isInteger(chunks) && chunks > 0 ? chunks : void 0,
148
+ hash: typeof hash === "string" ? hash : void 0
149
+ };
150
+ };
151
+ const deleteChunkRange = async (sourceId, from, to, namespace) => {
152
+ const ids = Array.from({ length: to - from }, (_, offset) => chunkVectorId(namespace, sourceId, from + offset));
153
+ if (ids.length === 0) {
154
+ return;
155
+ }
156
+ await context.vectors.deleteByIds(config.index, ids, namespace);
157
+ await textStore?.remove?.(ids, { namespace });
158
+ await config.lexicalStore?.remove?.(ids, { namespace });
159
+ };
160
+ const index = async (input) => {
161
+ checkNamespace(input.namespace);
162
+ if (input.importance !== void 0 && (typeof input.importance !== "number" || input.importance < 0 || input.importance > 1)) {
163
+ throw new LunoraError("BAD_REQUEST", "@lunora/ai/rag: `importance` must be a number in [0, 1]");
164
+ }
165
+ const effectiveNamespace = withModelTag(input.namespace);
166
+ const hash = await sha256Hex(input.text);
167
+ const previous = await readHead(input.id, effectiveNamespace);
168
+ if (previous.hash === hash && previous.chunks !== void 0) {
169
+ return {
170
+ chunks: previous.chunks,
171
+ ids: Array.from({ length: previous.chunks }, (_, chunkIndex) => chunkVectorId(effectiveNamespace, input.id, chunkIndex)),
172
+ unchanged: true
173
+ };
174
+ }
175
+ const pieces = splitter(input.text);
176
+ const ids = pieces.map((_, chunkIndex) => chunkVectorId(effectiveNamespace, input.id, chunkIndex));
177
+ if (pieces.length === 0 && input.allowEmptySources === false) {
178
+ throw new LunoraError("BAD_REQUEST", `@lunora/ai/rag: source "${input.id}" produced zero chunks — set allowEmptySources: true to allow this`);
179
+ }
180
+ if (pieces.length > 0) {
181
+ const storedChunks = pieces.map((text, chunkIndex) => {
182
+ return { chunkIndex, id: ids[chunkIndex], sourceId: input.id, text };
183
+ });
184
+ if (textStore) {
185
+ await textStore.put(storedChunks, { namespace: effectiveNamespace });
186
+ }
187
+ if (config.lexicalStore) {
188
+ await config.lexicalStore.index(storedChunks, { namespace: effectiveNamespace });
189
+ }
190
+ }
191
+ await concurrentMap(pieces, INDEX_CONCURRENCY, async (piece, chunkIndex) => {
192
+ const id = ids[chunkIndex];
193
+ const metadata = {
194
+ ...input.metadata,
195
+ [CHUNK_INDEX_KEY]: chunkIndex,
196
+ [SOURCE_KEY]: input.id
197
+ };
198
+ if (!textStore) {
199
+ metadata[TEXT_KEY] = piece;
200
+ }
201
+ if (input.importance !== void 0) {
202
+ metadata[IMPORTANCE_KEY] = input.importance;
203
+ }
204
+ if (chunkIndex === 0) {
205
+ metadata[HASH_KEY] = hash;
206
+ metadata[COUNT_KEY] = pieces.length;
207
+ if (modelTag !== void 0) {
208
+ metadata[MODEL_KEY] = modelTag;
209
+ }
210
+ }
211
+ await context.vectors.upsert(config.index, {
212
+ embed: embedText,
213
+ id,
214
+ input: piece,
215
+ metadata,
216
+ namespace: effectiveNamespace
217
+ });
218
+ input.onChunk?.({ chunkIndex, id, text: piece, total: pieces.length });
219
+ });
220
+ if (previous.chunks !== void 0 && previous.chunks > pieces.length) {
221
+ await deleteChunkRange(input.id, pieces.length, previous.chunks, effectiveNamespace);
222
+ }
223
+ return { chunks: pieces.length, ids, unchanged: false };
224
+ };
225
+ const remove = async (input) => {
226
+ checkNamespace(input.namespace);
227
+ const effectiveNamespace = withModelTag(input.namespace);
228
+ const previous = await readHead(input.id, effectiveNamespace);
229
+ const totalChunks = previous.chunks ?? 1;
230
+ await deleteChunkRange(input.id, 0, totalChunks, effectiveNamespace);
231
+ };
232
+ const textsByIds = async (ids, namespace) => {
233
+ const texts = /* @__PURE__ */ new Map();
234
+ if (ids.length === 0) {
235
+ return texts;
236
+ }
237
+ if (textStore) {
238
+ const stored = await textStore.getMany(ids, { namespace });
239
+ for (const [position, id] of ids.entries()) {
240
+ const text = stored[position];
241
+ if (typeof text === "string") {
242
+ texts.set(id, text);
243
+ }
244
+ }
245
+ return texts;
246
+ }
247
+ const records = await context.vectors.getByIds(config.index, ids, namespace);
248
+ for (const record of records) {
249
+ const text = record.metadata?.[TEXT_KEY];
250
+ if (typeof text === "string") {
251
+ texts.set(record.id, text);
252
+ }
253
+ }
254
+ return texts;
255
+ };
256
+ const expandChunks = async (chunks, options, effectiveNamespace) => {
257
+ const before = options?.chunkContext?.before ?? 0;
258
+ const after = options?.chunkContext?.after ?? 0;
259
+ if (before === 0 && after === 0) {
260
+ return chunks;
261
+ }
262
+ if (!Number.isInteger(before) || before < 0 || !Number.isInteger(after) || after < 0) {
263
+ throw new LunoraError("BAD_REQUEST", "@lunora/ai/rag: `chunkContext.before`/`chunkContext.after` must be non-negative integers");
264
+ }
265
+ const known = new Map(chunks.map((chunk) => [chunk.id, chunk.text]));
266
+ const neighbourIds = /* @__PURE__ */ new Set();
267
+ for (const chunk of chunks) {
268
+ for (let offset = -before; offset <= after; offset += 1) {
269
+ const neighbourIndex = chunk.chunkIndex + offset;
270
+ const id = chunkVectorId(effectiveNamespace, chunk.sourceId, neighbourIndex);
271
+ if (offset !== 0 && neighbourIndex >= 0 && !known.has(id)) {
272
+ neighbourIds.add(id);
273
+ }
274
+ }
275
+ }
276
+ const neighbourTexts = await textsByIds([...neighbourIds], effectiveNamespace);
277
+ const textOf = (sourceId, chunkIndex) => {
278
+ const id = chunkVectorId(effectiveNamespace, sourceId, chunkIndex);
279
+ return known.get(id) ?? neighbourTexts.get(id);
280
+ };
281
+ return chunks.map((chunk) => {
282
+ const parts = [];
283
+ for (let offset = -before; offset <= after; offset += 1) {
284
+ const text = offset === 0 ? chunk.text : textOf(chunk.sourceId, chunk.chunkIndex + offset);
285
+ if (text !== void 0) {
286
+ parts.push(text);
287
+ }
288
+ }
289
+ return { ...chunk, text: parts.join("\n") };
290
+ });
291
+ };
292
+ const resolveFilter = (filter) => {
293
+ if (typeof filter === "string") {
294
+ const resolved = config.filters?.[filter];
295
+ if (!resolved) {
296
+ throw new LunoraError(
297
+ "NOT_FOUND",
298
+ `@lunora/ai/rag: unknown named filter "${filter}" — must be one of the keys declared in RagConfig.filters`
299
+ );
300
+ }
301
+ return resolved.filter;
302
+ }
303
+ return filter;
304
+ };
305
+ const parseMatches = (result, namespace) => result.matches.map((match) => {
306
+ const metadata = match.metadata ?? {};
307
+ const parsed = parseChunkVectorId(match.id, namespace);
308
+ const rawText = metadata[TEXT_KEY];
309
+ const rawImportance = metadata[IMPORTANCE_KEY];
310
+ const importance = typeof rawImportance === "number" && rawImportance >= 0 && rawImportance <= 1 ? rawImportance : 1;
311
+ return {
312
+ chunkIndex: parsed.chunkIndex,
313
+ id: match.id,
314
+ importance,
315
+ metadata: userMetadataOf(metadata),
316
+ score: match.score * importance,
317
+ sourceId: parsed.sourceId,
318
+ text: typeof rawText === "string" ? rawText : ""
319
+ };
320
+ });
321
+ const hydrateFromStore = async (chunks, namespace) => {
322
+ if (!textStore) {
323
+ return chunks;
324
+ }
325
+ const texts = await textsByIds(
326
+ chunks.map((chunk) => chunk.id),
327
+ namespace
328
+ );
329
+ return chunks.flatMap((chunk) => {
330
+ const text = texts.get(chunk.id);
331
+ return text === void 0 ? [] : [{ ...chunk, text }];
332
+ });
333
+ };
334
+ const retrieve = async (query, options) => {
335
+ checkNamespace(options?.namespace);
336
+ const effectiveNamespace = withModelTag(options?.namespace);
337
+ const resolvedFilter = resolveFilter(options?.filter);
338
+ const rlsFilter = config.rlsFilter ? await config.rlsFilter(context.auth) : void 0;
339
+ const effectiveFilter = rlsFilter ? { ...resolvedFilter, ...rlsFilter } : resolvedFilter;
340
+ const topK = Math.min(options?.topK ?? defaultTopK, topKCeiling);
341
+ const vectorResult = await context.vectors.query(config.index, {
342
+ embed: embedText,
343
+ filter: effectiveFilter,
344
+ input: query,
345
+ namespace: effectiveNamespace,
346
+ returnMetadata: textStore ? "indexed" : "all",
347
+ topK
348
+ });
349
+ let chunks = await hydrateFromStore(parseMatches(vectorResult, effectiveNamespace), effectiveNamespace);
350
+ if (config.lexicalStore) {
351
+ const lexicalMatches = await config.lexicalStore.search(query, {
352
+ filter: effectiveFilter,
353
+ namespace: effectiveNamespace,
354
+ topK: config.lexicalTopK ?? topK
355
+ });
356
+ const lexicalChunks = lexicalMatches.map((match) => {
357
+ const parsed = parseChunkVectorId(match.id, effectiveNamespace);
358
+ return {
359
+ chunkIndex: parsed.chunkIndex,
360
+ id: match.id,
361
+ importance: 1,
362
+ metadata: void 0,
363
+ score: match.score,
364
+ sourceId: parsed.sourceId,
365
+ text: match.text
366
+ };
367
+ });
368
+ chunks = [...hybridRank(chunks, lexicalChunks)];
369
+ }
370
+ chunks.sort((a, b) => b.score - a.score);
371
+ const minScore = options?.minScore;
372
+ if (minScore !== void 0) {
373
+ chunks = chunks.filter((chunk) => chunk.score >= minScore);
374
+ }
375
+ chunks = [...await expandChunks(chunks, options, effectiveNamespace)];
376
+ const sources = [];
377
+ const seen = /* @__PURE__ */ new Set();
378
+ for (const chunk of chunks) {
379
+ if (!seen.has(chunk.sourceId)) {
380
+ seen.add(chunk.sourceId);
381
+ sources.push({ id: chunk.sourceId, metadata: chunk.metadata, weight: chunk.importance });
382
+ }
383
+ }
384
+ options?.onRetrieve?.({ matches: chunks.length, query });
385
+ return { chunks, context: assembleContext(chunks), sources };
386
+ };
387
+ const asTool = (options) => tool({
388
+ description: options?.description ?? `Search the "${config.index}" knowledge base for passages relevant to a natural-language query.`,
389
+ execute: async ({ query }) => retrieve(query, { namespace: options?.namespace, topK: options?.topK }),
390
+ inputSchema: jsonSchema({
391
+ properties: {
392
+ query: { description: "The natural-language search query.", type: "string" }
393
+ },
394
+ required: ["query"],
395
+ type: "object"
396
+ })
397
+ });
398
+ return { asTool, index, remove, retrieve };
399
+ };
400
+ };
401
+
402
+ export { defineRag as default };
@@ -0,0 +1,26 @@
1
+ const fixedWindowChunks = (text, size, overlap) => {
2
+ if (!Number.isInteger(size) || size < 1) {
3
+ throw new RangeError("fixedWindowChunks: `size` must be a positive integer");
4
+ }
5
+ if (!Number.isInteger(overlap) || overlap < 0 || overlap >= size) {
6
+ throw new RangeError("fixedWindowChunks: `overlap` must be a non-negative integer smaller than `size`");
7
+ }
8
+ const trimmed = text.trim();
9
+ if (trimmed.length === 0) {
10
+ return [];
11
+ }
12
+ if (trimmed.length <= size) {
13
+ return [trimmed];
14
+ }
15
+ const step = Math.max(1, size - overlap);
16
+ const chunks = [];
17
+ for (let start = 0; start < trimmed.length; start += step) {
18
+ chunks.push(trimmed.slice(start, start + size));
19
+ if (start + size >= trimmed.length) {
20
+ break;
21
+ }
22
+ }
23
+ return chunks;
24
+ };
25
+
26
+ export { fixedWindowChunks as default };
@@ -0,0 +1,20 @@
1
+ const hybridRank = (vectorResults, textResults, k = 60) => {
2
+ const fused = /* @__PURE__ */ new Map();
3
+ for (const [rank, chunk] of vectorResults.entries()) {
4
+ fused.set(chunk.id, { chunk, score: 1 / (k + rank), vectorRank: rank });
5
+ }
6
+ for (const [rank, chunk] of textResults.entries()) {
7
+ const existing = fused.get(chunk.id);
8
+ if (existing) {
9
+ existing.score += 1 / (k + rank);
10
+ } else {
11
+ fused.set(chunk.id, { chunk, score: 1 / (k + rank), vectorRank: Number.POSITIVE_INFINITY });
12
+ }
13
+ }
14
+ return [...fused.values()].toSorted((a, b) => {
15
+ const delta = b.score - a.score;
16
+ return delta === 0 ? a.vectorRank - b.vectorRank : delta;
17
+ }).map((entry) => entry.chunk);
18
+ };
19
+
20
+ export { hybridRank as default };
@@ -0,0 +1,28 @@
1
+ import { EmbeddingModel, LanguageModel } from 'ai';
2
+ interface AiBindingLike {
3
+ run: (model: string, inputs: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
4
+ }
5
+ interface WorkersAiProviderLike {
6
+ (modelId: string, settings?: Record<string, unknown>): LanguageModel;
7
+ textEmbeddingModel?: (modelId: string) => EmbeddingModel;
8
+ }
9
+ interface AiGatewayOptions {
10
+ [key: string]: unknown;
11
+ id: string;
12
+ }
13
+ interface LunoraAiOptions {
14
+ binding?: AiBindingLike;
15
+ defaultEmbeddingModel?: string;
16
+ defaultModel?: string;
17
+ gateway?: AiGatewayOptions;
18
+ provider?: WorkersAiProviderLike;
19
+ }
20
+ type ModelInput = LanguageModel;
21
+ type EmbeddingModelInput = EmbeddingModel | string;
22
+ interface LunoraAi {
23
+ embeddingModel: (model?: EmbeddingModelInput) => EmbeddingModel;
24
+ model: (model?: ModelInput) => LanguageModel;
25
+ run: (model: string, inputs: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
26
+ workersai: WorkersAiProviderLike;
27
+ }
28
+ 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,28 @@
1
+ import { EmbeddingModel, LanguageModel } from 'ai';
2
+ interface AiBindingLike {
3
+ run: (model: string, inputs: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
4
+ }
5
+ interface WorkersAiProviderLike {
6
+ (modelId: string, settings?: Record<string, unknown>): LanguageModel;
7
+ textEmbeddingModel?: (modelId: string) => EmbeddingModel;
8
+ }
9
+ interface AiGatewayOptions {
10
+ [key: string]: unknown;
11
+ id: string;
12
+ }
13
+ interface LunoraAiOptions {
14
+ binding?: AiBindingLike;
15
+ defaultEmbeddingModel?: string;
16
+ defaultModel?: string;
17
+ gateway?: AiGatewayOptions;
18
+ provider?: WorkersAiProviderLike;
19
+ }
20
+ type ModelInput = LanguageModel;
21
+ type EmbeddingModelInput = EmbeddingModel | string;
22
+ interface LunoraAi {
23
+ embeddingModel: (model?: EmbeddingModelInput) => EmbeddingModel;
24
+ model: (model?: ModelInput) => LanguageModel;
25
+ run: (model: string, inputs: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
26
+ workersai: WorkersAiProviderLike;
27
+ }
28
+ 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,173 @@
1
+ import { Tool } from 'ai';
2
+ import { E as EmbeddingModelInput, a as LunoraAi } from "../packem_shared/types.d-C6dA8LCy.mjs";
3
+ declare const fixedWindowChunks: (text: string, size: number, overlap: number) => ReadonlyArray<string>;
4
+ type RagEmbedder = (input: string) => Promise<ReadonlyArray<number>> | ReadonlyArray<number>;
5
+ interface RagVectorMatch {
6
+ id: string;
7
+ metadata?: Record<string, unknown>;
8
+ score: number;
9
+ }
10
+ interface RagVectorMatches {
11
+ count: number;
12
+ matches: ReadonlyArray<RagVectorMatch>;
13
+ }
14
+ interface RagVectorQueryInput {
15
+ embed?: RagEmbedder;
16
+ filter?: Record<string, unknown>;
17
+ input?: string;
18
+ namespace?: string;
19
+ returnMetadata?: "all" | "indexed" | "none";
20
+ topK?: number;
21
+ }
22
+ interface RagVectorRecord {
23
+ id: string;
24
+ metadata?: Record<string, unknown>;
25
+ }
26
+ interface RagVectorUpsertInput {
27
+ embed?: RagEmbedder;
28
+ id: string;
29
+ input: string;
30
+ metadata?: Record<string, unknown>;
31
+ namespace?: string;
32
+ }
33
+ interface RagVectors {
34
+ deleteByIds: (indexName: string, ids: ReadonlyArray<string>, namespace?: string) => Promise<unknown>;
35
+ getByIds: (indexName: string, ids: ReadonlyArray<string>, namespace?: string) => Promise<ReadonlyArray<RagVectorRecord>>;
36
+ query: (indexName: string, input: RagVectorQueryInput) => Promise<RagVectorMatches>;
37
+ upsert: (indexName: string, input: RagVectorUpsertInput) => Promise<unknown>;
38
+ }
39
+ interface RagContext {
40
+ ai?: Pick<LunoraAi, "embeddingModel">;
41
+ auth?: unknown;
42
+ vectors: RagVectors;
43
+ }
44
+ interface RagTextStore {
45
+ getMany: (ids: ReadonlyArray<string>, options: {
46
+ namespace?: string;
47
+ }) => Promise<ReadonlyArray<string | undefined>>;
48
+ put: (chunks: ReadonlyArray<StoredRagChunk>, options: {
49
+ namespace?: string;
50
+ }) => Promise<void>;
51
+ remove?: (ids: ReadonlyArray<string>, options: {
52
+ namespace?: string;
53
+ }) => Promise<void>;
54
+ }
55
+ interface StoredRagChunk {
56
+ chunkIndex: number;
57
+ id: string;
58
+ sourceId: string;
59
+ text: string;
60
+ }
61
+ interface LexicalMatch {
62
+ id: string;
63
+ score: number;
64
+ text: string;
65
+ }
66
+ interface RagLexicalStore {
67
+ index: (chunks: ReadonlyArray<StoredRagChunk>, options: {
68
+ namespace?: string;
69
+ }) => Promise<void>;
70
+ remove?: (ids: ReadonlyArray<string>, options: {
71
+ namespace?: string;
72
+ }) => Promise<void>;
73
+ search: (query: string, options: {
74
+ filter?: Record<string, unknown>;
75
+ namespace?: string;
76
+ topK: number;
77
+ }) => Promise<ReadonlyArray<LexicalMatch>>;
78
+ }
79
+ interface RagNamedFilter {
80
+ description?: string;
81
+ filter: Record<string, unknown>;
82
+ }
83
+ interface RagConfig {
84
+ allowSharedNamespace?: boolean;
85
+ chunk?: (text: string) => ReadonlyArray<string>;
86
+ chunkOverlap?: number;
87
+ chunkSize?: number;
88
+ embeddingModel?: EmbeddingModelInput;
89
+ embeddingModelVersion?: string;
90
+ filters?: Record<string, RagNamedFilter>;
91
+ index: string;
92
+ lexicalStore?: RagLexicalStore;
93
+ lexicalTopK?: number;
94
+ requireNamespace?: boolean;
95
+ rlsFilter?: (auth: unknown) => Promise<Record<string, unknown> | undefined> | Record<string, unknown> | undefined;
96
+ textStore?: RagTextStore;
97
+ topK?: number;
98
+ }
99
+ interface IndexInput {
100
+ allowEmptySources?: boolean;
101
+ id: string;
102
+ importance?: number;
103
+ metadata?: Record<string, unknown>;
104
+ namespace?: string;
105
+ onChunk?: (info: {
106
+ chunkIndex: number;
107
+ id: string;
108
+ text: string;
109
+ total: number;
110
+ }) => void;
111
+ text: string;
112
+ }
113
+ interface IndexResult {
114
+ chunks: number;
115
+ ids: ReadonlyArray<string>;
116
+ unchanged: boolean;
117
+ }
118
+ interface RemoveInput {
119
+ id: string;
120
+ namespace?: string;
121
+ }
122
+ interface RetrieveOptions {
123
+ chunkContext?: {
124
+ after?: number;
125
+ before?: number;
126
+ };
127
+ filter?: Record<string, unknown> | string;
128
+ minScore?: number;
129
+ namespace?: string;
130
+ onRetrieve?: (info: {
131
+ matches: number;
132
+ query: string;
133
+ }) => void;
134
+ topK?: number;
135
+ }
136
+ interface RetrievedChunk {
137
+ chunkIndex: number;
138
+ id: string;
139
+ importance: number;
140
+ metadata?: Record<string, unknown>;
141
+ score: number;
142
+ sourceId: string;
143
+ text: string;
144
+ }
145
+ interface RagSource {
146
+ id: string;
147
+ metadata?: Record<string, unknown>;
148
+ weight?: number;
149
+ }
150
+ interface RetrieveResult {
151
+ chunks: ReadonlyArray<RetrievedChunk>;
152
+ context: string;
153
+ sources: ReadonlyArray<RagSource>;
154
+ }
155
+ interface RagToolOptions {
156
+ description?: string;
157
+ namespace?: string;
158
+ topK?: number;
159
+ }
160
+ interface Rag {
161
+ asTool: (options?: RagToolOptions) => Tool<{
162
+ query: string;
163
+ }, RetrieveResult>;
164
+ index: (input: IndexInput) => Promise<IndexResult>;
165
+ remove: (input: RemoveInput) => Promise<void>;
166
+ retrieve: (query: string, options?: RetrieveOptions) => Promise<RetrieveResult>;
167
+ }
168
+ declare const defineRag: (config: RagConfig) => ((context: RagContext) => Rag);
169
+ declare const guessMimeTypeFromExtension: (extension: string) => string;
170
+ declare const contentHash: (data: BufferSource) => Promise<string>;
171
+ declare const hybridRank: (vectorResults: ReadonlyArray<RetrievedChunk>, textResults: ReadonlyArray<RetrievedChunk>, k?: number) => ReadonlyArray<RetrievedChunk>;
172
+ declare const bm25LexicalStore: () => RagLexicalStore;
173
+ 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 };
@@ -0,0 +1,173 @@
1
+ import { Tool } from 'ai';
2
+ import { E as EmbeddingModelInput, a as LunoraAi } from "../packem_shared/types.d-C6dA8LCy.js";
3
+ declare const fixedWindowChunks: (text: string, size: number, overlap: number) => ReadonlyArray<string>;
4
+ type RagEmbedder = (input: string) => Promise<ReadonlyArray<number>> | ReadonlyArray<number>;
5
+ interface RagVectorMatch {
6
+ id: string;
7
+ metadata?: Record<string, unknown>;
8
+ score: number;
9
+ }
10
+ interface RagVectorMatches {
11
+ count: number;
12
+ matches: ReadonlyArray<RagVectorMatch>;
13
+ }
14
+ interface RagVectorQueryInput {
15
+ embed?: RagEmbedder;
16
+ filter?: Record<string, unknown>;
17
+ input?: string;
18
+ namespace?: string;
19
+ returnMetadata?: "all" | "indexed" | "none";
20
+ topK?: number;
21
+ }
22
+ interface RagVectorRecord {
23
+ id: string;
24
+ metadata?: Record<string, unknown>;
25
+ }
26
+ interface RagVectorUpsertInput {
27
+ embed?: RagEmbedder;
28
+ id: string;
29
+ input: string;
30
+ metadata?: Record<string, unknown>;
31
+ namespace?: string;
32
+ }
33
+ interface RagVectors {
34
+ deleteByIds: (indexName: string, ids: ReadonlyArray<string>, namespace?: string) => Promise<unknown>;
35
+ getByIds: (indexName: string, ids: ReadonlyArray<string>, namespace?: string) => Promise<ReadonlyArray<RagVectorRecord>>;
36
+ query: (indexName: string, input: RagVectorQueryInput) => Promise<RagVectorMatches>;
37
+ upsert: (indexName: string, input: RagVectorUpsertInput) => Promise<unknown>;
38
+ }
39
+ interface RagContext {
40
+ ai?: Pick<LunoraAi, "embeddingModel">;
41
+ auth?: unknown;
42
+ vectors: RagVectors;
43
+ }
44
+ interface RagTextStore {
45
+ getMany: (ids: ReadonlyArray<string>, options: {
46
+ namespace?: string;
47
+ }) => Promise<ReadonlyArray<string | undefined>>;
48
+ put: (chunks: ReadonlyArray<StoredRagChunk>, options: {
49
+ namespace?: string;
50
+ }) => Promise<void>;
51
+ remove?: (ids: ReadonlyArray<string>, options: {
52
+ namespace?: string;
53
+ }) => Promise<void>;
54
+ }
55
+ interface StoredRagChunk {
56
+ chunkIndex: number;
57
+ id: string;
58
+ sourceId: string;
59
+ text: string;
60
+ }
61
+ interface LexicalMatch {
62
+ id: string;
63
+ score: number;
64
+ text: string;
65
+ }
66
+ interface RagLexicalStore {
67
+ index: (chunks: ReadonlyArray<StoredRagChunk>, options: {
68
+ namespace?: string;
69
+ }) => Promise<void>;
70
+ remove?: (ids: ReadonlyArray<string>, options: {
71
+ namespace?: string;
72
+ }) => Promise<void>;
73
+ search: (query: string, options: {
74
+ filter?: Record<string, unknown>;
75
+ namespace?: string;
76
+ topK: number;
77
+ }) => Promise<ReadonlyArray<LexicalMatch>>;
78
+ }
79
+ interface RagNamedFilter {
80
+ description?: string;
81
+ filter: Record<string, unknown>;
82
+ }
83
+ interface RagConfig {
84
+ allowSharedNamespace?: boolean;
85
+ chunk?: (text: string) => ReadonlyArray<string>;
86
+ chunkOverlap?: number;
87
+ chunkSize?: number;
88
+ embeddingModel?: EmbeddingModelInput;
89
+ embeddingModelVersion?: string;
90
+ filters?: Record<string, RagNamedFilter>;
91
+ index: string;
92
+ lexicalStore?: RagLexicalStore;
93
+ lexicalTopK?: number;
94
+ requireNamespace?: boolean;
95
+ rlsFilter?: (auth: unknown) => Promise<Record<string, unknown> | undefined> | Record<string, unknown> | undefined;
96
+ textStore?: RagTextStore;
97
+ topK?: number;
98
+ }
99
+ interface IndexInput {
100
+ allowEmptySources?: boolean;
101
+ id: string;
102
+ importance?: number;
103
+ metadata?: Record<string, unknown>;
104
+ namespace?: string;
105
+ onChunk?: (info: {
106
+ chunkIndex: number;
107
+ id: string;
108
+ text: string;
109
+ total: number;
110
+ }) => void;
111
+ text: string;
112
+ }
113
+ interface IndexResult {
114
+ chunks: number;
115
+ ids: ReadonlyArray<string>;
116
+ unchanged: boolean;
117
+ }
118
+ interface RemoveInput {
119
+ id: string;
120
+ namespace?: string;
121
+ }
122
+ interface RetrieveOptions {
123
+ chunkContext?: {
124
+ after?: number;
125
+ before?: number;
126
+ };
127
+ filter?: Record<string, unknown> | string;
128
+ minScore?: number;
129
+ namespace?: string;
130
+ onRetrieve?: (info: {
131
+ matches: number;
132
+ query: string;
133
+ }) => void;
134
+ topK?: number;
135
+ }
136
+ interface RetrievedChunk {
137
+ chunkIndex: number;
138
+ id: string;
139
+ importance: number;
140
+ metadata?: Record<string, unknown>;
141
+ score: number;
142
+ sourceId: string;
143
+ text: string;
144
+ }
145
+ interface RagSource {
146
+ id: string;
147
+ metadata?: Record<string, unknown>;
148
+ weight?: number;
149
+ }
150
+ interface RetrieveResult {
151
+ chunks: ReadonlyArray<RetrievedChunk>;
152
+ context: string;
153
+ sources: ReadonlyArray<RagSource>;
154
+ }
155
+ interface RagToolOptions {
156
+ description?: string;
157
+ namespace?: string;
158
+ topK?: number;
159
+ }
160
+ interface Rag {
161
+ asTool: (options?: RagToolOptions) => Tool<{
162
+ query: string;
163
+ }, RetrieveResult>;
164
+ index: (input: IndexInput) => Promise<IndexResult>;
165
+ remove: (input: RemoveInput) => Promise<void>;
166
+ retrieve: (query: string, options?: RetrieveOptions) => Promise<RetrieveResult>;
167
+ }
168
+ declare const defineRag: (config: RagConfig) => ((context: RagContext) => Rag);
169
+ declare const guessMimeTypeFromExtension: (extension: string) => string;
170
+ declare const contentHash: (data: BufferSource) => Promise<string>;
171
+ declare const hybridRank: (vectorResults: ReadonlyArray<RetrievedChunk>, textResults: ReadonlyArray<RetrievedChunk>, k?: number) => ReadonlyArray<RetrievedChunk>;
172
+ declare const bm25LexicalStore: () => RagLexicalStore;
173
+ 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 };
@@ -0,0 +1,5 @@
1
+ export { default as fixedWindowChunks } from '../packem_shared/fixedWindowChunks-J-WfQDw9.mjs';
2
+ export { default as defineRag } from '../packem_shared/defineRag-BwrgePLE.mjs';
3
+ export { contentHash, guessMimeTypeFromExtension } from '../packem_shared/contentHash-Cgz5KRGD.mjs';
4
+ export { default as hybridRank } from '../packem_shared/hybridRank-DPC9c2ON.mjs';
5
+ export { default as bm25LexicalStore } from '../packem_shared/bm25LexicalStore-Q28z6fbO.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/ai",
3
- "version": "1.0.0-alpha.13",
3
+ "version": "1.0.0-alpha.14",
4
4
  "description": "Workers AI helper for Lunora: provider-agnostic AI SDK access from functions, Workers AI by default",
5
5
  "keywords": [
6
6
  "ai",
@@ -10,6 +10,8 @@
10
10
  "inference",
11
11
  "llm",
12
12
  "lunora",
13
+ "rag",
14
+ "retrieval",
13
15
  "workers",
14
16
  "workers-ai"
15
17
  ],
@@ -41,6 +43,10 @@
41
43
  "types": "./dist/index.d.ts",
42
44
  "import": "./dist/index.mjs"
43
45
  },
46
+ "./rag": {
47
+ "types": "./dist/rag/index.d.ts",
48
+ "import": "./dist/rag/index.mjs"
49
+ },
44
50
  "./package.json": "./package.json"
45
51
  },
46
52
  "publishConfig": {