@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 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
@@ -10,6 +10,8 @@
10
10
 
11
11
  <!-- END_PACKAGE_OG_IMAGE_PLACEHOLDER -->
12
12
 
13
+ > **Experimental** — this package is outside the Lunora 1.0 stability promise: its API may change in any release, without a major version bump.
14
+
13
15
  <br />
14
16
 
15
17
  <div align="center">
package/dist/index.d.mts CHANGED
@@ -1,30 +1,28 @@
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-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
- 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
- defaultModel?: string;
18
- gateway?: AiGatewayOptions;
19
- provider?: WorkersAiProviderLike;
20
- }
21
- type ModelInput = LanguageModel;
22
- type EmbeddingModelInput = EmbeddingModel | string;
23
- interface LunoraAi {
24
- embeddingModel: (model?: EmbeddingModelInput) => EmbeddingModel;
25
- model: (model?: ModelInput) => LanguageModel;
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 AiBindingLike, type AiGatewayOptions, type EmbeddingModelInput, type LunoraAi, type LunoraAiOptions, type ModelInput, type WorkersAiProviderLike, createAi };
28
+ export { type LunoraAi, type LunoraAiOptions, createAi };
package/dist/index.d.ts CHANGED
@@ -1,30 +1,28 @@
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-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
- 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
- defaultModel?: string;
18
- gateway?: AiGatewayOptions;
19
- provider?: WorkersAiProviderLike;
20
- }
21
- type ModelInput = LanguageModel;
22
- type EmbeddingModelInput = EmbeddingModel | string;
23
- interface LunoraAi {
24
- embeddingModel: (model?: EmbeddingModelInput) => EmbeddingModel;
25
- model: (model?: ModelInput) => LanguageModel;
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 AiBindingLike, type AiGatewayOptions, type EmbeddingModelInput, type LunoraAi, type LunoraAiOptions, type ModelInput, type WorkersAiProviderLike, createAi };
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-CaLhvwAd.mjs';
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 Error("@lunora/ai: createAi requires a `binding` (env.AI) or a pre-built `provider`");
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 Error("@lunora/ai: no model supplied and no `defaultModel` configured — pass a model id or an AI SDK model");
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 TypeError(
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 ?? defaultModel;
34
+ const modelId = input ?? defaultEmbeddingModel;
33
35
  if (!modelId) {
34
- throw new Error(
35
- "@lunora/ai: no embedding model supplied and no `defaultModel` configured — pass an embedding model id or an AI SDK EmbeddingModel"
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 Error("@lunora/ai: ai.run requires the `binding` (env.AI) — it is unavailable when only a custom `provider` was supplied");
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
  };