@lunora/ai 1.0.0-alpha.89 → 1.0.0-alpha.90
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/packem_shared/defineRag-DK8Ifpn_.mjs +8 -0
- package/dist/packem_shared/hybridRank-B4skyCLx.mjs +1 -0
- package/dist/rag/index.d.mts +134 -11
- package/dist/rag/index.d.ts +134 -11
- package/dist/rag/index.mjs +1 -1
- package/package.json +2 -2
- package/dist/packem_shared/defineRag-wBDjkuHP.mjs +0 -8
- package/dist/packem_shared/hybridRank-DejmVw2I.mjs +0 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import{LunoraError as w,isLunoraError as Ne}from"@lunora/errors";import{tool as Ae,jsonSchema as Me,embedMany as Oe,embed as De}from"ai";import{estimateModelCost as Re}from"./DEFAULT_MODEL_PRICES-Q8uxdiuV.mjs";import Ce from"./fixedWindowChunks-C461ahRE.mjs";import{c as $e,I as Be}from"./concurrent-C6nqBv41.mjs";import{contentHash as Ke}from"./contentHash-BIn6ECP8.mjs";import{hybridRank as Ue}from"./hybridRank-B4skyCLx.mjs";import{VECTORIZE_CAPABILITIES as ce,vectorizeStore as Fe}from"./VECTORIZE_CAPABILITIES-CUQDoxis.mjs";const Qe=/["\\\u0000-\u001F\uD800-\uDFFF]/,de=e=>Qe.test(e)?JSON.stringify(e):`"${e}"`,Y=e=>{if(e===void 0)return"null";if(typeof e=="bigint")throw new TypeError("stableStringify: cannot use a bigint in a stable JSON cache key — pass it as a string, or use stableWireKey");if(typeof e=="number"){if(Number.isNaN(e))return"nan";if(e===1/0)return"inf";if(e===-1/0)return"-inf";if(Object.is(e,-0))return"-0"}if(typeof e=="string")return de(e);if(e===null||typeof e!="object")return JSON.stringify(e);if(Array.isArray(e)){let v="[";for(let T=0;T<e.length;T++)T>0&&(v+=","),v+=Y(e[T]);return v+"]"}const c=Object.getPrototypeOf(e);if(c!==null&&c!==Object.prototype){const v=e.constructor?.name??"value";throw new TypeError(`stableStringify: cannot use a ${v} in a stable JSON cache key — only plain objects, arrays, and JSON primitives are supported (wire-typed values key via stableWireKey)`)}const m=e,p=Object.keys(m).sort();let f="{",k=!0;for(const v of p){const T=m[v];T!==void 0&&(k?k=!1:f+=",",f+=de(v),f+=":",f+=Y(T))}return f+"}"},Ve=1e3,je=200,Le=5,ze=4,ue=ce.maxMetadataBytes===!1?Number.POSITIVE_INFINITY:ce.maxMetadataBytes,Pe=2*1024,fe="__ragChunk",pe="__ragSource",$="__ragText",q="__ragHash",H="__ragChunks",U="__ragImportance",ge="__ragModel",Ye=new Set([fe,H,q,U,ge,pe,$]),qe=(e,c,m,p)=>{if(p===!1)return;const f=new TextEncoder().encode(JSON.stringify(e)).length;if(f<=p)return;const v=(typeof e[$]=="string"?new TextEncoder().encode(e[$]).length:0)*2>f?"lower `chunkSize` (it counts characters, not bytes — multibyte text costs up to 3 bytes each), or supply `textStore` to move chunk text out of metadata entirely":"attach less per-source `metadata`";throw new w("BAD_REQUEST",`@lunora/ai/rag: chunk ${String(c)} of "${m}" carries ${String(f)} bytes of metadata, over the store's ${String(p)}-byte per-vector ceiling — ${v}`)},He=(e,c,m)=>{if(m===!1)return;const p=new TextEncoder().encode(e).length;if(!(p<=m))throw new w("BAD_REQUEST",`@lunora/ai/rag: chunk id "${e}" for source "${c}" is ${String(p)} bytes, over the store's ${String(m)}-byte per-vector id ceiling — shorten the source id (hash long keys before indexing them) or shorten the \`namespace\`, which is prefixed onto every chunk id`)},We=/^[\w.-]{1,40}$/,be=e=>e===void 0?"":`${encodeURIComponent(e)}#`,C=(e,c,m)=>`${be(e)}${c}#${String(m)}`,le=(e,c)=>{const m=be(c),p=m!==""&&e.startsWith(m)?e.slice(m.length):e,f=p.lastIndexOf("#"),k=f===-1?Number.NaN:Number(p.slice(f+1));return f===-1||!Number.isInteger(k)||k<0?{chunkIndex:0,sourceId:p}:{chunkIndex:k,sourceId:p.slice(0,f)}},Je=async e=>Ke(new TextEncoder().encode(e)),Ze=e=>{try{return Y([e.text,e.metadata,e.importance])}catch{return}},Xe=(e,c)=>{const m=[],p=[];for(const f of e)f.score>=c?m.push(f):p.push(f.id);return{kept:m,rejectedIds:p}},Ge=e=>e!==void 0&&Object.keys(e).length>0,z=e=>{if(!e)return;const c=Object.entries(e).filter(([m])=>!Ye.has(m));return c.length>0?Object.fromEntries(c):void 0},me=new Set,et=e=>{me.has(e)||(me.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 }.`))},tt=e=>e.map(c=>`[source:${c.sourceId}#${String(c.chunkIndex)}]
|
|
5
|
+
${c.text}`).join(`
|
|
6
|
+
|
|
7
|
+
`),he=(e,c)=>{if(typeof e=="object")return e;if(c===void 0)throw new w("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 c.embeddingModel(e)},P=e=>{const c=e.modelId;return typeof c=="string"&&c.length>0?c:void 0},nt=e=>{if(!(typeof e!="object"||e===null)){for(const c of Object.values(e))if(typeof c=="object"&&c!==null){const{cost:m}=c;if(typeof m=="number"&&Number.isFinite(m))return m}}},rt=e=>{if(e===void 0)throw new w("INTERNAL","@lunora/ai/rag: the bound context has no `vectors` (env.VECTORIZE) and no `store` is configured — bind a context whose `ctx.vectors` is wired, or configure `store` (e.g. `sqliteVectorStore`) to back this index without Vectorize.");return e},ht=e=>{if(typeof e.index!="string"||e.index.length===0)throw new w("BAD_REQUEST","@lunora/ai/rag: `index` must be a non-empty Vectorize index name");const c=e.chunkSize??Ve,m=e.chunkOverlap??je;if(!Number.isInteger(c)||c<1)throw new w("BAD_REQUEST","@lunora/ai/rag: `chunkSize` must be a positive integer");if(!Number.isInteger(m)||m<0||m>=c)throw new w("BAD_REQUEST","@lunora/ai/rag: `chunkOverlap` must be a non-negative integer smaller than `chunkSize`");const p=ue-Pe;if(!e.chunk&&!e.textStore&&!e.store&&c>p)throw new w("BAD_REQUEST",`@lunora/ai/rag: \`chunkSize\` of ${String(c)} leaves no room under Vectorize's ${String(ue)}-byte metadata limit, which also carries this chunk's bookkeeping and any \`metadata\` you attach — keep it under ${String(p)}, or supply \`textStore\` to move chunk text out of metadata entirely`);const f=e.topK??Le;if(!Number.isInteger(f)||f<1)throw new w("BAD_REQUEST","@lunora/ai/rag: `topK` must be a positive integer");if(e.maxEmbeddingDimensions!==void 0&&e.maxEmbeddingDimensions!==!1&&(!Number.isInteger(e.maxEmbeddingDimensions)||e.maxEmbeddingDimensions<1))throw new w("BAD_REQUEST","@lunora/ai/rag: `maxEmbeddingDimensions` must be a positive integer, or `false` to disable the check");if(e.embeddingModelVersion!==void 0&&!We.test(e.embeddingModelVersion))throw new w("BAD_REQUEST",'@lunora/ai/rag: `embeddingModelVersion` must match /^[A-Za-z0-9._-]{1,40}$/ (a short, stable tag like "bge-v1.5")');if(e.candidates!==void 0&&(!Number.isInteger(e.candidates)||e.candidates<1))throw new w("BAD_REQUEST","@lunora/ai/rag: `candidates` must be a positive integer");if(e.cacheEmbeddings!==void 0&&(!Number.isInteger(e.cacheEmbeddings)||e.cacheEmbeddings<0))throw new w("BAD_REQUEST","@lunora/ai/rag: `cacheEmbeddings` must be a non-negative integer");const k=e.cacheEmbeddings??0,v=e.rerank,T=e.chunk??(g=>Ce(g,c,m)),{textStore:N}=e,B=e.embeddingModelVersion,Q=g=>B===void 0?g:g===void 0?B:`${B}::${g}`;return g=>{const x=e.store?e.store(g):Fe(rt(g.vectors),e.index),W=N?x.capabilities.maxTopK:x.capabilities.maxTopKWithMetadata,F=e.maxEmbeddingDimensions??x.capabilities.maxDimensions;let K;const J=typeof g.trace=="function"?g.trace:void 0;let Z=F===!1;const X=(t,r)=>{if(Z||(Z=!0,F===!1||t<=F))return;const s=P(r);throw new w("BAD_REQUEST",`@lunora/ai/rag: embedding model${s===void 0?"":` "${s}"`} produces ${String(t)}-dimension vectors, over the ${String(F)}-dimension ceiling of index "${e.index}" — either truncate them with the provider's \`dimensions\` option (Matryoshka models such as text-embedding-3-large support this), or set \`maxEmbeddingDimensions: false\` if this index is not Vectorize-backed`)},D=new Map,ye=(t,r)=>{if(k!==0)for(D.set(t,r);D.size>k;){const s=D.keys().next();if(s.done===!0)break;D.delete(s.value)}},G=async t=>{const r=D.get(t);if(r!==void 0)return r;K??=he(e.embeddingModel,g.ai);const s=K,a=async n=>{const{embedding:h,providerMetadata:E,usage:l}=await De({model:s,value:t});if(X(h.length,s),n!==void 0){const d=l.tokens;typeof d=="number"&&Number.isFinite(d)&&n.setAttribute("gen_ai.usage.input_tokens",d);const u=nt(E),b=u??Re(P(s),{inputTokens:typeof d=="number"?d:void 0});b!==void 0&&(n.setAttribute("gen_ai.usage.cost",b),n.setAttribute("lunora.usage.cost.source",u===void 0?"estimated":"provider"))}return ye(t,h),h};if(J===void 0)return a();const o=P(s),i=typeof g.conversationId=="string"&&g.conversationId.length>0?g.conversationId:void 0;return J("ai.embed",(n,h)=>a(h),{"gen_ai.operation.name":"embeddings",...o===void 0?{}:{"gen_ai.request.model":o},...i===void 0?{}:{"gen_ai.conversation.id":i}})},we=async(t,r,s)=>{if(!e.transformQuery||r?.transformQuery===!1)return[t];const a=typeof g.conversationId=="string"&&g.conversationId.length>0?g.conversationId:void 0,o=await e.transformQuery(t,{conversationId:a,namespace:s}),i=(typeof o=="string"?[o]:[...o]).map(n=>n.trim()).filter(n=>n.length>0);return i.length>0?i:[t]},Ee=async t=>{const r=new Map,s=[...new Set(t.filter(a=>!D.has(a)))];if(s.length<2)return r;K??=he(e.embeddingModel,g.ai);try{const{embeddings:a}=await Oe({model:K,values:s});if(a.length!==s.length)return r;const[o]=a;o!==void 0&&X(o.length,K);for(const[i,n]of s.entries())r.set(n,a[i])}catch(a){if(Ne(a))throw a}return r},V=t=>{if(t===void 0){if(e.requireNamespace)throw new w("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||et(e.index)}},ee=async(t,r)=>{const[s]=await x.getByIds([C(r,t,0)],r),a=s?.metadata?.[q],o=s?.metadata?.[H];return{chunks:typeof o=="number"&&Number.isInteger(o)&&o>0?o:void 0,hash:typeof a=="string"?a:void 0}},te=async(t,r,s,a)=>{const o=Array.from({length:s-r},(i,n)=>C(a,t,r+n));o.length!==0&&(await x.deleteByIds(o,a),await N?.remove?.(o,{namespace:a}),await e.lexicalStore?.remove?.(o,{namespace:a}))},ve=async t=>{if(V(t.namespace),t.importance!==void 0&&(typeof t.importance!="number"||t.importance<0||t.importance>1))throw new w("BAD_REQUEST","@lunora/ai/rag: `importance` must be a number in [0, 1]");const r=Q(t.namespace),s=Ze(t),a=await Je(s??t.text),o=await ee(t.id,r);if(t.reindex!==!0&&s!==void 0&&o.hash===a&&o.chunks!==void 0)return{chunks:o.chunks,ids:Array.from({length:o.chunks},(d,u)=>C(r,t.id,u)),unchanged:!0};const i=T(t.text),n=i.map((d,u)=>C(r,t.id,u)),h=n.at(-1);if(h!==void 0&&He(h,t.id,x.capabilities.maxIdBytes),i.length===0&&t.allowEmptySources===!1)throw new w("BAD_REQUEST",`@lunora/ai/rag: source "${t.id}" produced zero chunks — set allowEmptySources: true to allow this`);if(i.length>0){const d=i.map((u,b)=>({chunkIndex:b,id:n[b],sourceId:t.id,text:u,...t.metadata===void 0?{}:{metadata:t.metadata}}));N&&await N.put(d,{namespace:r}),e.lexicalStore&&await e.lexicalStore.index(d,{namespace:r})}const E=await Ee(i),l=async d=>E.get(d)??await G(d);return await $e(i,Be,async(d,u)=>{const b=n[u],S={...t.metadata,[fe]:u,[pe]:t.id};N||(S[$]=d),t.importance!==void 0&&(S[U]=t.importance),u===0&&(S[q]=a,S[H]=i.length,B!==void 0&&(S[ge]=B)),qe(S,u,t.id,x.capabilities.maxMetadataBytes),await x.upsert({embed:l,id:b,input:d,metadata:S,namespace:r}),t.onChunk?.({chunkIndex:u,id:b,text:d,total:i.length})}),o.chunks!==void 0&&o.chunks>i.length&&await te(t.id,i.length,o.chunks,r),{chunks:i.length,ids:n,unchanged:!1}},xe=async t=>{V(t.namespace);const r=Q(t.namespace),a=(await ee(t.id,r)).chunks??1;await te(t.id,0,a,r)},ne=async(t,r)=>{const s=new Map;if(t.length===0)return s;if(N){const o=await N.getMany(t,{namespace:r});for(const[i,n]of t.entries()){const h=o[i];typeof h=="string"&&s.set(n,h)}return s}const a=await x.getByIds(t,r);for(const o of a){const i=o.metadata?.[$];typeof i=="string"&&s.set(o.id,i)}return s},Se=async(t,r,s)=>{const a=r?.chunkContext?.before??0,o=r?.chunkContext?.after??0;if(a===0&&o===0)return t;if(!Number.isInteger(a)||a<0||!Number.isInteger(o)||o<0)throw new w("BAD_REQUEST","@lunora/ai/rag: `chunkContext.before`/`chunkContext.after` must be non-negative integers");const i=new Map(t.map(l=>[l.id,l.text])),n=new Set;for(const l of t)for(let d=-a;d<=o;d+=1){const u=l.chunkIndex+d,b=C(s,l.sourceId,u);d!==0&&u>=0&&!i.has(b)&&n.add(b)}const h=await ne([...n],s),E=(l,d)=>{const u=C(s,l,d);return i.get(u)??h.get(u)};return t.map(l=>{const d=[];for(let u=-a;u<=o;u+=1){const b=u===0?l.text:E(l.sourceId,l.chunkIndex+u);b!==void 0&&d.push(b)}return{...l,text:d.join(`
|
|
8
|
+
`)}})},Ie=t=>{if(typeof t=="string"){const r=e.filters!==void 0&&Object.hasOwn(e.filters,t)?e.filters[t]:void 0;if(!r)throw new w("NOT_FOUND",`@lunora/ai/rag: unknown named filter "${t}" — must be one of the keys declared in RagConfig.filters`);return r.filter}return t},ke=(t,r)=>t.matches.map(s=>{const a=s.metadata??{},o=le(s.id,r),i=a[$],n=a[U],h=typeof n=="number"&&n>=0&&n<=1?n:1;return{chunkIndex:o.chunkIndex,id:s.id,importance:h,metadata:z(a),score:s.score*h,sourceId:o.sourceId,text:typeof i=="string"?i:""}}),_e=async(t,r)=>{if(!N)return t;const s=t.map(n=>n.id),[a,o]=await Promise.all([ne(s,r),x.getByIds(s,r)]),i=new Map(o.map(n=>[n.id,n.metadata]));return t.flatMap(n=>{const h=a.get(n.id);if(h===void 0)return[];const E=i.get(n.id),l=E?.[U],d=typeof l=="number"&&l>=0&&l<=1?l:n.importance,b=(n.importance===0?0:n.score/n.importance)*d;return[{...n,importance:d,metadata:z(E)??n.metadata,score:b,text:h}]})},re=async(t,r,s)=>{const a=t.map(n=>n.id).filter(n=>!r.has(n)),o=a.length===0?[]:await x.getByIds(a,s),i=new Map(o.map(n=>[n.id,n.metadata]));return t.map(n=>{const h=le(n.id,s),E=i.get(n.id),l=E?.[U];return{chunkIndex:h.chunkIndex,id:n.id,importance:typeof l=="number"&&l>=0&&l<=1?l:1,metadata:z(E),score:n.score,sourceId:h.sourceId,text:n.text}})},se=async(t,r)=>{V(r?.namespace);const s=Q(r?.namespace),a=Ie(r?.filter),o=e.rlsFilter?await e.rlsFilter(g.auth):void 0,i=o?{...a,...o}:a,n=Math.min(r?.topK??f,W),h=await we(t,r,s),E=h[0],l=v!==void 0&&r?.rerank!==!1,u=l||e.lexicalStore!==void 0||e.graphStore!==void 0||h.length>1?Math.min(e.candidates??n*ze,W):n,b=r?.minScore,S=new Set,oe=async y=>{const A=await x.query({embed:G,filter:i,input:y,namespace:s,returnMetadata:N?"indexed":"all",topK:u}),O=await _e(ke(A,s),s);if(b===void 0)return O;const{kept:_,rejectedIds:M}=Xe(O,b);for(const Te of M)S.add(Te);return _},R=[{chunks:await oe(E)}];for(const y of h.slice(1))R.push({chunks:await oe(y)});if(e.lexicalStore){const y=await e.lexicalStore.search(E,{filter:i,namespace:s,topK:e.lexicalTopK??u}),A=new Set(R.flatMap(_=>_.chunks).map(_=>_.id)),O=y.filter(_=>!S.has(_.id)||A.has(_.id));R.push({chunks:await re(O,A,s)})}const j=R.flatMap(y=>[...y.chunks]);if(e.graphStore&&j.length>0&&(e.graphStore.enforcesFilter||!Ge(i))){const y=[...new Set(j.map(M=>M.sourceId))],A=await e.graphStore.related(y,{filter:i,namespace:s,topK:e.graphTopK??u}),O=new Set(j.map(M=>M.id)),_=A.filter(M=>!S.has(M.id)||O.has(M.id));R.push({chunks:await re(_,O,s),weight:"proximity"})}const L=R.filter(y=>y.chunks.length>0);let I=L.length>1?[...Ue(L)]:[...L[0]?.chunks??[]];I.sort((y,A)=>A.score-y.score),l&&(I=[...await v(E,I)]),I=I.slice(0,n),I=[...await Se(I,r,s)];const ae=[],ie=new Set;for(const y of I)ie.has(y.sourceId)||(ie.add(y.sourceId),ae.push({id:y.sourceId,metadata:y.metadata,weight:y.importance}));return r?.onRetrieve?.({matches:I.length,query:t}),{chunks:I,context:tt(I),sources:ae}};return{asTool:t=>Ae({description:t?.description??`Search the "${e.index}" knowledge base for passages relevant to a natural-language query.`,execute:async({query:r})=>se(r,{namespace:t?.namespace,topK:t?.topK}),inputSchema:Me({properties:{query:{description:"The natural-language search query.",type:"string"}},required:["query"],type:"object"})}),index:ve,remove:xe,retrieve:se}}};export{ht as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const d=(a,k={})=>{const m=k.k??60,t=new Map;for(const[r,e]of a.entries()){const o=e.weight==="proximity";for(const[s,n]of e.chunks.entries()){const c=(o?Math.min(Math.max(n.score,0),1):1)/(m+s),i=t.get(n.id);i?i.score+=c:t.set(n.id,{chunk:n,primaryRank:r===0?s:Number.POSITIVE_INFINITY,score:c})}}return[...t.values()].map(r=>({...r,scored:{...r.chunk,score:r.score*r.chunk.importance}})).toSorted((r,e)=>{const o=e.scored.score-r.scored.score;return o!==0?o:r.primaryRank===e.primaryRank?0:r.primaryRank<e.primaryRank?-1:1}).map(r=>r.scored)};export{d as hybridRank};
|
package/dist/rag/index.d.mts
CHANGED
|
@@ -357,6 +357,82 @@ interface RagLexicalStore {
|
|
|
357
357
|
topK: number;
|
|
358
358
|
}) => Promise<ReadonlyArray<LexicalMatch>>;
|
|
359
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* One graph hit returned by {@link RagGraphStore.related}.
|
|
362
|
+
* @experimental
|
|
363
|
+
*/
|
|
364
|
+
interface GraphMatch {
|
|
365
|
+
/** The chunk vector id — the same id scheme the other legs use, so RRF can fuse the three. */
|
|
366
|
+
id: string;
|
|
367
|
+
/**
|
|
368
|
+
* Depth-decaying PROXIMITY, not relevance: `1` for a direct neighbour,
|
|
369
|
+
* halving per hop — exactly what `ctx.db.related` puts on each node. `hybridRank`
|
|
370
|
+
* scales this leg's RRF term by it, so a leg of distant hits weighs less than
|
|
371
|
+
* a leg of direct ones even when the two rank identically among themselves.
|
|
372
|
+
* Values outside `[0, 1]` are clamped.
|
|
373
|
+
*/
|
|
374
|
+
score: number;
|
|
375
|
+
/** The chunk text, returned so a graph-only hit needs no extra hydration round-trip. */
|
|
376
|
+
text: string;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Pluggable relation-graph store — the third retrieval signal, alongside the
|
|
380
|
+
* vector (semantic) and lexical (keyword) legs.
|
|
381
|
+
*
|
|
382
|
+
* Keyword and embedding search both answer "which passages look like this
|
|
383
|
+
* question". Neither can answer "what is this connected to" when the connecting
|
|
384
|
+
* fact lives in a foreign key rather than in the text — a ticket's customer, a
|
|
385
|
+
* customer's other tickets, those tickets' messages. `ctx.db.related` walks
|
|
386
|
+
* exactly that graph, and this is the seam that feeds its result into retrieval.
|
|
387
|
+
*
|
|
388
|
+
* Implement it over a Lunora app whose RAG source ids ARE document ids: expand
|
|
389
|
+
* from the seed ids with `ctx.db.related`, then return the indexed chunks of the
|
|
390
|
+
* documents it reached, each carrying that node's `score`. Mirrors the
|
|
391
|
+
* {@link RagLexicalStore} shape (namespace-partitioned, `topK`-bounded).
|
|
392
|
+
* @experimental
|
|
393
|
+
*/
|
|
394
|
+
interface RagGraphStore {
|
|
395
|
+
/**
|
|
396
|
+
* Whether {@link RagGraphStore.related} applies the `filter` it is handed to
|
|
397
|
+
* every document it returns.
|
|
398
|
+
*
|
|
399
|
+
* Required, and asked rather than assumed, because the honest answer is not
|
|
400
|
+
* derivable: `related` is somebody else's traversal and this package cannot
|
|
401
|
+
* see whether it narrows. `retrieve()` passes the SAME effective filter to
|
|
402
|
+
* all three legs — the caller's filter with `RagConfig.rlsFilter` merged
|
|
403
|
+
* over it — and the vector and lexical stores enforce it. A graph store that
|
|
404
|
+
* ignored it would return the neighbours of a document the caller may see
|
|
405
|
+
* even when those neighbours belong to another tenant, which is a leak the
|
|
406
|
+
* other two legs are specifically built to prevent. So a store that answers
|
|
407
|
+
* `false` is SKIPPED whenever a filter is in play, rather than trusted:
|
|
408
|
+
* retrieval loses its third signal and keeps its isolation. With no filter
|
|
409
|
+
* (no `rlsFilter`, no `RetrieveOptions.filter`) there is nothing to enforce
|
|
410
|
+
* and `false` costs nothing.
|
|
411
|
+
*
|
|
412
|
+
* Answer `true` only if every returned chunk's document really is matched
|
|
413
|
+
* against the filter — `ctx.db.related` under a schema with RLS policies
|
|
414
|
+
* does not count on its own, because the filter here is RAG metadata, not a
|
|
415
|
+
* row policy. The stricter reading is the safe one: if in doubt, `false`.
|
|
416
|
+
*/
|
|
417
|
+
enforcesFilter: boolean;
|
|
418
|
+
/**
|
|
419
|
+
* Expand from the SOURCE document ids the search legs found and return
|
|
420
|
+
* chunks of the documents they connect to, best (nearest) first.
|
|
421
|
+
*
|
|
422
|
+
* Seeded rather than queried: the graph has no notion of a query string, so
|
|
423
|
+
* it widens a ranking the other legs produced instead of ranking on its own.
|
|
424
|
+
* A seed id that is not a graph node simply contributes nothing.
|
|
425
|
+
*
|
|
426
|
+
* `options.filter` is the effective metadata filter for this retrieval, and
|
|
427
|
+
* is present only for stores that declared {@link RagGraphStore.enforcesFilter}
|
|
428
|
+
* — see there for what declaring it commits you to.
|
|
429
|
+
*/
|
|
430
|
+
related: (sourceIds: ReadonlyArray<string>, options: {
|
|
431
|
+
filter?: Record<string, unknown>;
|
|
432
|
+
namespace?: string;
|
|
433
|
+
topK: number;
|
|
434
|
+
}) => Promise<ReadonlyArray<GraphMatch>>;
|
|
435
|
+
}
|
|
360
436
|
/**
|
|
361
437
|
* A pre-defined, reusable filter expression. Declared on `RagConfig.filters`
|
|
362
438
|
* (keyed by name) and referenced by name from `RetrieveOptions.filter` — avoids
|
|
@@ -477,6 +553,15 @@ interface RagConfig {
|
|
|
477
553
|
* name is not found here — catches spelling mistakes early.
|
|
478
554
|
*/
|
|
479
555
|
filters?: Record<string, RagNamedFilter>;
|
|
556
|
+
/**
|
|
557
|
+
* Pluggable relation-graph store — the third retrieval signal. When set,
|
|
558
|
+
* `retrieve()` seeds a traversal from the source documents the vector and
|
|
559
|
+
* lexical legs found and fuses the connected documents' chunks into the same
|
|
560
|
+
* RRF ranking, weighted by how far away they are. See {@link RagGraphStore}.
|
|
561
|
+
*/
|
|
562
|
+
graphStore?: RagGraphStore;
|
|
563
|
+
/** Retrieval depth for the graph leg. Defaults to the effective candidate pool. */
|
|
564
|
+
graphTopK?: number;
|
|
480
565
|
/** The Vectorize index name (a `ctx.vectors` index binding key). */
|
|
481
566
|
index: string;
|
|
482
567
|
/**
|
|
@@ -814,13 +899,45 @@ declare const guessMimeTypeFromExtension: (extension: string) => string;
|
|
|
814
899
|
*/
|
|
815
900
|
declare const contentHash: (data: BufferSource) => Promise<string>;
|
|
816
901
|
/**
|
|
817
|
-
*
|
|
818
|
-
* _rank position_ rather than their absolute scores, which are not comparable
|
|
819
|
-
* across different search methods (cosine vs BM25).
|
|
902
|
+
* How a leg's reciprocal-rank term is weighted before it is summed.
|
|
820
903
|
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
904
|
+
* `"rank"` is plain RRF: position in the leg is the only signal, which is the
|
|
905
|
+
* point — cosine, BM25 and graph-proximity scores are not comparable.
|
|
906
|
+
*
|
|
907
|
+
* `"proximity"` additionally scales the term by the chunk's own `score`, clamped
|
|
908
|
+
* into `[0, 1]`. It exists for the graph leg, where `score` is the depth decay
|
|
909
|
+
* `ctx.db.related` assigned (1, 0.5, 0.25 …) rather than a relevance score: a
|
|
910
|
+
* depth-2 neighbour should add half of what a depth-1 one does even where the
|
|
911
|
+
* two are adjacent in the leg's own ranking. Rank alone cannot express that — a
|
|
912
|
+
* leg of nothing but depth-3 hits would otherwise contribute exactly as much as
|
|
913
|
+
* a leg of direct neighbours, which is the whole distinction a proximity signal
|
|
914
|
+
* exists to carry.
|
|
915
|
+
*/
|
|
916
|
+
type FusionWeight = "proximity" | "rank";
|
|
917
|
+
/** One ranked list feeding the fusion, and the rule its contribution follows. */
|
|
918
|
+
interface FusionLeg {
|
|
919
|
+
/** The leg's chunks, best first. */
|
|
920
|
+
chunks: ReadonlyArray<RetrievedChunk>;
|
|
921
|
+
/** Defaults to `"rank"`. See {@link FusionWeight}. */
|
|
922
|
+
weight?: FusionWeight;
|
|
923
|
+
}
|
|
924
|
+
interface HybridRankOptions {
|
|
925
|
+
/** RRF damping constant; higher flattens rank differences. Defaults to 60. */
|
|
926
|
+
k?: number;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Reciprocal Rank Fusion (RRF): merge ranked lists of chunks by their _rank
|
|
930
|
+
* position_ rather than their absolute scores, which are not comparable across
|
|
931
|
+
* different search methods (cosine vs BM25 vs graph proximity).
|
|
932
|
+
*
|
|
933
|
+
* Each leg contributes `1 / (k + rank)` to each chunk's fused score, where
|
|
934
|
+
* `rank` is 0-based position within that leg. The constant `k` (default 60)
|
|
935
|
+
* dampens the influence of high ranks. A leg declared `weight: "proximity"`
|
|
936
|
+
* scales its term by the chunk's own score — see {@link FusionWeight}.
|
|
937
|
+
*
|
|
938
|
+
* Legs are a LIST rather than one parameter per search method: what a leg needs
|
|
939
|
+
* to declare is its scoring rule, not its position, and a positional graph slot
|
|
940
|
+
* forced callers with no lexical leg to write `hybridRank(vector, [], graph)`.
|
|
824
941
|
*
|
|
825
942
|
* **The returned chunks carry the fused score in `score`**, multiplied by the
|
|
826
943
|
* chunk's `importance` so source weighting still applies. Writing it back is
|
|
@@ -830,19 +947,25 @@ declare const contentHash: (data: BufferSource) => Promise<string>;
|
|
|
830
947
|
* Since BM25 is unbounded while cosine is `[0, 1]`, that silently promoted
|
|
831
948
|
* every lexical-only hit above every vector hit.
|
|
832
949
|
*
|
|
950
|
+
* Importance is applied ONCE, by this call. A caller with several signals passes
|
|
951
|
+
* them as several legs to ONE call, never one call per leg: the returned list is
|
|
952
|
+
* both weighted and sorted by that weighting, so handing it back in as a leg
|
|
953
|
+
* makes the next pass derive its ranks from an already-weighted ordering and
|
|
954
|
+
* weight it a second time.
|
|
955
|
+
*
|
|
833
956
|
* So in hybrid mode `RetrievedChunk.score` is an RRF score (small, ~`1/60`
|
|
834
957
|
* scale), not a cosine similarity. `retrieve()` applies `minScore` to the
|
|
835
958
|
* vector leg *before* fusion for exactly this reason — the option is documented
|
|
836
959
|
* against the cosine scale.
|
|
837
960
|
*
|
|
838
|
-
* Ties are broken by preferring the chunk ranked higher in the
|
|
839
|
-
*
|
|
961
|
+
* Ties are broken by preferring the chunk ranked higher in the FIRST leg,
|
|
962
|
+
* conventionally the vector one and typically the more semantically accurate.
|
|
840
963
|
*
|
|
841
|
-
* Callers MUST ensure every chunk
|
|
964
|
+
* Callers MUST ensure every chunk across every leg carries a unique, comparable
|
|
842
965
|
* `id` — guaranteed by the chunk-id scheme `${sourceId}#${chunkIndex}`.
|
|
843
966
|
* @experimental
|
|
844
967
|
*/
|
|
845
|
-
declare const hybridRank: (
|
|
968
|
+
declare const hybridRank: (legs: ReadonlyArray<FusionLeg>, options?: HybridRankOptions) => ReadonlyArray<RetrievedChunk>;
|
|
846
969
|
/**
|
|
847
970
|
* An **in-memory** Okapi BM25 lexical store — the reference adapter behind
|
|
848
971
|
* `RagConfig.lexicalStore`, giving hybrid retrieval its keyword leg with zero
|
|
@@ -1197,4 +1320,4 @@ declare const ragSyncTriggers: <Document extends Record<string, unknown> = Recor
|
|
|
1197
1320
|
afterInsert: RagSyncHandler;
|
|
1198
1321
|
afterUpdate: RagSyncHandler;
|
|
1199
1322
|
};
|
|
1200
|
-
export { type BatchRerankerOptions, type ChunkerOptions, type IndexInput, type IndexResult, type LexicalMatch, type Rag, type RagConfig, type RagContext, type RagEmbedder, type RagExtractor, type RagLexicalStore, type RagNamedFilter, type RagObjectSource, type RagQueryTransform, type RagReranker, type RagSource, type RagSourceObject, type RagSourceOptions, type RagSourceSync, type RagSqlExec, type RagSyncActionReference, type RagSyncArgs, type RagSyncOptions, type RagSyncPassOptions, type RagSyncReport, type RagTextStore, type RagToolOptions, type RagVectorMatch, type RagVectorMatches, type RagVectorQueryInput, type RagVectorRecord, type RagVectorStore, type RagVectorStoreCapabilities, type RagVectorUpsertInput, type RagVectors, type RemoveInput, type RetrieveOptions, type RetrieveResult, type RetrievedChunk, type ScoreRerankerOptions, type SqlLexicalStoreOptions, type SqliteVectorStoreOptions, type StoredRagChunk, type TokenChunkerOptions, VECTORIZE_CAPABILITIES, batchReranker, bm25LexicalStore, contentHash, defineRag, defineRagSource, fixedWindowChunks, guessMimeTypeFromExtension, hybridRank, markdownChunker, matchesMetadataFilter, ragSyncTriggers, scoreReranker, sentenceChunker, sqlLexicalStore, sqliteVectorStore, tokenChunker, vectorizeStore };
|
|
1323
|
+
export { type BatchRerankerOptions, type ChunkerOptions, type FusionLeg, type FusionWeight, type GraphMatch, type HybridRankOptions, type IndexInput, type IndexResult, type LexicalMatch, type Rag, type RagConfig, type RagContext, type RagEmbedder, type RagExtractor, type RagGraphStore, type RagLexicalStore, type RagNamedFilter, type RagObjectSource, type RagQueryTransform, type RagReranker, type RagSource, type RagSourceObject, type RagSourceOptions, type RagSourceSync, type RagSqlExec, type RagSyncActionReference, type RagSyncArgs, type RagSyncOptions, type RagSyncPassOptions, type RagSyncReport, type RagTextStore, type RagToolOptions, type RagVectorMatch, type RagVectorMatches, type RagVectorQueryInput, type RagVectorRecord, type RagVectorStore, type RagVectorStoreCapabilities, type RagVectorUpsertInput, type RagVectors, type RemoveInput, type RetrieveOptions, type RetrieveResult, type RetrievedChunk, type ScoreRerankerOptions, type SqlLexicalStoreOptions, type SqliteVectorStoreOptions, type StoredRagChunk, type TokenChunkerOptions, VECTORIZE_CAPABILITIES, batchReranker, bm25LexicalStore, contentHash, defineRag, defineRagSource, fixedWindowChunks, guessMimeTypeFromExtension, hybridRank, markdownChunker, matchesMetadataFilter, ragSyncTriggers, scoreReranker, sentenceChunker, sqlLexicalStore, sqliteVectorStore, tokenChunker, vectorizeStore };
|
package/dist/rag/index.d.ts
CHANGED
|
@@ -357,6 +357,82 @@ interface RagLexicalStore {
|
|
|
357
357
|
topK: number;
|
|
358
358
|
}) => Promise<ReadonlyArray<LexicalMatch>>;
|
|
359
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* One graph hit returned by {@link RagGraphStore.related}.
|
|
362
|
+
* @experimental
|
|
363
|
+
*/
|
|
364
|
+
interface GraphMatch {
|
|
365
|
+
/** The chunk vector id — the same id scheme the other legs use, so RRF can fuse the three. */
|
|
366
|
+
id: string;
|
|
367
|
+
/**
|
|
368
|
+
* Depth-decaying PROXIMITY, not relevance: `1` for a direct neighbour,
|
|
369
|
+
* halving per hop — exactly what `ctx.db.related` puts on each node. `hybridRank`
|
|
370
|
+
* scales this leg's RRF term by it, so a leg of distant hits weighs less than
|
|
371
|
+
* a leg of direct ones even when the two rank identically among themselves.
|
|
372
|
+
* Values outside `[0, 1]` are clamped.
|
|
373
|
+
*/
|
|
374
|
+
score: number;
|
|
375
|
+
/** The chunk text, returned so a graph-only hit needs no extra hydration round-trip. */
|
|
376
|
+
text: string;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Pluggable relation-graph store — the third retrieval signal, alongside the
|
|
380
|
+
* vector (semantic) and lexical (keyword) legs.
|
|
381
|
+
*
|
|
382
|
+
* Keyword and embedding search both answer "which passages look like this
|
|
383
|
+
* question". Neither can answer "what is this connected to" when the connecting
|
|
384
|
+
* fact lives in a foreign key rather than in the text — a ticket's customer, a
|
|
385
|
+
* customer's other tickets, those tickets' messages. `ctx.db.related` walks
|
|
386
|
+
* exactly that graph, and this is the seam that feeds its result into retrieval.
|
|
387
|
+
*
|
|
388
|
+
* Implement it over a Lunora app whose RAG source ids ARE document ids: expand
|
|
389
|
+
* from the seed ids with `ctx.db.related`, then return the indexed chunks of the
|
|
390
|
+
* documents it reached, each carrying that node's `score`. Mirrors the
|
|
391
|
+
* {@link RagLexicalStore} shape (namespace-partitioned, `topK`-bounded).
|
|
392
|
+
* @experimental
|
|
393
|
+
*/
|
|
394
|
+
interface RagGraphStore {
|
|
395
|
+
/**
|
|
396
|
+
* Whether {@link RagGraphStore.related} applies the `filter` it is handed to
|
|
397
|
+
* every document it returns.
|
|
398
|
+
*
|
|
399
|
+
* Required, and asked rather than assumed, because the honest answer is not
|
|
400
|
+
* derivable: `related` is somebody else's traversal and this package cannot
|
|
401
|
+
* see whether it narrows. `retrieve()` passes the SAME effective filter to
|
|
402
|
+
* all three legs — the caller's filter with `RagConfig.rlsFilter` merged
|
|
403
|
+
* over it — and the vector and lexical stores enforce it. A graph store that
|
|
404
|
+
* ignored it would return the neighbours of a document the caller may see
|
|
405
|
+
* even when those neighbours belong to another tenant, which is a leak the
|
|
406
|
+
* other two legs are specifically built to prevent. So a store that answers
|
|
407
|
+
* `false` is SKIPPED whenever a filter is in play, rather than trusted:
|
|
408
|
+
* retrieval loses its third signal and keeps its isolation. With no filter
|
|
409
|
+
* (no `rlsFilter`, no `RetrieveOptions.filter`) there is nothing to enforce
|
|
410
|
+
* and `false` costs nothing.
|
|
411
|
+
*
|
|
412
|
+
* Answer `true` only if every returned chunk's document really is matched
|
|
413
|
+
* against the filter — `ctx.db.related` under a schema with RLS policies
|
|
414
|
+
* does not count on its own, because the filter here is RAG metadata, not a
|
|
415
|
+
* row policy. The stricter reading is the safe one: if in doubt, `false`.
|
|
416
|
+
*/
|
|
417
|
+
enforcesFilter: boolean;
|
|
418
|
+
/**
|
|
419
|
+
* Expand from the SOURCE document ids the search legs found and return
|
|
420
|
+
* chunks of the documents they connect to, best (nearest) first.
|
|
421
|
+
*
|
|
422
|
+
* Seeded rather than queried: the graph has no notion of a query string, so
|
|
423
|
+
* it widens a ranking the other legs produced instead of ranking on its own.
|
|
424
|
+
* A seed id that is not a graph node simply contributes nothing.
|
|
425
|
+
*
|
|
426
|
+
* `options.filter` is the effective metadata filter for this retrieval, and
|
|
427
|
+
* is present only for stores that declared {@link RagGraphStore.enforcesFilter}
|
|
428
|
+
* — see there for what declaring it commits you to.
|
|
429
|
+
*/
|
|
430
|
+
related: (sourceIds: ReadonlyArray<string>, options: {
|
|
431
|
+
filter?: Record<string, unknown>;
|
|
432
|
+
namespace?: string;
|
|
433
|
+
topK: number;
|
|
434
|
+
}) => Promise<ReadonlyArray<GraphMatch>>;
|
|
435
|
+
}
|
|
360
436
|
/**
|
|
361
437
|
* A pre-defined, reusable filter expression. Declared on `RagConfig.filters`
|
|
362
438
|
* (keyed by name) and referenced by name from `RetrieveOptions.filter` — avoids
|
|
@@ -477,6 +553,15 @@ interface RagConfig {
|
|
|
477
553
|
* name is not found here — catches spelling mistakes early.
|
|
478
554
|
*/
|
|
479
555
|
filters?: Record<string, RagNamedFilter>;
|
|
556
|
+
/**
|
|
557
|
+
* Pluggable relation-graph store — the third retrieval signal. When set,
|
|
558
|
+
* `retrieve()` seeds a traversal from the source documents the vector and
|
|
559
|
+
* lexical legs found and fuses the connected documents' chunks into the same
|
|
560
|
+
* RRF ranking, weighted by how far away they are. See {@link RagGraphStore}.
|
|
561
|
+
*/
|
|
562
|
+
graphStore?: RagGraphStore;
|
|
563
|
+
/** Retrieval depth for the graph leg. Defaults to the effective candidate pool. */
|
|
564
|
+
graphTopK?: number;
|
|
480
565
|
/** The Vectorize index name (a `ctx.vectors` index binding key). */
|
|
481
566
|
index: string;
|
|
482
567
|
/**
|
|
@@ -814,13 +899,45 @@ declare const guessMimeTypeFromExtension: (extension: string) => string;
|
|
|
814
899
|
*/
|
|
815
900
|
declare const contentHash: (data: BufferSource) => Promise<string>;
|
|
816
901
|
/**
|
|
817
|
-
*
|
|
818
|
-
* _rank position_ rather than their absolute scores, which are not comparable
|
|
819
|
-
* across different search methods (cosine vs BM25).
|
|
902
|
+
* How a leg's reciprocal-rank term is weighted before it is summed.
|
|
820
903
|
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
904
|
+
* `"rank"` is plain RRF: position in the leg is the only signal, which is the
|
|
905
|
+
* point — cosine, BM25 and graph-proximity scores are not comparable.
|
|
906
|
+
*
|
|
907
|
+
* `"proximity"` additionally scales the term by the chunk's own `score`, clamped
|
|
908
|
+
* into `[0, 1]`. It exists for the graph leg, where `score` is the depth decay
|
|
909
|
+
* `ctx.db.related` assigned (1, 0.5, 0.25 …) rather than a relevance score: a
|
|
910
|
+
* depth-2 neighbour should add half of what a depth-1 one does even where the
|
|
911
|
+
* two are adjacent in the leg's own ranking. Rank alone cannot express that — a
|
|
912
|
+
* leg of nothing but depth-3 hits would otherwise contribute exactly as much as
|
|
913
|
+
* a leg of direct neighbours, which is the whole distinction a proximity signal
|
|
914
|
+
* exists to carry.
|
|
915
|
+
*/
|
|
916
|
+
type FusionWeight = "proximity" | "rank";
|
|
917
|
+
/** One ranked list feeding the fusion, and the rule its contribution follows. */
|
|
918
|
+
interface FusionLeg {
|
|
919
|
+
/** The leg's chunks, best first. */
|
|
920
|
+
chunks: ReadonlyArray<RetrievedChunk>;
|
|
921
|
+
/** Defaults to `"rank"`. See {@link FusionWeight}. */
|
|
922
|
+
weight?: FusionWeight;
|
|
923
|
+
}
|
|
924
|
+
interface HybridRankOptions {
|
|
925
|
+
/** RRF damping constant; higher flattens rank differences. Defaults to 60. */
|
|
926
|
+
k?: number;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Reciprocal Rank Fusion (RRF): merge ranked lists of chunks by their _rank
|
|
930
|
+
* position_ rather than their absolute scores, which are not comparable across
|
|
931
|
+
* different search methods (cosine vs BM25 vs graph proximity).
|
|
932
|
+
*
|
|
933
|
+
* Each leg contributes `1 / (k + rank)` to each chunk's fused score, where
|
|
934
|
+
* `rank` is 0-based position within that leg. The constant `k` (default 60)
|
|
935
|
+
* dampens the influence of high ranks. A leg declared `weight: "proximity"`
|
|
936
|
+
* scales its term by the chunk's own score — see {@link FusionWeight}.
|
|
937
|
+
*
|
|
938
|
+
* Legs are a LIST rather than one parameter per search method: what a leg needs
|
|
939
|
+
* to declare is its scoring rule, not its position, and a positional graph slot
|
|
940
|
+
* forced callers with no lexical leg to write `hybridRank(vector, [], graph)`.
|
|
824
941
|
*
|
|
825
942
|
* **The returned chunks carry the fused score in `score`**, multiplied by the
|
|
826
943
|
* chunk's `importance` so source weighting still applies. Writing it back is
|
|
@@ -830,19 +947,25 @@ declare const contentHash: (data: BufferSource) => Promise<string>;
|
|
|
830
947
|
* Since BM25 is unbounded while cosine is `[0, 1]`, that silently promoted
|
|
831
948
|
* every lexical-only hit above every vector hit.
|
|
832
949
|
*
|
|
950
|
+
* Importance is applied ONCE, by this call. A caller with several signals passes
|
|
951
|
+
* them as several legs to ONE call, never one call per leg: the returned list is
|
|
952
|
+
* both weighted and sorted by that weighting, so handing it back in as a leg
|
|
953
|
+
* makes the next pass derive its ranks from an already-weighted ordering and
|
|
954
|
+
* weight it a second time.
|
|
955
|
+
*
|
|
833
956
|
* So in hybrid mode `RetrievedChunk.score` is an RRF score (small, ~`1/60`
|
|
834
957
|
* scale), not a cosine similarity. `retrieve()` applies `minScore` to the
|
|
835
958
|
* vector leg *before* fusion for exactly this reason — the option is documented
|
|
836
959
|
* against the cosine scale.
|
|
837
960
|
*
|
|
838
|
-
* Ties are broken by preferring the chunk ranked higher in the
|
|
839
|
-
*
|
|
961
|
+
* Ties are broken by preferring the chunk ranked higher in the FIRST leg,
|
|
962
|
+
* conventionally the vector one and typically the more semantically accurate.
|
|
840
963
|
*
|
|
841
|
-
* Callers MUST ensure every chunk
|
|
964
|
+
* Callers MUST ensure every chunk across every leg carries a unique, comparable
|
|
842
965
|
* `id` — guaranteed by the chunk-id scheme `${sourceId}#${chunkIndex}`.
|
|
843
966
|
* @experimental
|
|
844
967
|
*/
|
|
845
|
-
declare const hybridRank: (
|
|
968
|
+
declare const hybridRank: (legs: ReadonlyArray<FusionLeg>, options?: HybridRankOptions) => ReadonlyArray<RetrievedChunk>;
|
|
846
969
|
/**
|
|
847
970
|
* An **in-memory** Okapi BM25 lexical store — the reference adapter behind
|
|
848
971
|
* `RagConfig.lexicalStore`, giving hybrid retrieval its keyword leg with zero
|
|
@@ -1197,4 +1320,4 @@ declare const ragSyncTriggers: <Document extends Record<string, unknown> = Recor
|
|
|
1197
1320
|
afterInsert: RagSyncHandler;
|
|
1198
1321
|
afterUpdate: RagSyncHandler;
|
|
1199
1322
|
};
|
|
1200
|
-
export { type BatchRerankerOptions, type ChunkerOptions, type IndexInput, type IndexResult, type LexicalMatch, type Rag, type RagConfig, type RagContext, type RagEmbedder, type RagExtractor, type RagLexicalStore, type RagNamedFilter, type RagObjectSource, type RagQueryTransform, type RagReranker, type RagSource, type RagSourceObject, type RagSourceOptions, type RagSourceSync, type RagSqlExec, type RagSyncActionReference, type RagSyncArgs, type RagSyncOptions, type RagSyncPassOptions, type RagSyncReport, type RagTextStore, type RagToolOptions, type RagVectorMatch, type RagVectorMatches, type RagVectorQueryInput, type RagVectorRecord, type RagVectorStore, type RagVectorStoreCapabilities, type RagVectorUpsertInput, type RagVectors, type RemoveInput, type RetrieveOptions, type RetrieveResult, type RetrievedChunk, type ScoreRerankerOptions, type SqlLexicalStoreOptions, type SqliteVectorStoreOptions, type StoredRagChunk, type TokenChunkerOptions, VECTORIZE_CAPABILITIES, batchReranker, bm25LexicalStore, contentHash, defineRag, defineRagSource, fixedWindowChunks, guessMimeTypeFromExtension, hybridRank, markdownChunker, matchesMetadataFilter, ragSyncTriggers, scoreReranker, sentenceChunker, sqlLexicalStore, sqliteVectorStore, tokenChunker, vectorizeStore };
|
|
1323
|
+
export { type BatchRerankerOptions, type ChunkerOptions, type FusionLeg, type FusionWeight, type GraphMatch, type HybridRankOptions, type IndexInput, type IndexResult, type LexicalMatch, type Rag, type RagConfig, type RagContext, type RagEmbedder, type RagExtractor, type RagGraphStore, type RagLexicalStore, type RagNamedFilter, type RagObjectSource, type RagQueryTransform, type RagReranker, type RagSource, type RagSourceObject, type RagSourceOptions, type RagSourceSync, type RagSqlExec, type RagSyncActionReference, type RagSyncArgs, type RagSyncOptions, type RagSyncPassOptions, type RagSyncReport, type RagTextStore, type RagToolOptions, type RagVectorMatch, type RagVectorMatches, type RagVectorQueryInput, type RagVectorRecord, type RagVectorStore, type RagVectorStoreCapabilities, type RagVectorUpsertInput, type RagVectors, type RemoveInput, type RetrieveOptions, type RetrieveResult, type RetrievedChunk, type ScoreRerankerOptions, type SqlLexicalStoreOptions, type SqliteVectorStoreOptions, type StoredRagChunk, type TokenChunkerOptions, VECTORIZE_CAPABILITIES, batchReranker, bm25LexicalStore, contentHash, defineRag, defineRagSource, fixedWindowChunks, guessMimeTypeFromExtension, hybridRank, markdownChunker, matchesMetadataFilter, ragSyncTriggers, scoreReranker, sentenceChunker, sqlLexicalStore, sqliteVectorStore, tokenChunker, vectorizeStore };
|
package/dist/rag/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{default as o}from"../packem_shared/fixedWindowChunks-C461ahRE.mjs";import{markdownChunker as a,sentenceChunker as f,tokenChunker as n}from"../packem_shared/markdownChunker-Bcv56GEz.mjs";import{default as x}from"../packem_shared/defineRag-
|
|
1
|
+
import{default as o}from"../packem_shared/fixedWindowChunks-C461ahRE.mjs";import{markdownChunker as a,sentenceChunker as f,tokenChunker as n}from"../packem_shared/markdownChunker-Bcv56GEz.mjs";import{default as x}from"../packem_shared/defineRag-DK8Ifpn_.mjs";import{contentHash as p,guessMimeTypeFromExtension as i}from"../packem_shared/contentHash-BIn6ECP8.mjs";import{hybridRank as d}from"../packem_shared/hybridRank-B4skyCLx.mjs";import{default as k}from"../packem_shared/bm25LexicalStore-DMUzAL0O.mjs";import{default as h}from"../packem_shared/matchesMetadataFilter-BbIOyA5g.mjs";import{batchReranker as g,scoreReranker as C}from"../packem_shared/batchReranker-Bc38FBLH.mjs";import{defineRagSource as E}from"../packem_shared/defineRagSource-Q3f3niU8.mjs";import{sqlLexicalStore as T}from"../packem_shared/sqlLexicalStore-4C_cIwef.mjs";import{sqliteVectorStore as y}from"../packem_shared/sqliteVectorStore-D32l9lP0.mjs";import{ragSyncTriggers as q}from"../packem_shared/ragSyncTriggers-DPqzBNFw.mjs";import{VECTORIZE_CAPABILITIES as A,vectorizeStore as F}from"../packem_shared/VECTORIZE_CAPABILITIES-CUQDoxis.mjs";export{A as VECTORIZE_CAPABILITIES,g as batchReranker,k as bm25LexicalStore,p as contentHash,x as defineRag,E as defineRagSource,o as fixedWindowChunks,i as guessMimeTypeFromExtension,d as hybridRank,a as markdownChunker,h as matchesMetadataFilter,q as ragSyncTriggers,C as scoreReranker,f as sentenceChunker,T as sqlLexicalStore,y as sqliteVectorStore,n as tokenChunker,F as vectorizeStore};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/ai",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.90",
|
|
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",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"access": "public"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
56
|
+
"@lunora/errors": "1.0.0-alpha.36",
|
|
57
57
|
"ai": "7.0.93",
|
|
58
58
|
"workers-ai-provider": "4.0.0"
|
|
59
59
|
},
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import{LunoraError as y,isLunoraError as Me}from"@lunora/errors";import{tool as De,jsonSchema as Oe,embedMany as Re,embed as Ce}from"ai";import{estimateModelCost as $e}from"./DEFAULT_MODEL_PRICES-Q8uxdiuV.mjs";import Be from"./fixedWindowChunks-C461ahRE.mjs";import{c as Ke,I as Ue}from"./concurrent-C6nqBv41.mjs";import{contentHash as Qe}from"./contentHash-BIn6ECP8.mjs";import de from"./hybridRank-DejmVw2I.mjs";import{VECTORIZE_CAPABILITIES as ue,vectorizeStore as Fe}from"./VECTORIZE_CAPABILITIES-CUQDoxis.mjs";const Ve=/["\\\u0000-\u001F\uD800-\uDFFF]/,le=e=>Ve.test(e)?JSON.stringify(e):`"${e}"`,Y=e=>{if(e===void 0)return"null";if(typeof e=="bigint")throw new TypeError("stableStringify: cannot use a bigint in a stable JSON cache key — pass it as a string, or use stableWireKey");if(typeof e=="number"){if(Number.isNaN(e))return"nan";if(e===1/0)return"inf";if(e===-1/0)return"-inf";if(Object.is(e,-0))return"-0"}if(typeof e=="string")return le(e);if(e===null||typeof e!="object")return JSON.stringify(e);if(Array.isArray(e)){let v="[";for(let T=0;T<e.length;T++)T>0&&(v+=","),v+=Y(e[T]);return v+"]"}const i=Object.getPrototypeOf(e);if(i!==null&&i!==Object.prototype){const v=e.constructor?.name??"value";throw new TypeError(`stableStringify: cannot use a ${v} in a stable JSON cache key — only plain objects, arrays, and JSON primitives are supported (wire-typed values key via stableWireKey)`)}const l=e,p=Object.keys(l).sort();let f="{",k=!0;for(const v of p){const T=l[v];T!==void 0&&(k?k=!1:f+=",",f+=le(v),f+=":",f+=Y(T))}return f+"}"},je=1e3,ze=200,Le=5,Pe=4,me=ue.maxMetadataBytes===!1?Number.POSITIVE_INFINITY:ue.maxMetadataBytes,Ye=2*1024,ge="__ragChunk",be="__ragSource",O="__ragText",q="__ragHash",H="__ragChunks",B="__ragImportance",ye="__ragModel",qe=new Set([ge,H,q,B,ye,be,O]),He=(e,i,l,p)=>{if(p===!1)return;const f=new TextEncoder().encode(JSON.stringify(e)).length;if(f<=p)return;const v=(typeof e[O]=="string"?new TextEncoder().encode(e[O]).length:0)*2>f?"lower `chunkSize` (it counts characters, not bytes — multibyte text costs up to 3 bytes each), or supply `textStore` to move chunk text out of metadata entirely":"attach less per-source `metadata`";throw new y("BAD_REQUEST",`@lunora/ai/rag: chunk ${String(i)} of "${l}" carries ${String(f)} bytes of metadata, over the store's ${String(p)}-byte per-vector ceiling — ${v}`)},We=(e,i,l)=>{if(l===!1)return;const p=new TextEncoder().encode(e).length;if(!(p<=l))throw new y("BAD_REQUEST",`@lunora/ai/rag: chunk id "${e}" for source "${i}" is ${String(p)} bytes, over the store's ${String(l)}-byte per-vector id ceiling — shorten the source id (hash long keys before indexing them) or shorten the \`namespace\`, which is prefixed onto every chunk id`)},Je=/^[\w.-]{1,40}$/,we=e=>e===void 0?"":`${encodeURIComponent(e)}#`,D=(e,i,l)=>`${we(e)}${i}#${String(l)}`,he=(e,i)=>{const l=we(i),p=l!==""&&e.startsWith(l)?e.slice(l.length):e,f=p.lastIndexOf("#"),k=f===-1?Number.NaN:Number(p.slice(f+1));return f===-1||!Number.isInteger(k)||k<0?{chunkIndex:0,sourceId:p}:{chunkIndex:k,sourceId:p.slice(0,f)}},Ze=async e=>Qe(new TextEncoder().encode(e)),Xe=e=>{try{return Y([e.text,e.metadata,e.importance])}catch{return}},Ge=(e,i)=>{const l=[],p=[];for(const f of e)f.score>=i?l.push(f):p.push(f.id);return{kept:l,rejectedIds:p}},L=e=>{if(!e)return;const i=Object.entries(e).filter(([l])=>!qe.has(l));return i.length>0?Object.fromEntries(i):void 0},fe=new Set,et=e=>{fe.has(e)||(fe.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 }.`))},tt=e=>e.map(i=>`[source:${i.sourceId}#${String(i.chunkIndex)}]
|
|
5
|
-
${i.text}`).join(`
|
|
6
|
-
|
|
7
|
-
`),pe=(e,i)=>{if(typeof e=="object")return e;if(i===void 0)throw new y("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 i.embeddingModel(e)},P=e=>{const i=e.modelId;return typeof i=="string"&&i.length>0?i:void 0},nt=e=>{if(!(typeof e!="object"||e===null)){for(const i of Object.values(e))if(typeof i=="object"&&i!==null){const{cost:l}=i;if(typeof l=="number"&&Number.isFinite(l))return l}}},rt=e=>{if(e===void 0)throw new y("INTERNAL","@lunora/ai/rag: the bound context has no `vectors` (env.VECTORIZE) and no `store` is configured — bind a context whose `ctx.vectors` is wired, or configure `store` (e.g. `sqliteVectorStore`) to back this index without Vectorize.");return e},ht=e=>{if(typeof e.index!="string"||e.index.length===0)throw new y("BAD_REQUEST","@lunora/ai/rag: `index` must be a non-empty Vectorize index name");const i=e.chunkSize??je,l=e.chunkOverlap??ze;if(!Number.isInteger(i)||i<1)throw new y("BAD_REQUEST","@lunora/ai/rag: `chunkSize` must be a positive integer");if(!Number.isInteger(l)||l<0||l>=i)throw new y("BAD_REQUEST","@lunora/ai/rag: `chunkOverlap` must be a non-negative integer smaller than `chunkSize`");const p=me-Ye;if(!e.chunk&&!e.textStore&&!e.store&&i>p)throw new y("BAD_REQUEST",`@lunora/ai/rag: \`chunkSize\` of ${String(i)} leaves no room under Vectorize's ${String(me)}-byte metadata limit, which also carries this chunk's bookkeeping and any \`metadata\` you attach — keep it under ${String(p)}, or supply \`textStore\` to move chunk text out of metadata entirely`);const f=e.topK??Le;if(!Number.isInteger(f)||f<1)throw new y("BAD_REQUEST","@lunora/ai/rag: `topK` must be a positive integer");if(e.maxEmbeddingDimensions!==void 0&&e.maxEmbeddingDimensions!==!1&&(!Number.isInteger(e.maxEmbeddingDimensions)||e.maxEmbeddingDimensions<1))throw new y("BAD_REQUEST","@lunora/ai/rag: `maxEmbeddingDimensions` must be a positive integer, or `false` to disable the check");if(e.embeddingModelVersion!==void 0&&!Je.test(e.embeddingModelVersion))throw new y("BAD_REQUEST",'@lunora/ai/rag: `embeddingModelVersion` must match /^[A-Za-z0-9._-]{1,40}$/ (a short, stable tag like "bge-v1.5")');if(e.candidates!==void 0&&(!Number.isInteger(e.candidates)||e.candidates<1))throw new y("BAD_REQUEST","@lunora/ai/rag: `candidates` must be a positive integer");if(e.cacheEmbeddings!==void 0&&(!Number.isInteger(e.cacheEmbeddings)||e.cacheEmbeddings<0))throw new y("BAD_REQUEST","@lunora/ai/rag: `cacheEmbeddings` must be a non-negative integer");const k=e.cacheEmbeddings??0,v=e.rerank,T=e.chunk??(g=>Be(g,i,l)),{textStore:N}=e,R=e.embeddingModelVersion,F=g=>R===void 0?g:g===void 0?R:`${R}::${g}`;return g=>{const I=e.store?e.store(g):Fe(rt(g.vectors),e.index),W=N?I.capabilities.maxTopK:I.capabilities.maxTopKWithMetadata,K=e.maxEmbeddingDimensions??I.capabilities.maxDimensions;let C;const J=typeof g.trace=="function"?g.trace:void 0;let Z=K===!1;const X=(t,n)=>{if(Z||(Z=!0,K===!1||t<=K))return;const r=P(n);throw new y("BAD_REQUEST",`@lunora/ai/rag: embedding model${r===void 0?"":` "${r}"`} produces ${String(t)}-dimension vectors, over the ${String(K)}-dimension ceiling of index "${e.index}" — either truncate them with the provider's \`dimensions\` option (Matryoshka models such as text-embedding-3-large support this), or set \`maxEmbeddingDimensions: false\` if this index is not Vectorize-backed`)},A=new Map,Ee=(t,n)=>{if(k!==0)for(A.set(t,n);A.size>k;){const r=A.keys().next();if(r.done===!0)break;A.delete(r.value)}},G=async t=>{const n=A.get(t);if(n!==void 0)return n;C??=pe(e.embeddingModel,g.ai);const r=C,a=async o=>{const{embedding:h,providerMetadata:S,usage:m}=await Ce({model:r,value:t});if(X(h.length,r),o!==void 0){const d=m.tokens;typeof d=="number"&&Number.isFinite(d)&&o.setAttribute("gen_ai.usage.input_tokens",d);const u=nt(S),b=u??$e(P(r),{inputTokens:typeof d=="number"?d:void 0});b!==void 0&&(o.setAttribute("gen_ai.usage.cost",b),o.setAttribute("lunora.usage.cost.source",u===void 0?"estimated":"provider"))}return Ee(t,h),h};if(J===void 0)return a();const s=P(r),c=typeof g.conversationId=="string"&&g.conversationId.length>0?g.conversationId:void 0;return J("ai.embed",(o,h)=>a(h),{"gen_ai.operation.name":"embeddings",...s===void 0?{}:{"gen_ai.request.model":s},...c===void 0?{}:{"gen_ai.conversation.id":c}})},ve=async(t,n,r)=>{if(!e.transformQuery||n?.transformQuery===!1)return[t];const a=typeof g.conversationId=="string"&&g.conversationId.length>0?g.conversationId:void 0,s=await e.transformQuery(t,{conversationId:a,namespace:r}),c=(typeof s=="string"?[s]:[...s]).map(o=>o.trim()).filter(o=>o.length>0);return c.length>0?c:[t]},xe=async t=>{const n=new Map,r=[...new Set(t.filter(a=>!A.has(a)))];if(r.length<2)return n;C??=pe(e.embeddingModel,g.ai);try{const{embeddings:a}=await Re({model:C,values:r});if(a.length!==r.length)return n;const[s]=a;s!==void 0&&X(s.length,C);for(const[c,o]of r.entries())n.set(o,a[c])}catch(a){if(Me(a))throw a}return n},V=t=>{if(t===void 0){if(e.requireNamespace)throw new y("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||et(e.index)}},ee=async(t,n)=>{const[r]=await I.getByIds([D(n,t,0)],n),a=r?.metadata?.[q],s=r?.metadata?.[H];return{chunks:typeof s=="number"&&Number.isInteger(s)&&s>0?s:void 0,hash:typeof a=="string"?a:void 0}},te=async(t,n,r,a)=>{const s=Array.from({length:r-n},(c,o)=>D(a,t,n+o));s.length!==0&&(await I.deleteByIds(s,a),await N?.remove?.(s,{namespace:a}),await e.lexicalStore?.remove?.(s,{namespace:a}))},Ie=async t=>{if(V(t.namespace),t.importance!==void 0&&(typeof t.importance!="number"||t.importance<0||t.importance>1))throw new y("BAD_REQUEST","@lunora/ai/rag: `importance` must be a number in [0, 1]");const n=F(t.namespace),r=Xe(t),a=await Ze(r??t.text),s=await ee(t.id,n);if(t.reindex!==!0&&r!==void 0&&s.hash===a&&s.chunks!==void 0)return{chunks:s.chunks,ids:Array.from({length:s.chunks},(d,u)=>D(n,t.id,u)),unchanged:!0};const c=T(t.text),o=c.map((d,u)=>D(n,t.id,u)),h=o.at(-1);if(h!==void 0&&We(h,t.id,I.capabilities.maxIdBytes),c.length===0&&t.allowEmptySources===!1)throw new y("BAD_REQUEST",`@lunora/ai/rag: source "${t.id}" produced zero chunks — set allowEmptySources: true to allow this`);if(c.length>0){const d=c.map((u,b)=>({chunkIndex:b,id:o[b],sourceId:t.id,text:u,...t.metadata===void 0?{}:{metadata:t.metadata}}));N&&await N.put(d,{namespace:n}),e.lexicalStore&&await e.lexicalStore.index(d,{namespace:n})}const S=await xe(c),m=async d=>S.get(d)??await G(d);return await Ke(c,Ue,async(d,u)=>{const b=o[u],_={...t.metadata,[ge]:u,[be]:t.id};N||(_[O]=d),t.importance!==void 0&&(_[B]=t.importance),u===0&&(_[q]=a,_[H]=c.length,R!==void 0&&(_[ye]=R)),He(_,u,t.id,I.capabilities.maxMetadataBytes),await I.upsert({embed:m,id:b,input:d,metadata:_,namespace:n}),t.onChunk?.({chunkIndex:u,id:b,text:d,total:c.length})}),s.chunks!==void 0&&s.chunks>c.length&&await te(t.id,c.length,s.chunks,n),{chunks:c.length,ids:o,unchanged:!1}},Se=async t=>{V(t.namespace);const n=F(t.namespace),a=(await ee(t.id,n)).chunks??1;await te(t.id,0,a,n)},ne=async(t,n)=>{const r=new Map;if(t.length===0)return r;if(N){const s=await N.getMany(t,{namespace:n});for(const[c,o]of t.entries()){const h=s[c];typeof h=="string"&&r.set(o,h)}return r}const a=await I.getByIds(t,n);for(const s of a){const c=s.metadata?.[O];typeof c=="string"&&r.set(s.id,c)}return r},ke=async(t,n,r)=>{const a=n?.chunkContext?.before??0,s=n?.chunkContext?.after??0;if(a===0&&s===0)return t;if(!Number.isInteger(a)||a<0||!Number.isInteger(s)||s<0)throw new y("BAD_REQUEST","@lunora/ai/rag: `chunkContext.before`/`chunkContext.after` must be non-negative integers");const c=new Map(t.map(m=>[m.id,m.text])),o=new Set;for(const m of t)for(let d=-a;d<=s;d+=1){const u=m.chunkIndex+d,b=D(r,m.sourceId,u);d!==0&&u>=0&&!c.has(b)&&o.add(b)}const h=await ne([...o],r),S=(m,d)=>{const u=D(r,m,d);return c.get(u)??h.get(u)};return t.map(m=>{const d=[];for(let u=-a;u<=s;u+=1){const b=u===0?m.text:S(m.sourceId,m.chunkIndex+u);b!==void 0&&d.push(b)}return{...m,text:d.join(`
|
|
8
|
-
`)}})},_e=t=>{if(typeof t=="string"){const n=e.filters!==void 0&&Object.hasOwn(e.filters,t)?e.filters[t]:void 0;if(!n)throw new y("NOT_FOUND",`@lunora/ai/rag: unknown named filter "${t}" — must be one of the keys declared in RagConfig.filters`);return n.filter}return t},Te=(t,n)=>t.matches.map(r=>{const a=r.metadata??{},s=he(r.id,n),c=a[O],o=a[B],h=typeof o=="number"&&o>=0&&o<=1?o:1;return{chunkIndex:s.chunkIndex,id:r.id,importance:h,metadata:L(a),score:r.score*h,sourceId:s.sourceId,text:typeof c=="string"?c:""}}),Ne=async(t,n)=>{if(!N)return t;const r=t.map(o=>o.id),[a,s]=await Promise.all([ne(r,n),I.getByIds(r,n)]),c=new Map(s.map(o=>[o.id,o.metadata]));return t.flatMap(o=>{const h=a.get(o.id);if(h===void 0)return[];const S=c.get(o.id),m=S?.[B],d=typeof m=="number"&&m>=0&&m<=1?m:o.importance,b=(o.importance===0?0:o.score/o.importance)*d;return[{...o,importance:d,metadata:L(S)??o.metadata,score:b,text:h}]})},re=async(t,n)=>{V(n?.namespace);const r=F(n?.namespace),a=_e(n?.filter),s=e.rlsFilter?await e.rlsFilter(g.auth):void 0,c=s?{...a,...s}:a,o=Math.min(n?.topK??f,W),h=await ve(t,n,r),S=h[0],m=v!==void 0&&n?.rerank!==!1,u=m||e.lexicalStore!==void 0||h.length>1?Math.min(e.candidates??o*Pe,W):o,b=n?.minScore,_=new Set,se=async x=>{const M=await I.query({embed:G,filter:c,input:x,namespace:r,returnMetadata:N?"indexed":"all",topK:u}),$=await Ne(Te(M,r),r);if(b===void 0)return $;const{kept:U,rejectedIds:j}=Ge($,b);for(const z of j)_.add(z);return U};let E=await se(S);for(const x of h.slice(1))E=[...de(E,await se(x))];if(e.lexicalStore){const x=await e.lexicalStore.search(S,{filter:c,namespace:r,topK:e.lexicalTopK??u}),M=new Set(E.map(w=>w.id)),$=x.filter(w=>!_.has(w.id)||M.has(w.id)),U=$.map(w=>w.id).filter(w=>!M.has(w)),j=U.length===0?[]:await I.getByIds(U,r),z=new Map(j.map(w=>[w.id,w.metadata])),Ae=$.map(w=>{const ie=he(w.id,r),ce=z.get(w.id),Q=ce?.[B];return{chunkIndex:ie.chunkIndex,id:w.id,importance:typeof Q=="number"&&Q>=0&&Q<=1?Q:1,metadata:L(ce),score:w.score,sourceId:ie.sourceId,text:w.text}});E=[...de(E,Ae)]}E.sort((x,M)=>M.score-x.score),m&&(E=[...await v(S,E)]),E=E.slice(0,o),E=[...await ke(E,n,r)];const oe=[],ae=new Set;for(const x of E)ae.has(x.sourceId)||(ae.add(x.sourceId),oe.push({id:x.sourceId,metadata:x.metadata,weight:x.importance}));return n?.onRetrieve?.({matches:E.length,query:t}),{chunks:E,context:tt(E),sources:oe}};return{asTool:t=>De({description:t?.description??`Search the "${e.index}" knowledge base for passages relevant to a natural-language query.`,execute:async({query:n})=>re(n,{namespace:t?.namespace,topK:t?.topK}),inputSchema:Oe({properties:{query:{description:"The natural-language search query.",type:"string"}},required:["query"],type:"object"})}),index:Ie,remove:Se,retrieve:re}}};export{ht as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const a=(s,t,n=60)=>{const c=new Map;for(const[e,r]of s.entries())c.set(r.id,{chunk:r,score:1/(n+e),vectorRank:e});for(const[e,r]of t.entries()){const o=c.get(r.id);o?o.score+=1/(n+e):c.set(r.id,{chunk:r,score:1/(n+e),vectorRank:Number.POSITIVE_INFINITY})}return[...c.values()].map(e=>({...e,scored:{...e.chunk,score:e.score*e.chunk.importance}})).toSorted((e,r)=>{const o=r.scored.score-e.scored.score;return o!==0?o:e.vectorRank===r.vectorRank?0:e.vectorRank<r.vectorRank?-1:1}).map(e=>e.scored)};export{a as default};
|