@lunora/ai 1.0.0-alpha.2 → 1.0.0-alpha.20
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-DgVXmNGw.mjs +447 -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 +546 -0
- package/dist/rag/index.d.ts +546 -0
- package/dist/rag/index.mjs +5 -0
- package/package.json +12 -5
package/LICENSE.md
CHANGED
|
@@ -103,3 +103,9 @@ Unless required by applicable law or agreed to in writing, software distributed
|
|
|
103
103
|
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
104
|
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
105
|
specific language governing permissions and limitations under the License.
|
|
106
|
+
|
|
107
|
+
<!-- DEPENDENCIES -->
|
|
108
|
+
<!-- /DEPENDENCIES -->
|
|
109
|
+
|
|
110
|
+
<!-- TYPE_DEPENDENCIES -->
|
|
111
|
+
<!-- /TYPE_DEPENDENCIES -->
|
package/README.md
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -1,30 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { L as LunoraAiOptions, a as LunoraAi } from "./packem_shared/types.d-BXCiRv1x.mjs";
|
|
2
|
+
export type { A as AiBindingLike, b as AiGatewayOptions, E as EmbeddingModelInput, M as ModelInput, W as WorkersAiProviderLike } from "./packem_shared/types.d-BXCiRv1x.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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
run: (model: string, inputs: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
|
|
27
|
-
workersai: WorkersAiProviderLike;
|
|
28
|
-
}
|
|
5
|
+
/**
|
|
6
|
+
* Create the `ctx.ai` helper over a Workers `AI` binding.
|
|
7
|
+
*
|
|
8
|
+
* Workers AI is the zero-config default, but `@lunora/ai` is provider-agnostic:
|
|
9
|
+
* every helper takes either a model id string (resolved against the Workers AI
|
|
10
|
+
* provider) or any AI SDK {@link LanguageModel}/{@link EmbeddingModel} object
|
|
11
|
+
* (`@ai-sdk/openai`, `@ai-sdk/anthropic`, OpenRouter, …), so apps are never
|
|
12
|
+
* locked to Workers AI. Pair `embed` with `@lunora/bindings/vectors` for RAG.
|
|
13
|
+
*
|
|
14
|
+
* Combine with the re-exported `generateText`/`streamText`/`generateObject`/
|
|
15
|
+
* `embed`/`tool` from this package:
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { streamText } from "@lunora/ai";
|
|
19
|
+
*
|
|
20
|
+
* const result = streamText({
|
|
21
|
+
* model: ctx.ai.model("@cf/meta/llama-3.3-70b-instruct-fp8-fast"),
|
|
22
|
+
* messages,
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
* @experimental
|
|
26
|
+
*/
|
|
29
27
|
declare const createAi: (options: LunoraAiOptions) => LunoraAi;
|
|
30
|
-
export { type
|
|
28
|
+
export { type LunoraAi, type LunoraAiOptions, createAi };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,30 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { L as LunoraAiOptions, a as LunoraAi } from "./packem_shared/types.d-BXCiRv1x.js";
|
|
2
|
+
export type { A as AiBindingLike, b as AiGatewayOptions, E as EmbeddingModelInput, M as ModelInput, W as WorkersAiProviderLike } from "./packem_shared/types.d-BXCiRv1x.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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
run: (model: string, inputs: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
|
|
27
|
-
workersai: WorkersAiProviderLike;
|
|
28
|
-
}
|
|
5
|
+
/**
|
|
6
|
+
* Create the `ctx.ai` helper over a Workers `AI` binding.
|
|
7
|
+
*
|
|
8
|
+
* Workers AI is the zero-config default, but `@lunora/ai` is provider-agnostic:
|
|
9
|
+
* every helper takes either a model id string (resolved against the Workers AI
|
|
10
|
+
* provider) or any AI SDK {@link LanguageModel}/{@link EmbeddingModel} object
|
|
11
|
+
* (`@ai-sdk/openai`, `@ai-sdk/anthropic`, OpenRouter, …), so apps are never
|
|
12
|
+
* locked to Workers AI. Pair `embed` with `@lunora/bindings/vectors` for RAG.
|
|
13
|
+
*
|
|
14
|
+
* Combine with the re-exported `generateText`/`streamText`/`generateObject`/
|
|
15
|
+
* `embed`/`tool` from this package:
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { streamText } from "@lunora/ai";
|
|
19
|
+
*
|
|
20
|
+
* const result = streamText({
|
|
21
|
+
* model: ctx.ai.model("@cf/meta/llama-3.3-70b-instruct-fp8-fast"),
|
|
22
|
+
* messages,
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
* @experimental
|
|
26
|
+
*/
|
|
29
27
|
declare const createAi: (options: LunoraAiOptions) => LunoraAi;
|
|
30
|
-
export { type
|
|
28
|
+
export { type LunoraAi, type LunoraAiOptions, createAi };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { default as createAi } from './packem_shared/createAi-
|
|
2
|
-
export { embed, embedMany, generateObject, generateText, streamObject, streamText, tool } from 'ai';
|
|
1
|
+
export { default as createAi } from './packem_shared/createAi-CYxQuvEL.mjs';
|
|
2
|
+
export { embed, embedMany, generateObject, generateText, hasToolCall, jsonSchema, streamObject, streamText, tool } from 'ai';
|
|
3
3
|
export { createWorkersAI } from 'workers-ai-provider';
|
|
@@ -0,0 +1,117 @@
|
|
|
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 filterWarned = /* @__PURE__ */ new WeakSet();
|
|
6
|
+
const bm25LexicalStore = () => {
|
|
7
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
8
|
+
const stateFor = (namespace = "") => {
|
|
9
|
+
let state = namespaces.get(namespace);
|
|
10
|
+
if (!state) {
|
|
11
|
+
state = { documents: /* @__PURE__ */ new Map(), postings: /* @__PURE__ */ new Map(), totalLength: 0 };
|
|
12
|
+
namespaces.set(namespace, state);
|
|
13
|
+
}
|
|
14
|
+
return state;
|
|
15
|
+
};
|
|
16
|
+
const removeDocument = (namespace, id) => {
|
|
17
|
+
const state = stateFor(namespace);
|
|
18
|
+
const existing = state.documents.get(id);
|
|
19
|
+
if (!existing) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const term of existing.termFrequency.keys()) {
|
|
23
|
+
const posting = state.postings.get(term);
|
|
24
|
+
if (posting) {
|
|
25
|
+
posting.delete(id);
|
|
26
|
+
if (posting.size === 0) {
|
|
27
|
+
state.postings.delete(term);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
state.totalLength -= existing.length;
|
|
32
|
+
state.documents.delete(id);
|
|
33
|
+
};
|
|
34
|
+
const store = {
|
|
35
|
+
index: (chunks, options) => {
|
|
36
|
+
const state = stateFor(options.namespace);
|
|
37
|
+
for (const chunk of chunks) {
|
|
38
|
+
removeDocument(options.namespace, chunk.id);
|
|
39
|
+
const tokens = tokenize(chunk.text);
|
|
40
|
+
if (tokens.length === 0) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const termFrequency = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const token of tokens) {
|
|
45
|
+
termFrequency.set(token, (termFrequency.get(token) ?? 0) + 1);
|
|
46
|
+
}
|
|
47
|
+
for (const [term, frequency] of termFrequency) {
|
|
48
|
+
let posting = state.postings.get(term);
|
|
49
|
+
if (!posting) {
|
|
50
|
+
posting = /* @__PURE__ */ new Map();
|
|
51
|
+
state.postings.set(term, posting);
|
|
52
|
+
}
|
|
53
|
+
posting.set(chunk.id, frequency);
|
|
54
|
+
}
|
|
55
|
+
state.documents.set(chunk.id, { length: tokens.length, termFrequency, text: chunk.text });
|
|
56
|
+
state.totalLength += tokens.length;
|
|
57
|
+
}
|
|
58
|
+
return Promise.resolve();
|
|
59
|
+
},
|
|
60
|
+
remove: (ids, options) => {
|
|
61
|
+
for (const id of ids) {
|
|
62
|
+
removeDocument(options.namespace, id);
|
|
63
|
+
}
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
},
|
|
66
|
+
search: (query, options) => {
|
|
67
|
+
if (options.filter && Object.keys(options.filter).length > 0) {
|
|
68
|
+
if (!filterWarned.has(store)) {
|
|
69
|
+
filterWarned.add(store);
|
|
70
|
+
console.warn(
|
|
71
|
+
"[@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."
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return Promise.resolve([]);
|
|
75
|
+
}
|
|
76
|
+
const state = stateFor(options.namespace);
|
|
77
|
+
const documentCount = state.documents.size;
|
|
78
|
+
if (documentCount === 0) {
|
|
79
|
+
return Promise.resolve([]);
|
|
80
|
+
}
|
|
81
|
+
const queryTerms = [...new Set(tokenize(query))];
|
|
82
|
+
if (queryTerms.length === 0) {
|
|
83
|
+
return Promise.resolve([]);
|
|
84
|
+
}
|
|
85
|
+
const averageLength = state.totalLength / documentCount;
|
|
86
|
+
const scores = /* @__PURE__ */ new Map();
|
|
87
|
+
for (const term of queryTerms) {
|
|
88
|
+
const posting = state.postings.get(term);
|
|
89
|
+
if (!posting) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const documentFrequency = posting.size;
|
|
93
|
+
const idf = Math.log(1 + (documentCount - documentFrequency + 0.5) / (documentFrequency + 0.5));
|
|
94
|
+
for (const [id, frequency] of posting) {
|
|
95
|
+
const document = state.documents.get(id);
|
|
96
|
+
if (!document) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const denominator = frequency + BM25_K1 * (1 - BM25_B + BM25_B * document.length / averageLength);
|
|
100
|
+
const contribution = idf * (frequency * (BM25_K1 + 1) / denominator);
|
|
101
|
+
scores.set(id, (scores.get(id) ?? 0) + contribution);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const matches = [...scores.entries()].map(([id, score]) => {
|
|
105
|
+
return {
|
|
106
|
+
id,
|
|
107
|
+
score,
|
|
108
|
+
text: state.documents.get(id)?.text ?? ""
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
return Promise.resolve(matches.toSorted((a, b) => b.score - a.score).slice(0, options.topK));
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
return store;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
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 };
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
1
2
|
import { createWorkersAI } from 'workers-ai-provider';
|
|
2
3
|
|
|
3
4
|
const buildProvider = (binding, gateway) => createWorkersAI({ binding, gateway });
|
|
4
5
|
const createAi = (options) => {
|
|
5
|
-
const { binding, defaultModel, gateway, provider } = options;
|
|
6
|
+
const { binding, defaultEmbeddingModel, defaultModel, gateway, provider } = options;
|
|
6
7
|
if (!provider && !binding) {
|
|
7
|
-
throw new
|
|
8
|
+
throw new LunoraError("INTERNAL", "@lunora/ai: createAi requires a `binding` (env.AI) or a pre-built `provider`");
|
|
8
9
|
}
|
|
9
10
|
const workersai = provider ?? buildProvider(binding, gateway);
|
|
10
11
|
const model = (input) => {
|
|
11
12
|
if (input === void 0) {
|
|
12
13
|
if (!defaultModel) {
|
|
13
|
-
throw new
|
|
14
|
+
throw new LunoraError("INTERNAL", "@lunora/ai: no model supplied and no `defaultModel` configured — pass a model id or an AI SDK model");
|
|
14
15
|
}
|
|
15
16
|
return workersai(defaultModel);
|
|
16
17
|
}
|
|
@@ -19,7 +20,8 @@ const createAi = (options) => {
|
|
|
19
20
|
const resolveEmbeddingModel = (modelId) => {
|
|
20
21
|
const factory = workersai.textEmbeddingModel;
|
|
21
22
|
if (typeof factory !== "function") {
|
|
22
|
-
throw new
|
|
23
|
+
throw new LunoraError(
|
|
24
|
+
"INTERNAL",
|
|
23
25
|
"@lunora/ai: the Workers AI provider does not expose `textEmbeddingModel`; pass an AI SDK EmbeddingModel (e.g. from @ai-sdk/openai) to embed()"
|
|
24
26
|
);
|
|
25
27
|
}
|
|
@@ -29,17 +31,21 @@ const createAi = (options) => {
|
|
|
29
31
|
if (typeof input === "object") {
|
|
30
32
|
return input;
|
|
31
33
|
}
|
|
32
|
-
const modelId = input ??
|
|
34
|
+
const modelId = input ?? defaultEmbeddingModel;
|
|
33
35
|
if (!modelId) {
|
|
34
|
-
throw new
|
|
35
|
-
"
|
|
36
|
+
throw new LunoraError(
|
|
37
|
+
"INTERNAL",
|
|
38
|
+
"@lunora/ai: no embedding model supplied and no `defaultEmbeddingModel` configured — pass an embedding model id or an AI SDK EmbeddingModel"
|
|
36
39
|
);
|
|
37
40
|
}
|
|
38
41
|
return resolveEmbeddingModel(modelId);
|
|
39
42
|
};
|
|
40
43
|
const run = async (modelId, inputs, runOptions) => {
|
|
41
44
|
if (!binding) {
|
|
42
|
-
throw new
|
|
45
|
+
throw new LunoraError(
|
|
46
|
+
"INTERNAL",
|
|
47
|
+
"@lunora/ai: ai.run requires the `binding` (env.AI) — it is unavailable when only a custom `provider` was supplied"
|
|
48
|
+
);
|
|
43
49
|
}
|
|
44
50
|
return binding.run(modelId, inputs, runOptions);
|
|
45
51
|
};
|