@lunora/ai 1.0.0-alpha.25 → 1.0.0-alpha.26

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/README.md CHANGED
@@ -36,7 +36,7 @@
36
36
 
37
37
  ---
38
38
 
39
- A small AI helper for Lunora, built on the [Vercel AI SDK](https://ai-sdk.dev) v6 core and Cloudflare's official [`workers-ai-provider`](https://github.com/cloudflare/ai). Call `generateText`/`streamText`/`generateObject`/`embed`/`tool` from any function handler. **Cloudflare Workers AI is the zero-config default**, but the helper is provider-agnostic: every call takes either a Workers AI model id (a string) or any AI SDK model object — `@ai-sdk/openai`, `@ai-sdk/anthropic`, OpenRouter, … — so apps are never locked to Workers AI. Pair `embed` with [`@lunora/bindings/vectors`](https://www.npmjs.com/package/@lunora/bindings) for RAG.
39
+ A small AI helper for Lunora, built on the [Vercel AI SDK](https://ai-sdk.dev) v7 core and Cloudflare's official [`workers-ai-provider`](https://github.com/cloudflare/ai). Call `generateText`/`streamText`/`generateObject`/`embed`/`tool` from any function handler. **Cloudflare Workers AI is the zero-config default**, but the helper is provider-agnostic: every call takes either a Workers AI model id (a string) or any AI SDK model object — `@ai-sdk/openai`, `@ai-sdk/anthropic`, OpenRouter, … — so apps are never locked to Workers AI. Pair `embed` with [`@lunora/bindings/vectors`](https://www.npmjs.com/package/@lunora/bindings) for RAG.
40
40
 
41
41
  Part of the [Lunora](https://github.com/anolilab/lunora) framework — a type-safe, real-time backend on Cloudflare Workers + Durable Objects with a Vite-first DX.
42
42
 
package/dist/index.mjs CHANGED
@@ -1,4 +1 @@
1
- export { default as createAi } from './packem_shared/createAi-Cep3l8Rk.mjs';
2
- export { AI_GATEWAY_ACCOUNT_ID_ENV, AI_GATEWAY_ID_ENV, AI_GATEWAY_TOKEN_ENV, buildAiGatewayMetadataFields, resolveAiGateway } from './packem_shared/AI_GATEWAY_ACCOUNT_ID_ENV-CIAjehQF.mjs';
3
- export { embed, embedMany, generateObject, generateText, hasToolCall, jsonSchema, streamObject, streamText, tool } from 'ai';
4
- export { createWorkersAI } from 'workers-ai-provider';
1
+ import{default as a}from"./packem_shared/createAi-k_AcrXgH.mjs";import{AI_GATEWAY_ACCOUNT_ID_ENV as o,AI_GATEWAY_ID_ENV as A,AI_GATEWAY_TOKEN_ENV as _,buildAiGatewayMetadataFields as m,resolveAiGateway as l}from"./packem_shared/AI_GATEWAY_ACCOUNT_ID_ENV-JzYYnjLP.mjs";import{embed as T,embedMany as E,generateObject as d,generateText as x,hasToolCall as I,jsonSchema as b,streamObject as c,streamText as f,tool as i}from"ai";import{createWorkersAI as N}from"workers-ai-provider";export{o as AI_GATEWAY_ACCOUNT_ID_ENV,A as AI_GATEWAY_ID_ENV,_ as AI_GATEWAY_TOKEN_ENV,m as buildAiGatewayMetadataFields,a as createAi,N as createWorkersAI,T as embed,E as embedMany,d as generateObject,x as generateText,I as hasToolCall,b as jsonSchema,l as resolveAiGateway,c as streamObject,f as streamText,i as tool};
@@ -0,0 +1 @@
1
+ const r=(t,a)=>{const n=t[a];return typeof n=="string"&&n.length>0?n:void 0},c=t=>{if(t===void 0)return;const a={};return typeof t.functionPath=="string"&&t.functionPath.length>0&&(a.functionPath=t.functionPath),typeof t.traceId=="string"&&t.traceId.length>0&&(a.traceId=t.traceId),Object.keys(a).length>0?a:void 0},d=t=>{const a=c(t);return a===void 0?void 0:JSON.stringify(a)},s="LUNORA_AI_GATEWAY_ACCOUNT_ID",_="LUNORA_AI_GATEWAY_ID",I="LUNORA_AI_GATEWAY_TOKEN",u=(t,a)=>{const n=r(t,s),o=r(t,_);if(n===void 0||o===void 0)return;const e=r(t,I),i={};e!==void 0&&(i["cf-aig-authorization"]=`Bearer ${e}`);const A=d(a);return A!==void 0&&(i["cf-aig-metadata"]=A),{accountId:n,baseURL:`https://gateway.ai.cloudflare.com/v1/${n}/${o}`,gatewayId:o,headers:i}};export{s as AI_GATEWAY_ACCOUNT_ID_ENV,_ as AI_GATEWAY_ID_ENV,I as AI_GATEWAY_TOKEN_ENV,c as buildAiGatewayMetadataFields,u as resolveAiGateway};
@@ -0,0 +1 @@
1
+ const P=/[a-z0-9]+/g,x=l=>l.toLowerCase().match(P)??[],L=new WeakSet,k=()=>{const l=new Map,g=(s="")=>{let e=l.get(s);return e||(e={documents:new Map,postings:new Map,totalLength:0},l.set(s,e)),e},u=(s,e)=>{const t=g(s),o=t.documents.get(e);if(o){for(const r of o.termFrequency.keys()){const a=t.postings.get(r);a&&(a.delete(e),a.size===0&&t.postings.delete(r))}t.totalLength-=o.length,t.documents.delete(e)}},d={index:(s,e)=>{const t=g(e.namespace);for(const o of s){u(e.namespace,o.id);const r=x(o.text);if(r.length===0)continue;const a=new Map;for(const c of r)a.set(c,(a.get(c)??0)+1);for(const[c,f]of a){let n=t.postings.get(c);n||(n=new Map,t.postings.set(c,n)),n.set(o.id,f)}t.documents.set(o.id,{length:r.length,termFrequency:a,text:o.text}),t.totalLength+=r.length}return Promise.resolve()},remove:(s,e)=>{for(const t of s)u(e.namespace,t);return Promise.resolve()},search:(s,e)=>{if(e.filter&&Object.keys(e.filter).length>0)return L.has(d)||(L.add(d),console.warn("[@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.")),Promise.resolve([]);const t=g(e.namespace),o=t.documents.size;if(o===0)return Promise.resolve([]);const r=[...new Set(x(s))];if(r.length===0)return Promise.resolve([]);const a=t.totalLength/o,c=new Map;for(const n of r){const i=t.postings.get(n);if(!i)continue;const p=i.size,v=Math.log(1+(o-p+.5)/(p+.5));for(const[m,h]of i){const w=t.documents.get(m);if(!w)continue;const M=h+1.5*(1-.75+.75*w.length/a),S=v*(h*(1.5+1)/M);c.set(m,(c.get(m)??0)+S)}}const f=[...c.entries()].map(([n,i])=>({id:n,score:i,text:t.documents.get(n)?.text??""}));return Promise.resolve(f.toSorted((n,i)=>i.score-n.score).slice(0,e.topK))}};return d};export{k as default};
@@ -0,0 +1 @@
1
+ const o=/^\.+/u,p={avif:"image/avif",bmp:"image/bmp",gif:"image/gif",ico:"image/x-icon",jpeg:"image/jpeg",jpg:"image/jpeg",png:"image/png",svg:"image/svg+xml",tiff:"image/tiff",tif:"image/tiff",webp:"image/webp",avi:"video/x-msvideo",mkv:"video/x-matroska",mov:"video/quicktime",mp4:"video/mp4",mpeg:"video/mpeg",mpg:"video/mpeg",webm:"video/webm",wmv:"video/x-ms-wmv",aac:"audio/aac",flac:"audio/flac",m4a:"audio/mp4",mp3:"audio/mpeg",ogg:"audio/ogg",opus:"audio/opus",wav:"audio/wav",wma:"audio/x-ms-wma",csv:"text/csv",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",odp:"application/vnd.oasis.opendocument.presentation",ods:"application/vnd.oasis.opendocument.spreadsheet",odt:"application/vnd.oasis.opendocument.text",pdf:"application/pdf",ppt:"application/vnd.ms-powerpoint",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation",rtf:"application/rtf",xls:"application/vnd.ms-excel",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",css:"text/css",html:"text/html",htm:"text/html",ini:"text/plain",json:"application/json",js:"text/javascript",mjs:"text/javascript",md:"text/markdown",jsx:"text/javascript",ts:"text/typescript",tsx:"text/typescript",txt:"text/plain",xml:"application/xml",yaml:"application/x-yaml",yml:"application/x-yaml","7z":"application/x-7z-compressed",bz2:"application/x-bzip2",gz:"application/gzip",jar:"application/java-archive",rar:"application/vnd.rar",tar:"application/x-tar",zip:"application/zip",otf:"font/otf",ttf:"font/ttf",woff:"font/woff",woff2:"font/woff2",bin:"application/octet-stream",epub:"application/epub+zip",exe:"application/vnd.microsoft.portable-executable",iso:"application/x-iso9660-image",sql:"application/sql",toml:"application/toml"},e=t=>{const a=t.replace(o,"").toLowerCase();return p[a]??"application/octet-stream"},n=async t=>{const a=await crypto.subtle.digest("SHA-256",t);return[...new Uint8Array(a)].map(i=>i.toString(16).padStart(2,"0")).join("")};export{n as contentHash,e as guessMimeTypeFromExtension};
@@ -0,0 +1 @@
1
+ import{LunoraError as t}from"@lunora/errors";import{createWorkersAI as v}from"workers-ai-provider";import{buildAiGatewayMetadataFields as c,resolveAiGateway as A}from"./AI_GATEWAY_ACCOUNT_ID_ENV-JzYYnjLP.mjs";const I=(i,o,s)=>{const d=c(s);if(i!==void 0)return d!==void 0&&i.metadata===void 0?{...i,metadata:d}:i;if(o===void 0)return;const a=A(o,s);if(a!==void 0)return d===void 0?{id:a.gatewayId}:{id:a.gatewayId,metadata:d}},y=(i,o)=>v({binding:i,gateway:o}),h=i=>{const{binding:o,defaultEmbeddingModel:s,defaultModel:d,env:a,gateway:f,metadata:g,provider:m}=i;if(!m&&!o)throw new t("INTERNAL","@lunora/ai: createAi requires a `binding` (env.AI) or a pre-built `provider`");const u=I(f,a,g),n=m??y(o,u),p=e=>{if(e===void 0){if(!d)throw new t("INTERNAL","@lunora/ai: no model supplied and no `defaultModel` configured — pass a model id or an AI SDK model");return n(d)}return typeof e=="string"?n(e):e},w=e=>{const r=n.textEmbeddingModel;if(typeof r!="function")throw new t("INTERNAL","@lunora/ai: the Workers AI provider does not expose `textEmbeddingModel`; pass an AI SDK EmbeddingModel (e.g. from @ai-sdk/openai) to embed()");return r.call(n,e)};return{embeddingModel:e=>{if(typeof e=="object")return e;const r=e??s;if(!r)throw new t("INTERNAL","@lunora/ai: no embedding model supplied and no `defaultEmbeddingModel` configured — pass an embedding model id or an AI SDK EmbeddingModel");return w(r)},model:p,run:async(e,r,l)=>{if(!o)throw new t("INTERNAL","@lunora/ai: ai.run requires the `binding` (env.AI) — it is unavailable when only a custom `provider` was supplied");const b=u!==void 0&&l?.gateway===void 0?{...l,gateway:u}:l;return o.run(e,r,b)},workersai:n}};export{h as default};
@@ -0,0 +1,8 @@
1
+ import{LunoraError as x}from"@lunora/errors";import{tool as G,jsonSchema as J,embed as ee}from"ai";import te from"./fixedWindowChunks-XJRXHEoz.mjs";import ne from"./hybridRank-U6PmGuz1.mjs";const ae=8,re=async(e,s,h)=>{if(!Number.isInteger(s)||s<1)throw new RangeError("concurrentMap: `limit` must be a positive integer");if(e.length===0)return[];const b=Math.max(1,Math.min(s,e.length)),y=Array.from({length:e.length});let f=0,k=!1,v;const E=async()=>{for(;;){if(k)return;const I=f;if(f+=1,I>=e.length)return;try{y[I]=await h(e[I],I)}catch(N){k||(k=!0,v=N);return}}},m=Array.from({length:b},()=>E());if(await Promise.all(m),k)throw v;return y},oe=1e3,ie=200,se=5,ce=20,de=100,z="__ragChunk",V="__ragSource",A="__ragText",R="__ragHash",T="__ragChunks",M="__ragImportance",F="__ragModel",ue=new Set([z,T,R,M,F,V,A]),le=/^[\w.-]{1,40}$/,P=e=>e===void 0?"":`${encodeURIComponent(e)}#`,_=(e,s,h)=>`${P(e)}${s}#${String(h)}`,K=(e,s)=>{const h=P(s),b=h!==""&&e.startsWith(h)?e.slice(h.length):e,y=b.lastIndexOf("#"),f=y===-1?Number.NaN:Number(b.slice(y+1));return y===-1||!Number.isInteger(f)||f<0?{chunkIndex:0,sourceId:b}:{chunkIndex:f,sourceId:b.slice(0,y)}},me=async e=>{const s=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(e));return[...new Uint8Array(s)].map(h=>h.toString(16).padStart(2,"0")).join("")},O=e=>{if(!e)return;const s=Object.entries(e).filter(([h])=>!ue.has(h));return s.length>0?Object.fromEntries(s):void 0},Q=new Set,he=e=>{Q.has(e)||(Q.add(e),console.warn(`[@lunora/ai/rag] index "${e}" is used without a namespace — in a multi-tenant/sharded
2
+ app this shares one tenant's chunks (text included) with every other tenant, since
3
+ Vectorize indexes are account-global. Pass \`namespace\` (the shard/tenant key) on both
4
+ index() and retrieve(). Single-tenant apps suppress this via { allowSharedNamespace: true }.`))},pe=e=>e.map(s=>`[source:${s.sourceId}#${String(s.chunkIndex)}]
5
+ ${s.text}`).join(`
6
+
7
+ `),ge=(e,s)=>{if(typeof e=="object")return e;if(s===void 0)throw new x("INTERNAL","@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.");return s.embeddingModel(e)},fe=e=>{const s=e.modelId;return typeof s=="string"&&s.length>0?s:void 0},we=e=>{if(!(typeof e!="object"||e===null)){for(const s of Object.values(e))if(typeof s=="object"&&s!==null){const{cost:h}=s;if(typeof h=="number"&&Number.isFinite(h))return h}}},Ie=e=>{if(typeof e.index!="string"||e.index.length===0)throw new x("BAD_REQUEST","@lunora/ai/rag: `index` must be a non-empty Vectorize index name");const s=e.chunkSize??oe,h=e.chunkOverlap??ie;if(!Number.isInteger(s)||s<1)throw new x("BAD_REQUEST","@lunora/ai/rag: `chunkSize` must be a positive integer");if(!Number.isInteger(h)||h<0||h>=s)throw new x("BAD_REQUEST","@lunora/ai/rag: `chunkOverlap` must be a non-negative integer smaller than `chunkSize`");const b=e.topK??se;if(!Number.isInteger(b)||b<1)throw new x("BAD_REQUEST","@lunora/ai/rag: `topK` must be a positive integer");if(e.embeddingModelVersion!==void 0&&!le.test(e.embeddingModelVersion))throw new x("BAD_REQUEST",'@lunora/ai/rag: `embeddingModelVersion` must match /^[A-Za-z0-9._-]{1,40}$/ (a short, stable tag like "bge-v1.5")');const y=e.chunk??(m=>te(m,s,h)),{textStore:f}=e,k=f?de:ce,v=e.embeddingModelVersion,E=m=>v===void 0?m:m===void 0?v:`${v}::${m}`;return m=>{let I;const N=typeof m.trace=="function"?m.trace:void 0,B=async t=>{I??=ge(e.embeddingModel,m.ai);const n=I,o=async d=>{const{embedding:r,providerMetadata:u,usage:l}=await ee({model:n,value:t});if(d!==void 0){const c=l.tokens;typeof c=="number"&&Number.isFinite(c)&&d.setAttribute("gen_ai.usage.input_tokens",c);const p=we(u);p!==void 0&&d.setAttribute("gen_ai.usage.cost",p)}return r};if(N===void 0)return o();const i=fe(n),a=typeof m.conversationId=="string"&&m.conversationId.length>0?m.conversationId:void 0;return N("ai.embed",(d,r)=>o(r),{"gen_ai.operation.name":"embeddings",...i===void 0?{}:{"gen_ai.request.model":i},...a===void 0?{}:{"gen_ai.conversation.id":a}})},$=t=>{if(t===void 0){if(e.requireNamespace)throw new x("BAD_REQUEST",`@lunora/ai/rag: index "${e.index}" requires a namespace (requireNamespace is set) — pass the tenant/shard key on index()/retrieve()/remove()`);e.allowSharedNamespace||he(e.index)}},U=async(t,n)=>{const[o]=await m.vectors.getByIds(e.index,[_(n,t,0)],n),i=o?.metadata?.[R],a=o?.metadata?.[T];return{chunks:typeof a=="number"&&Number.isInteger(a)&&a>0?a:void 0,hash:typeof i=="string"?i:void 0}},j=async(t,n,o,i)=>{const a=Array.from({length:o-n},(d,r)=>_(i,t,n+r));a.length!==0&&(await m.vectors.deleteByIds(e.index,a,i),await f?.remove?.(a,{namespace:i}),await e.lexicalStore?.remove?.(a,{namespace:i}))},W=async t=>{if($(t.namespace),t.importance!==void 0&&(typeof t.importance!="number"||t.importance<0||t.importance>1))throw new x("BAD_REQUEST","@lunora/ai/rag: `importance` must be a number in [0, 1]");const n=E(t.namespace),o=await me(t.text),i=await U(t.id,n);if(i.hash===o&&i.chunks!==void 0)return{chunks:i.chunks,ids:Array.from({length:i.chunks},(r,u)=>_(n,t.id,u)),unchanged:!0};const a=y(t.text),d=a.map((r,u)=>_(n,t.id,u));if(a.length===0&&t.allowEmptySources===!1)throw new x("BAD_REQUEST",`@lunora/ai/rag: source "${t.id}" produced zero chunks — set allowEmptySources: true to allow this`);if(a.length>0){const r=a.map((u,l)=>({chunkIndex:l,id:d[l],sourceId:t.id,text:u}));f&&await f.put(r,{namespace:n}),e.lexicalStore&&await e.lexicalStore.index(r,{namespace:n})}return await re(a,ae,async(r,u)=>{const l=d[u],c={...t.metadata,[z]:u,[V]:t.id};f||(c[A]=r),t.importance!==void 0&&(c[M]=t.importance),u===0&&(c[R]=o,c[T]=a.length,v!==void 0&&(c[F]=v)),await m.vectors.upsert(e.index,{embed:B,id:l,input:r,metadata:c,namespace:n}),t.onChunk?.({chunkIndex:u,id:l,text:r,total:a.length})}),i.chunks!==void 0&&i.chunks>a.length&&await j(t.id,a.length,i.chunks,n),{chunks:a.length,ids:d,unchanged:!1}},H=async t=>{$(t.namespace);const n=E(t.namespace),o=(await U(t.id,n)).chunks??1;await j(t.id,0,o,n)},q=async(t,n)=>{const o=new Map;if(t.length===0)return o;if(f){const a=await f.getMany(t,{namespace:n});for(const[d,r]of t.entries()){const u=a[d];typeof u=="string"&&o.set(r,u)}return o}const i=await m.vectors.getByIds(e.index,t,n);for(const a of i){const d=a.metadata?.[A];typeof d=="string"&&o.set(a.id,d)}return o},L=async(t,n,o)=>{const i=n?.chunkContext?.before??0,a=n?.chunkContext?.after??0;if(i===0&&a===0)return t;if(!Number.isInteger(i)||i<0||!Number.isInteger(a)||a<0)throw new x("BAD_REQUEST","@lunora/ai/rag: `chunkContext.before`/`chunkContext.after` must be non-negative integers");const d=new Map(t.map(c=>[c.id,c.text])),r=new Set;for(const c of t)for(let p=-i;p<=a;p+=1){const w=c.chunkIndex+p,g=_(o,c.sourceId,w);p!==0&&w>=0&&!d.has(g)&&r.add(g)}const u=await q([...r],o),l=(c,p)=>{const w=_(o,c,p);return d.get(w)??u.get(w)};return t.map(c=>{const p=[];for(let w=-i;w<=a;w+=1){const g=w===0?c.text:l(c.sourceId,c.chunkIndex+w);g!==void 0&&p.push(g)}return{...c,text:p.join(`
8
+ `)}})},X=t=>{if(typeof t=="string"){const n=e.filters?.[t];if(!n)throw new x("NOT_FOUND",`@lunora/ai/rag: unknown named filter "${t}" — must be one of the keys declared in RagConfig.filters`);return n.filter}return t},Y=(t,n)=>t.matches.map(o=>{const i=o.metadata??{},a=K(o.id,n),d=i[A],r=i[M],u=typeof r=="number"&&r>=0&&r<=1?r:1;return{chunkIndex:a.chunkIndex,id:o.id,importance:u,metadata:O(i),score:o.score*u,sourceId:a.sourceId,text:typeof d=="string"?d:""}}),Z=async(t,n)=>{if(!f)return t;const o=t.map(r=>r.id),[i,a]=await Promise.all([q(o,n),m.vectors.getByIds(e.index,o,n)]),d=new Map(a.map(r=>[r.id,r.metadata]));return t.flatMap(r=>{const u=i.get(r.id);if(u===void 0)return[];const l=d.get(r.id),c=l?.[M],p=typeof c=="number"&&c>=0&&c<=1?c:r.importance,w=r.score/r.importance*p;return[{...r,importance:p,metadata:O(l)??r.metadata,score:w,text:u}]})},C=async(t,n)=>{$(n?.namespace);const o=E(n?.namespace),i=X(n?.filter),a=e.rlsFilter?await e.rlsFilter(m.auth):void 0,d=a?{...i,...a}:i,r=Math.min(n?.topK??b,k),u=await m.vectors.query(e.index,{embed:B,filter:d,input:t,namespace:o,returnMetadata:f?"indexed":"all",topK:r});let l=await Z(Y(u,o),o);const c=n?.minScore;if(c!==void 0&&(l=l.filter(g=>g.score>=c)),e.lexicalStore){const g=(await e.lexicalStore.search(t,{filter:d,namespace:o,topK:e.lexicalTopK??r})).map(S=>{const D=K(S.id,o);return{chunkIndex:D.chunkIndex,id:S.id,importance:1,metadata:void 0,score:S.score,sourceId:D.sourceId,text:S.text}});l=[...ne(l,g)]}l.sort((g,S)=>S.score-g.score),l=[...await L(l,n,o)];const p=[],w=new Set;for(const g of l)w.has(g.sourceId)||(w.add(g.sourceId),p.push({id:g.sourceId,metadata:g.metadata,weight:g.importance}));return n?.onRetrieve?.({matches:l.length,query:t}),{chunks:l,context:pe(l),sources:p}};return{asTool:t=>G({description:t?.description??`Search the "${e.index}" knowledge base for passages relevant to a natural-language query.`,execute:async({query:n})=>C(n,{namespace:t?.namespace,topK:t?.topK}),inputSchema:J({properties:{query:{description:"The natural-language search query.",type:"string"}},required:["query"],type:"object"})}),index:W,remove:H,retrieve:C}}};export{Ie as default};
@@ -0,0 +1 @@
1
+ const a=(s,e,i)=>{if(!Number.isInteger(e)||e<1)throw new RangeError("fixedWindowChunks: `size` must be a positive integer");if(!Number.isInteger(i)||i<0||i>=e)throw new RangeError("fixedWindowChunks: `overlap` must be a non-negative integer smaller than `size`");const t=s.trim();if(t.length===0)return[];if(t.length<=e)return[t];const o=Math.max(1,e-i),n=[];for(let r=0;r<t.length&&(n.push(t.slice(r,r+e)),!(r+e>=t.length));r+=o);return n};export{a as default};
@@ -0,0 +1 @@
1
+ const a=(c,s,o=60)=>{const n=new Map;for(const[e,t]of c.entries())n.set(t.id,{chunk:t,score:1/(o+e),vectorRank:e});for(const[e,t]of s.entries()){const r=n.get(t.id);r?r.score+=1/(o+e):n.set(t.id,{chunk:t,score:1/(o+e),vectorRank:Number.POSITIVE_INFINITY})}return[...n.values()].toSorted((e,t)=>{const r=t.score-e.score;return r===0?e.vectorRank-t.vectorRank:r}).map(e=>e.chunk)};export{a as default};
@@ -1,5 +1 @@
1
- export { default as fixedWindowChunks } from '../packem_shared/fixedWindowChunks-J-WfQDw9.mjs';
2
- export { default as defineRag } from '../packem_shared/defineRag-DmczmIDj.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-BQzWLMqX.mjs';
1
+ import{default as r}from"../packem_shared/fixedWindowChunks-XJRXHEoz.mjs";import{default as a}from"../packem_shared/defineRag-BF3RwHZj.mjs";import{contentHash as s,guessMimeTypeFromExtension as d}from"../packem_shared/contentHash-BIn6ECP8.mjs";import{default as n}from"../packem_shared/hybridRank-U6PmGuz1.mjs";import{default as i}from"../packem_shared/bm25LexicalStore-RA9sesFC.mjs";export{i as bm25LexicalStore,s as contentHash,a as defineRag,r as fixedWindowChunks,d as guessMimeTypeFromExtension,n as hybridRank};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/ai",
3
- "version": "1.0.0-alpha.25",
3
+ "version": "1.0.0-alpha.26",
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",
@@ -54,12 +54,12 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@lunora/errors": "1.0.0-alpha.8",
57
- "ai": "7.0.31",
58
- "workers-ai-provider": "3.3.1"
57
+ "ai": "7.0.37",
58
+ "workers-ai-provider": "4.0.0"
59
59
  },
60
60
  "peerDependencies": {
61
- "@ai-sdk/anthropic": "4.0.16",
62
- "@ai-sdk/openai": "4.0.16"
61
+ "@ai-sdk/anthropic": "4.0.20",
62
+ "@ai-sdk/openai": "4.0.20"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "@ai-sdk/anthropic": {
@@ -1,48 +0,0 @@
1
- const readEnv = (env, key) => {
2
- const value = env[key];
3
- return typeof value === "string" && value.length > 0 ? value : void 0;
4
- };
5
- const buildAiGatewayMetadataFields = (metadata) => {
6
- if (metadata === void 0) {
7
- return void 0;
8
- }
9
- const fields = {};
10
- if (typeof metadata.functionPath === "string" && metadata.functionPath.length > 0) {
11
- fields["functionPath"] = metadata.functionPath;
12
- }
13
- if (typeof metadata.traceId === "string" && metadata.traceId.length > 0) {
14
- fields["traceId"] = metadata.traceId;
15
- }
16
- return Object.keys(fields).length > 0 ? fields : void 0;
17
- };
18
- const encodeMetadata = (metadata) => {
19
- const fields = buildAiGatewayMetadataFields(metadata);
20
- return fields === void 0 ? void 0 : JSON.stringify(fields);
21
- };
22
- const AI_GATEWAY_ACCOUNT_ID_ENV = "LUNORA_AI_GATEWAY_ACCOUNT_ID";
23
- const AI_GATEWAY_ID_ENV = "LUNORA_AI_GATEWAY_ID";
24
- const AI_GATEWAY_TOKEN_ENV = "LUNORA_AI_GATEWAY_TOKEN";
25
- const resolveAiGateway = (env, metadata) => {
26
- const accountId = readEnv(env, AI_GATEWAY_ACCOUNT_ID_ENV);
27
- const gatewayId = readEnv(env, AI_GATEWAY_ID_ENV);
28
- if (accountId === void 0 || gatewayId === void 0) {
29
- return void 0;
30
- }
31
- const token = readEnv(env, AI_GATEWAY_TOKEN_ENV);
32
- const headers = {};
33
- if (token !== void 0) {
34
- headers["cf-aig-authorization"] = `Bearer ${token}`;
35
- }
36
- const metadataHeader = encodeMetadata(metadata);
37
- if (metadataHeader !== void 0) {
38
- headers["cf-aig-metadata"] = metadataHeader;
39
- }
40
- return {
41
- accountId,
42
- baseURL: `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}`,
43
- gatewayId,
44
- headers
45
- };
46
- };
47
-
48
- export { AI_GATEWAY_ACCOUNT_ID_ENV, AI_GATEWAY_ID_ENV, AI_GATEWAY_TOKEN_ENV, buildAiGatewayMetadataFields, resolveAiGateway };
@@ -1,117 +0,0 @@
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 };
@@ -1,92 +0,0 @@
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,72 +0,0 @@
1
- import { LunoraError } from '@lunora/errors';
2
- import { createWorkersAI } from 'workers-ai-provider';
3
- import { buildAiGatewayMetadataFields, resolveAiGateway } from './AI_GATEWAY_ACCOUNT_ID_ENV-CIAjehQF.mjs';
4
-
5
- const resolveGatewayOption = (gateway, env, metadata) => {
6
- const metadataFields = buildAiGatewayMetadataFields(metadata);
7
- if (gateway !== void 0) {
8
- return metadataFields !== void 0 && gateway.metadata === void 0 ? { ...gateway, metadata: metadataFields } : gateway;
9
- }
10
- if (env === void 0) {
11
- return void 0;
12
- }
13
- const resolved = resolveAiGateway(env, metadata);
14
- if (resolved === void 0) {
15
- return void 0;
16
- }
17
- return metadataFields === void 0 ? { id: resolved.gatewayId } : { id: resolved.gatewayId, metadata: metadataFields };
18
- };
19
- const buildProvider = (binding, gateway) => createWorkersAI({ binding, gateway });
20
- const createAi = (options) => {
21
- const { binding, defaultEmbeddingModel, defaultModel, env, gateway, metadata, provider } = options;
22
- if (!provider && !binding) {
23
- throw new LunoraError("INTERNAL", "@lunora/ai: createAi requires a `binding` (env.AI) or a pre-built `provider`");
24
- }
25
- const resolvedGateway = resolveGatewayOption(gateway, env, metadata);
26
- const workersai = provider ?? buildProvider(binding, resolvedGateway);
27
- const model = (input) => {
28
- if (input === void 0) {
29
- if (!defaultModel) {
30
- throw new LunoraError("INTERNAL", "@lunora/ai: no model supplied and no `defaultModel` configured — pass a model id or an AI SDK model");
31
- }
32
- return workersai(defaultModel);
33
- }
34
- return typeof input === "string" ? workersai(input) : input;
35
- };
36
- const resolveEmbeddingModel = (modelId) => {
37
- const factory = workersai.textEmbeddingModel;
38
- if (typeof factory !== "function") {
39
- throw new LunoraError(
40
- "INTERNAL",
41
- "@lunora/ai: the Workers AI provider does not expose `textEmbeddingModel`; pass an AI SDK EmbeddingModel (e.g. from @ai-sdk/openai) to embed()"
42
- );
43
- }
44
- return factory.call(workersai, modelId);
45
- };
46
- const embeddingModel = (input) => {
47
- if (typeof input === "object") {
48
- return input;
49
- }
50
- const modelId = input ?? defaultEmbeddingModel;
51
- if (!modelId) {
52
- throw new LunoraError(
53
- "INTERNAL",
54
- "@lunora/ai: no embedding model supplied and no `defaultEmbeddingModel` configured — pass an embedding model id or an AI SDK EmbeddingModel"
55
- );
56
- }
57
- return resolveEmbeddingModel(modelId);
58
- };
59
- const run = async (modelId, inputs, runOptions) => {
60
- if (!binding) {
61
- throw new LunoraError(
62
- "INTERNAL",
63
- "@lunora/ai: ai.run requires the `binding` (env.AI) — it is unavailable when only a custom `provider` was supplied"
64
- );
65
- }
66
- const mergedOptions = resolvedGateway !== void 0 && runOptions?.gateway === void 0 ? { ...runOptions, gateway: resolvedGateway } : runOptions;
67
- return binding.run(modelId, inputs, mergedOptions);
68
- };
69
- return { embeddingModel, model, run, workersai };
70
- };
71
-
72
- export { createAi as default };
@@ -1,474 +0,0 @@
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
- if (items.length === 0) {
12
- return [];
13
- }
14
- const effectiveLimit = Math.max(1, Math.min(limit, items.length));
15
- const results = Array.from({ length: items.length });
16
- let cursor = 0;
17
- let failed = false;
18
- let firstError;
19
- const worker = async () => {
20
- for (; ; ) {
21
- if (failed) {
22
- return;
23
- }
24
- const index = cursor;
25
- cursor += 1;
26
- if (index >= items.length) {
27
- return;
28
- }
29
- try {
30
- results[index] = await function_(items[index], index);
31
- } catch (error) {
32
- if (!failed) {
33
- failed = true;
34
- firstError = error;
35
- }
36
- return;
37
- }
38
- }
39
- };
40
- const workers = Array.from({ length: effectiveLimit }, () => worker());
41
- await Promise.all(workers);
42
- if (failed) {
43
- throw firstError;
44
- }
45
- return results;
46
- };
47
-
48
- const DEFAULT_CHUNK_SIZE = 1e3;
49
- const DEFAULT_CHUNK_OVERLAP = 200;
50
- const DEFAULT_TOP_K = 5;
51
- const MAX_TOP_K_FULL_METADATA = 20;
52
- const MAX_TOP_K = 100;
53
- const CHUNK_INDEX_KEY = "__ragChunk";
54
- const SOURCE_KEY = "__ragSource";
55
- const TEXT_KEY = "__ragText";
56
- const HASH_KEY = "__ragHash";
57
- const COUNT_KEY = "__ragChunks";
58
- const IMPORTANCE_KEY = "__ragImportance";
59
- const MODEL_KEY = "__ragModel";
60
- const INTERNAL_KEYS = /* @__PURE__ */ new Set([CHUNK_INDEX_KEY, COUNT_KEY, HASH_KEY, IMPORTANCE_KEY, MODEL_KEY, SOURCE_KEY, TEXT_KEY]);
61
- const MODEL_VERSION_PATTERN = /^[\w.-]{1,40}$/;
62
- const namespacePrefix = (namespace) => namespace === void 0 ? "" : `${encodeURIComponent(namespace)}#`;
63
- const chunkVectorId = (namespace, sourceId, chunkIndex) => `${namespacePrefix(namespace)}${sourceId}#${String(chunkIndex)}`;
64
- const parseChunkVectorId = (id, namespace) => {
65
- const prefix = namespacePrefix(namespace);
66
- const body = prefix !== "" && id.startsWith(prefix) ? id.slice(prefix.length) : id;
67
- const separator = body.lastIndexOf("#");
68
- const chunkIndex = separator === -1 ? Number.NaN : Number(body.slice(separator + 1));
69
- if (separator === -1 || !Number.isInteger(chunkIndex) || chunkIndex < 0) {
70
- return { chunkIndex: 0, sourceId: body };
71
- }
72
- return { chunkIndex, sourceId: body.slice(0, separator) };
73
- };
74
- const sha256Hex = async (text) => {
75
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
76
- return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
77
- };
78
- const userMetadataOf = (metadata) => {
79
- if (!metadata) {
80
- return void 0;
81
- }
82
- const entries = Object.entries(metadata).filter(([key]) => !INTERNAL_KEYS.has(key));
83
- return entries.length > 0 ? Object.fromEntries(entries) : void 0;
84
- };
85
- const sharedNamespaceWarned = /* @__PURE__ */ new Set();
86
- const warnSharedNamespace = (indexName) => {
87
- if (sharedNamespaceWarned.has(indexName)) {
88
- return;
89
- }
90
- sharedNamespaceWarned.add(indexName);
91
- console.warn(
92
- `[@lunora/ai/rag] index "${indexName}" is used without a namespace — in a multi-tenant/sharded
93
- app this shares one tenant's chunks (text included) with every other tenant, since
94
- Vectorize indexes are account-global. Pass \`namespace\` (the shard/tenant key) on both
95
- index() and retrieve(). Single-tenant apps suppress this via { allowSharedNamespace: true }.`
96
- );
97
- };
98
- const assembleContext = (chunks) => chunks.map((chunk) => `[source:${chunk.sourceId}#${String(chunk.chunkIndex)}]
99
- ${chunk.text}`).join("\n\n");
100
- const resolveEmbeddingModel = (input, ai) => {
101
- if (typeof input === "object") {
102
- return input;
103
- }
104
- if (ai === void 0) {
105
- throw new LunoraError(
106
- "INTERNAL",
107
- "@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."
108
- );
109
- }
110
- return ai.embeddingModel(input);
111
- };
112
- const modelIdOf = (model) => {
113
- const id = model.modelId;
114
- return typeof id === "string" && id.length > 0 ? id : void 0;
115
- };
116
- const embedCostOf = (providerMetadata) => {
117
- if (typeof providerMetadata !== "object" || providerMetadata === null) {
118
- return void 0;
119
- }
120
- for (const bag of Object.values(providerMetadata)) {
121
- if (typeof bag === "object" && bag !== null) {
122
- const { cost } = bag;
123
- if (typeof cost === "number" && Number.isFinite(cost)) {
124
- return cost;
125
- }
126
- }
127
- }
128
- return void 0;
129
- };
130
- const defineRag = (config) => {
131
- if (typeof config.index !== "string" || config.index.length === 0) {
132
- throw new LunoraError("BAD_REQUEST", "@lunora/ai/rag: `index` must be a non-empty Vectorize index name");
133
- }
134
- const chunkSize = config.chunkSize ?? DEFAULT_CHUNK_SIZE;
135
- const chunkOverlap = config.chunkOverlap ?? DEFAULT_CHUNK_OVERLAP;
136
- if (!Number.isInteger(chunkSize) || chunkSize < 1) {
137
- throw new LunoraError("BAD_REQUEST", "@lunora/ai/rag: `chunkSize` must be a positive integer");
138
- }
139
- if (!Number.isInteger(chunkOverlap) || chunkOverlap < 0 || chunkOverlap >= chunkSize) {
140
- throw new LunoraError("BAD_REQUEST", "@lunora/ai/rag: `chunkOverlap` must be a non-negative integer smaller than `chunkSize`");
141
- }
142
- const defaultTopK = config.topK ?? DEFAULT_TOP_K;
143
- if (!Number.isInteger(defaultTopK) || defaultTopK < 1) {
144
- throw new LunoraError("BAD_REQUEST", "@lunora/ai/rag: `topK` must be a positive integer");
145
- }
146
- if (config.embeddingModelVersion !== void 0 && !MODEL_VERSION_PATTERN.test(config.embeddingModelVersion)) {
147
- throw new LunoraError(
148
- "BAD_REQUEST",
149
- '@lunora/ai/rag: `embeddingModelVersion` must match /^[A-Za-z0-9._-]{1,40}$/ (a short, stable tag like "bge-v1.5")'
150
- );
151
- }
152
- const splitter = config.chunk ?? ((text) => fixedWindowChunks(text, chunkSize, chunkOverlap));
153
- const { textStore } = config;
154
- const topKCeiling = textStore ? MAX_TOP_K : MAX_TOP_K_FULL_METADATA;
155
- const modelTag = config.embeddingModelVersion;
156
- const withModelTag = (namespace) => {
157
- if (modelTag === void 0) {
158
- return namespace;
159
- }
160
- return namespace === void 0 ? modelTag : `${modelTag}::${namespace}`;
161
- };
162
- return (context) => {
163
- let model;
164
- const tracer = typeof context.trace === "function" ? context.trace : void 0;
165
- const embedText = async (text) => {
166
- model ??= resolveEmbeddingModel(config.embeddingModel, context.ai);
167
- const resolvedModel = model;
168
- const run = async (span) => {
169
- const { embedding, providerMetadata, usage } = await embed({ model: resolvedModel, value: text });
170
- if (span !== void 0) {
171
- const inputTokens = usage.tokens;
172
- if (typeof inputTokens === "number" && Number.isFinite(inputTokens)) {
173
- span.setAttribute("gen_ai.usage.input_tokens", inputTokens);
174
- }
175
- const cost = embedCostOf(providerMetadata);
176
- if (cost !== void 0) {
177
- span.setAttribute("gen_ai.usage.cost", cost);
178
- }
179
- }
180
- return embedding;
181
- };
182
- if (tracer === void 0) {
183
- return run();
184
- }
185
- const modelId = modelIdOf(resolvedModel);
186
- const conversationId = typeof context.conversationId === "string" && context.conversationId.length > 0 ? context.conversationId : void 0;
187
- return tracer("ai.embed", (_trace, span) => run(span), {
188
- "gen_ai.operation.name": "embeddings",
189
- ...modelId === void 0 ? {} : { "gen_ai.request.model": modelId },
190
- // Session/thread grouping — absent unless a conversation id was set.
191
- ...conversationId === void 0 ? {} : { "gen_ai.conversation.id": conversationId }
192
- });
193
- };
194
- const checkNamespace = (namespace) => {
195
- if (namespace !== void 0) {
196
- return;
197
- }
198
- if (config.requireNamespace) {
199
- throw new LunoraError(
200
- "BAD_REQUEST",
201
- `@lunora/ai/rag: index "${config.index}" requires a namespace (requireNamespace is set) — pass the tenant/shard key on index()/retrieve()/remove()`
202
- );
203
- }
204
- if (!config.allowSharedNamespace) {
205
- warnSharedNamespace(config.index);
206
- }
207
- };
208
- const readHead = async (sourceId, namespace) => {
209
- const [head] = await context.vectors.getByIds(config.index, [chunkVectorId(namespace, sourceId, 0)], namespace);
210
- const hash = head?.metadata?.[HASH_KEY];
211
- const chunks = head?.metadata?.[COUNT_KEY];
212
- return {
213
- chunks: typeof chunks === "number" && Number.isInteger(chunks) && chunks > 0 ? chunks : void 0,
214
- hash: typeof hash === "string" ? hash : void 0
215
- };
216
- };
217
- const deleteChunkRange = async (sourceId, from, to, namespace) => {
218
- const ids = Array.from({ length: to - from }, (_, offset) => chunkVectorId(namespace, sourceId, from + offset));
219
- if (ids.length === 0) {
220
- return;
221
- }
222
- await context.vectors.deleteByIds(config.index, ids, namespace);
223
- await textStore?.remove?.(ids, { namespace });
224
- await config.lexicalStore?.remove?.(ids, { namespace });
225
- };
226
- const index = async (input) => {
227
- checkNamespace(input.namespace);
228
- if (input.importance !== void 0 && (typeof input.importance !== "number" || input.importance < 0 || input.importance > 1)) {
229
- throw new LunoraError("BAD_REQUEST", "@lunora/ai/rag: `importance` must be a number in [0, 1]");
230
- }
231
- const effectiveNamespace = withModelTag(input.namespace);
232
- const hash = await sha256Hex(input.text);
233
- const previous = await readHead(input.id, effectiveNamespace);
234
- if (previous.hash === hash && previous.chunks !== void 0) {
235
- return {
236
- chunks: previous.chunks,
237
- ids: Array.from({ length: previous.chunks }, (_, chunkIndex) => chunkVectorId(effectiveNamespace, input.id, chunkIndex)),
238
- unchanged: true
239
- };
240
- }
241
- const pieces = splitter(input.text);
242
- const ids = pieces.map((_, chunkIndex) => chunkVectorId(effectiveNamespace, input.id, chunkIndex));
243
- if (pieces.length === 0 && input.allowEmptySources === false) {
244
- throw new LunoraError("BAD_REQUEST", `@lunora/ai/rag: source "${input.id}" produced zero chunks — set allowEmptySources: true to allow this`);
245
- }
246
- if (pieces.length > 0) {
247
- const storedChunks = pieces.map((text, chunkIndex) => {
248
- return { chunkIndex, id: ids[chunkIndex], sourceId: input.id, text };
249
- });
250
- if (textStore) {
251
- await textStore.put(storedChunks, { namespace: effectiveNamespace });
252
- }
253
- if (config.lexicalStore) {
254
- await config.lexicalStore.index(storedChunks, { namespace: effectiveNamespace });
255
- }
256
- }
257
- await concurrentMap(pieces, INDEX_CONCURRENCY, async (piece, chunkIndex) => {
258
- const id = ids[chunkIndex];
259
- const metadata = {
260
- ...input.metadata,
261
- [CHUNK_INDEX_KEY]: chunkIndex,
262
- [SOURCE_KEY]: input.id
263
- };
264
- if (!textStore) {
265
- metadata[TEXT_KEY] = piece;
266
- }
267
- if (input.importance !== void 0) {
268
- metadata[IMPORTANCE_KEY] = input.importance;
269
- }
270
- if (chunkIndex === 0) {
271
- metadata[HASH_KEY] = hash;
272
- metadata[COUNT_KEY] = pieces.length;
273
- if (modelTag !== void 0) {
274
- metadata[MODEL_KEY] = modelTag;
275
- }
276
- }
277
- await context.vectors.upsert(config.index, {
278
- embed: embedText,
279
- id,
280
- input: piece,
281
- metadata,
282
- namespace: effectiveNamespace
283
- });
284
- input.onChunk?.({ chunkIndex, id, text: piece, total: pieces.length });
285
- });
286
- if (previous.chunks !== void 0 && previous.chunks > pieces.length) {
287
- await deleteChunkRange(input.id, pieces.length, previous.chunks, effectiveNamespace);
288
- }
289
- return { chunks: pieces.length, ids, unchanged: false };
290
- };
291
- const remove = async (input) => {
292
- checkNamespace(input.namespace);
293
- const effectiveNamespace = withModelTag(input.namespace);
294
- const previous = await readHead(input.id, effectiveNamespace);
295
- const totalChunks = previous.chunks ?? 1;
296
- await deleteChunkRange(input.id, 0, totalChunks, effectiveNamespace);
297
- };
298
- const textsByIds = async (ids, namespace) => {
299
- const texts = /* @__PURE__ */ new Map();
300
- if (ids.length === 0) {
301
- return texts;
302
- }
303
- if (textStore) {
304
- const stored = await textStore.getMany(ids, { namespace });
305
- for (const [position, id] of ids.entries()) {
306
- const text = stored[position];
307
- if (typeof text === "string") {
308
- texts.set(id, text);
309
- }
310
- }
311
- return texts;
312
- }
313
- const records = await context.vectors.getByIds(config.index, ids, namespace);
314
- for (const record of records) {
315
- const text = record.metadata?.[TEXT_KEY];
316
- if (typeof text === "string") {
317
- texts.set(record.id, text);
318
- }
319
- }
320
- return texts;
321
- };
322
- const expandChunks = async (chunks, options, effectiveNamespace) => {
323
- const before = options?.chunkContext?.before ?? 0;
324
- const after = options?.chunkContext?.after ?? 0;
325
- if (before === 0 && after === 0) {
326
- return chunks;
327
- }
328
- if (!Number.isInteger(before) || before < 0 || !Number.isInteger(after) || after < 0) {
329
- throw new LunoraError("BAD_REQUEST", "@lunora/ai/rag: `chunkContext.before`/`chunkContext.after` must be non-negative integers");
330
- }
331
- const known = new Map(chunks.map((chunk) => [chunk.id, chunk.text]));
332
- const neighbourIds = /* @__PURE__ */ new Set();
333
- for (const chunk of chunks) {
334
- for (let offset = -before; offset <= after; offset += 1) {
335
- const neighbourIndex = chunk.chunkIndex + offset;
336
- const id = chunkVectorId(effectiveNamespace, chunk.sourceId, neighbourIndex);
337
- if (offset !== 0 && neighbourIndex >= 0 && !known.has(id)) {
338
- neighbourIds.add(id);
339
- }
340
- }
341
- }
342
- const neighbourTexts = await textsByIds([...neighbourIds], effectiveNamespace);
343
- const textOf = (sourceId, chunkIndex) => {
344
- const id = chunkVectorId(effectiveNamespace, sourceId, chunkIndex);
345
- return known.get(id) ?? neighbourTexts.get(id);
346
- };
347
- return chunks.map((chunk) => {
348
- const parts = [];
349
- for (let offset = -before; offset <= after; offset += 1) {
350
- const text = offset === 0 ? chunk.text : textOf(chunk.sourceId, chunk.chunkIndex + offset);
351
- if (text !== void 0) {
352
- parts.push(text);
353
- }
354
- }
355
- return { ...chunk, text: parts.join("\n") };
356
- });
357
- };
358
- const resolveFilter = (filter) => {
359
- if (typeof filter === "string") {
360
- const resolved = config.filters?.[filter];
361
- if (!resolved) {
362
- throw new LunoraError(
363
- "NOT_FOUND",
364
- `@lunora/ai/rag: unknown named filter "${filter}" — must be one of the keys declared in RagConfig.filters`
365
- );
366
- }
367
- return resolved.filter;
368
- }
369
- return filter;
370
- };
371
- const parseMatches = (result, namespace) => result.matches.map((match) => {
372
- const metadata = match.metadata ?? {};
373
- const parsed = parseChunkVectorId(match.id, namespace);
374
- const rawText = metadata[TEXT_KEY];
375
- const rawImportance = metadata[IMPORTANCE_KEY];
376
- const importance = typeof rawImportance === "number" && rawImportance >= 0 && rawImportance <= 1 ? rawImportance : 1;
377
- return {
378
- chunkIndex: parsed.chunkIndex,
379
- id: match.id,
380
- importance,
381
- metadata: userMetadataOf(metadata),
382
- score: match.score * importance,
383
- sourceId: parsed.sourceId,
384
- text: typeof rawText === "string" ? rawText : ""
385
- };
386
- });
387
- const hydrateFromStore = async (chunks, namespace) => {
388
- if (!textStore) {
389
- return chunks;
390
- }
391
- const ids = chunks.map((chunk) => chunk.id);
392
- const [texts, records] = await Promise.all([textsByIds(ids, namespace), context.vectors.getByIds(config.index, ids, namespace)]);
393
- const fullMetadataById = new Map(records.map((record) => [record.id, record.metadata]));
394
- return chunks.flatMap((chunk) => {
395
- const text = texts.get(chunk.id);
396
- if (text === void 0) {
397
- return [];
398
- }
399
- const fullMetadata = fullMetadataById.get(chunk.id);
400
- const rawImportance = fullMetadata?.[IMPORTANCE_KEY];
401
- const importance = typeof rawImportance === "number" && rawImportance >= 0 && rawImportance <= 1 ? rawImportance : chunk.importance;
402
- const score = chunk.score / chunk.importance * importance;
403
- return [{ ...chunk, importance, metadata: userMetadataOf(fullMetadata) ?? chunk.metadata, score, text }];
404
- });
405
- };
406
- const retrieve = async (query, options) => {
407
- checkNamespace(options?.namespace);
408
- const effectiveNamespace = withModelTag(options?.namespace);
409
- const resolvedFilter = resolveFilter(options?.filter);
410
- const rlsFilter = config.rlsFilter ? await config.rlsFilter(context.auth) : void 0;
411
- const effectiveFilter = rlsFilter ? { ...resolvedFilter, ...rlsFilter } : resolvedFilter;
412
- const topK = Math.min(options?.topK ?? defaultTopK, topKCeiling);
413
- const vectorResult = await context.vectors.query(config.index, {
414
- embed: embedText,
415
- filter: effectiveFilter,
416
- input: query,
417
- namespace: effectiveNamespace,
418
- returnMetadata: textStore ? "indexed" : "all",
419
- topK
420
- });
421
- let chunks = await hydrateFromStore(parseMatches(vectorResult, effectiveNamespace), effectiveNamespace);
422
- const minScore = options?.minScore;
423
- if (minScore !== void 0) {
424
- chunks = chunks.filter((chunk) => chunk.score >= minScore);
425
- }
426
- if (config.lexicalStore) {
427
- const lexicalMatches = await config.lexicalStore.search(query, {
428
- filter: effectiveFilter,
429
- namespace: effectiveNamespace,
430
- topK: config.lexicalTopK ?? topK
431
- });
432
- const lexicalChunks = lexicalMatches.map((match) => {
433
- const parsed = parseChunkVectorId(match.id, effectiveNamespace);
434
- return {
435
- chunkIndex: parsed.chunkIndex,
436
- id: match.id,
437
- importance: 1,
438
- metadata: void 0,
439
- score: match.score,
440
- sourceId: parsed.sourceId,
441
- text: match.text
442
- };
443
- });
444
- chunks = [...hybridRank(chunks, lexicalChunks)];
445
- }
446
- chunks.sort((a, b) => b.score - a.score);
447
- chunks = [...await expandChunks(chunks, options, effectiveNamespace)];
448
- const sources = [];
449
- const seen = /* @__PURE__ */ new Set();
450
- for (const chunk of chunks) {
451
- if (!seen.has(chunk.sourceId)) {
452
- seen.add(chunk.sourceId);
453
- sources.push({ id: chunk.sourceId, metadata: chunk.metadata, weight: chunk.importance });
454
- }
455
- }
456
- options?.onRetrieve?.({ matches: chunks.length, query });
457
- return { chunks, context: assembleContext(chunks), sources };
458
- };
459
- const asTool = (options) => tool({
460
- description: options?.description ?? `Search the "${config.index}" knowledge base for passages relevant to a natural-language query.`,
461
- execute: async ({ query }) => retrieve(query, { namespace: options?.namespace, topK: options?.topK }),
462
- inputSchema: jsonSchema({
463
- properties: {
464
- query: { description: "The natural-language search query.", type: "string" }
465
- },
466
- required: ["query"],
467
- type: "object"
468
- })
469
- });
470
- return { asTool, index, remove, retrieve };
471
- };
472
- };
473
-
474
- export { defineRag as default };
@@ -1,26 +0,0 @@
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 };
@@ -1,20 +0,0 @@
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 };