@arnilo/prism-rag 0.0.5
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/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/dist/chunk.d.ts +3 -0
- package/dist/chunk.js +67 -0
- package/dist/context.d.ts +3 -0
- package/dist/context.js +25 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +33 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/indexing.d.ts +2 -0
- package/dist/indexing.js +89 -0
- package/dist/limits.d.ts +36 -0
- package/dist/limits.js +55 -0
- package/dist/retrieve.d.ts +2 -0
- package/dist/retrieve.js +135 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.js +2 -0
- package/dist/util.d.ts +19 -0
- package/dist/util.js +94 -0
- package/package.json +60 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.0.5] - 2026-07-16
|
|
6
|
+
|
|
7
|
+
- Added optional bounded text/Markdown chunking, vector indexing/retrieval, and stable citations.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [0.0.4] - 2026-07-14
|
|
11
|
+
|
|
12
|
+
- Initial release: bounded deterministic text/Markdown chunking, batch vector indexing, filtered retrieval, stable citations, and ContextProvider integration.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Prism contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @arnilo/prism-rag
|
|
2
|
+
|
|
3
|
+
Optional bounded text/Markdown retrieval-augmented generation primitives for Prism. Reuses `Embedder` and `VectorStore` from `@arnilo/prism-memory`; no document framework, network loader, or core activation.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @arnilo/prism-rag @arnilo/prism-memory @arnilo/prism
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createHashEmbedder, createMemoryVectorStore } from "@arnilo/prism-memory";
|
|
15
|
+
import { chunkMarkdown, createRagContextProvider, indexChunks } from "@arnilo/prism-rag";
|
|
16
|
+
|
|
17
|
+
const embedder = createHashEmbedder(); // demo/test only
|
|
18
|
+
const store = createMemoryVectorStore();
|
|
19
|
+
const scope = { tenantId: "t1", resourceId: "docs", corpusId: "handbook" };
|
|
20
|
+
const chunks = chunkMarkdown("# Approval\n\nRecheck policy before side effects.", {
|
|
21
|
+
sourceId: "security-guide",
|
|
22
|
+
});
|
|
23
|
+
await indexChunks({ chunks, embedder, store, scope });
|
|
24
|
+
const context = createRagContextProvider({ embedder, store, scope, topK: 4 });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## API
|
|
28
|
+
|
|
29
|
+
- `chunkText()` / `chunkMarkdown()` — deterministic boundary-aware character chunks with overlap and stable citations.
|
|
30
|
+
- `indexChunks()` — bounded batch embedding and scoped vector upsert.
|
|
31
|
+
- `retrieveContext()` — bounded candidate query, shallow metadata filter, top-K hits, and citation rendering.
|
|
32
|
+
- `createRagContextProvider()` — explicit inert context injection through Prism's existing seam.
|
|
33
|
+
|
|
34
|
+
## Security
|
|
35
|
+
|
|
36
|
+
Every operation requires tenant/resource/corpus scope. Configure `redactor` or `secrets` before external embedding/persistence. Package performs no I/O; load remote/local sources through host-owned resource/media policies. Retrieved text is untrusted context and grants no tools or permissions.
|
|
37
|
+
|
|
38
|
+
See [RAG](../../docs/rag.md).
|
package/dist/chunk.d.ts
ADDED
package/dist/chunk.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { resolveRagLimits } from "./limits.js";
|
|
2
|
+
import { RagLimitError } from "./errors.js";
|
|
3
|
+
import { assertBytes, requireSourceId } from "./util.js";
|
|
4
|
+
export function chunkText(text, options) {
|
|
5
|
+
return chunkDocument(text, options, false);
|
|
6
|
+
}
|
|
7
|
+
export function chunkMarkdown(markdown, options) {
|
|
8
|
+
return chunkDocument(markdown, options, true);
|
|
9
|
+
}
|
|
10
|
+
function chunkDocument(text, options, markdown) {
|
|
11
|
+
const sourceId = requireSourceId(options.sourceId);
|
|
12
|
+
const limits = resolveRagLimits({
|
|
13
|
+
chunkSize: options.size,
|
|
14
|
+
chunkOverlap: options.overlap,
|
|
15
|
+
maxDocumentChars: options.maxDocumentChars,
|
|
16
|
+
maxChunks: options.maxChunks,
|
|
17
|
+
});
|
|
18
|
+
if (text.length > limits.maxDocumentChars) {
|
|
19
|
+
throw new RagLimitError(`document exceeds ${limits.maxDocumentChars} characters`);
|
|
20
|
+
}
|
|
21
|
+
if (options.metadata)
|
|
22
|
+
assertBytes(options.metadata, 64 * 1024, "chunk metadata");
|
|
23
|
+
if (!text.trim())
|
|
24
|
+
return Object.freeze([]);
|
|
25
|
+
const chunks = [];
|
|
26
|
+
let start = 0;
|
|
27
|
+
while (start < text.length) {
|
|
28
|
+
while (start < text.length && /\s/.test(text[start]))
|
|
29
|
+
start += 1;
|
|
30
|
+
if (start >= text.length)
|
|
31
|
+
break;
|
|
32
|
+
const ceiling = Math.min(start + limits.chunkSize, text.length);
|
|
33
|
+
const end = ceiling === text.length ? ceiling : preferredEnd(text, start, ceiling, markdown);
|
|
34
|
+
const raw = text.slice(start, end).trimEnd();
|
|
35
|
+
if (raw) {
|
|
36
|
+
const index = chunks.length;
|
|
37
|
+
const citationId = `${sourceId}#${String(index + 1).padStart(4, "0")}`;
|
|
38
|
+
chunks.push(Object.freeze({
|
|
39
|
+
id: citationId,
|
|
40
|
+
citationId,
|
|
41
|
+
sourceId,
|
|
42
|
+
index,
|
|
43
|
+
start,
|
|
44
|
+
end: start + raw.length,
|
|
45
|
+
text: raw,
|
|
46
|
+
...(options.metadata ? { metadata: Object.freeze({ ...options.metadata }) } : {}),
|
|
47
|
+
}));
|
|
48
|
+
if (chunks.length > limits.maxChunks)
|
|
49
|
+
throw new RagLimitError(`chunk count exceeds ${limits.maxChunks}`);
|
|
50
|
+
}
|
|
51
|
+
if (end >= text.length)
|
|
52
|
+
break;
|
|
53
|
+
start = Math.max(start + 1, end - limits.chunkOverlap);
|
|
54
|
+
}
|
|
55
|
+
return Object.freeze(chunks);
|
|
56
|
+
}
|
|
57
|
+
function preferredEnd(text, start, ceiling, markdown) {
|
|
58
|
+
const floor = start + Math.floor((ceiling - start) / 2);
|
|
59
|
+
const candidates = markdown ? ["\n#", "\n\n", "\n", " "] : ["\n\n", "\n", " "];
|
|
60
|
+
for (const separator of candidates) {
|
|
61
|
+
const found = text.lastIndexOf(separator, ceiling);
|
|
62
|
+
if (found >= floor)
|
|
63
|
+
return separator === "\n#" ? found : found + separator.length;
|
|
64
|
+
}
|
|
65
|
+
return ceiling;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=chunk.js.map
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { retrieveContext } from "./retrieve.js";
|
|
2
|
+
import { latestUserText } from "./util.js";
|
|
3
|
+
export function createRagContextProvider(options) {
|
|
4
|
+
return {
|
|
5
|
+
name: options.name ?? "rag",
|
|
6
|
+
async resolve(context) {
|
|
7
|
+
context.signal?.throwIfAborted();
|
|
8
|
+
const query = typeof options.query === "function"
|
|
9
|
+
? options.query({ messages: context.messages })
|
|
10
|
+
: options.query ?? latestUserText(context.messages);
|
|
11
|
+
if (!query?.trim())
|
|
12
|
+
return [];
|
|
13
|
+
const result = await retrieveContext(query, { ...options, signal: context.signal });
|
|
14
|
+
if (!result.text)
|
|
15
|
+
return [];
|
|
16
|
+
return [{
|
|
17
|
+
id: `${options.name ?? "rag"}:context`,
|
|
18
|
+
title: options.title ?? "Retrieved context",
|
|
19
|
+
content: result.text,
|
|
20
|
+
metadata: { citations: result.citations, inert: true },
|
|
21
|
+
}];
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=context.js.map
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class RagError extends Error {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
constructor(message: string, code?: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class RagValidationError extends RagError {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class RagLimitError extends RagError {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
export declare class RagScopeError extends RagError {
|
|
12
|
+
constructor(message: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class RagAbortError extends RagError {
|
|
15
|
+
constructor();
|
|
16
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export class RagError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(message, code = "ERR_PRISM_RAG") {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "RagError";
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class RagValidationError extends RagError {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message, "ERR_PRISM_RAG_VALIDATION");
|
|
12
|
+
this.name = "RagValidationError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class RagLimitError extends RagError {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message, "ERR_PRISM_RAG_LIMIT");
|
|
18
|
+
this.name = "RagLimitError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class RagScopeError extends RagError {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message, "ERR_PRISM_RAG_SCOPE");
|
|
24
|
+
this.name = "RagScopeError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class RagAbortError extends RagError {
|
|
28
|
+
constructor() {
|
|
29
|
+
super("RAG operation aborted", "ERR_PRISM_RAG_ABORTED");
|
|
30
|
+
this.name = "AbortError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=errors.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { DEFAULT_CHUNK_SIZE, HARD_CHUNK_SIZE_CAP, DEFAULT_CHUNK_OVERLAP, HARD_CHUNK_OVERLAP_CAP, DEFAULT_MAX_DOCUMENT_CHARS, HARD_MAX_DOCUMENT_CHARS_CAP, DEFAULT_MAX_CHUNKS, HARD_MAX_CHUNKS_CAP, DEFAULT_EMBED_BATCH_SIZE, HARD_EMBED_BATCH_SIZE_CAP, DEFAULT_TOP_K, HARD_TOP_K_CAP, DEFAULT_QUERY_CANDIDATES, HARD_QUERY_CANDIDATES_CAP, DEFAULT_MAX_RESULT_BYTES, HARD_MAX_RESULT_BYTES_CAP, DEFAULT_MAX_CONTEXT_TOKENS, HARD_MAX_CONTEXT_TOKENS_CAP, DEFAULT_MAX_METADATA_BYTES, HARD_MAX_METADATA_BYTES_CAP, DEFAULT_MAX_VECTOR_DIMENSIONS, resolveRagLimits, } from "./limits.js";
|
|
2
|
+
export type { RagLimits, RagLimitsInput } from "./limits.js";
|
|
3
|
+
export { RagAbortError, RagError, RagLimitError, RagScopeError, RagValidationError } from "./errors.js";
|
|
4
|
+
export { chunkMarkdown, chunkText } from "./chunk.js";
|
|
5
|
+
export { indexChunks } from "./indexing.js";
|
|
6
|
+
export { retrieveContext } from "./retrieve.js";
|
|
7
|
+
export { createRagContextProvider } from "./context.js";
|
|
8
|
+
export type { ChunkOptions, IndexChunksOptions, IndexChunksResult, RagChunk, RagCitation, RagContextProvider, RagContextProviderOptions, RagContextResult, RagHit, RagScope, RetrieveContextOptions, } from "./types.js";
|
|
9
|
+
export declare const packageName = "@arnilo/prism-rag";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { DEFAULT_CHUNK_SIZE, HARD_CHUNK_SIZE_CAP, DEFAULT_CHUNK_OVERLAP, HARD_CHUNK_OVERLAP_CAP, DEFAULT_MAX_DOCUMENT_CHARS, HARD_MAX_DOCUMENT_CHARS_CAP, DEFAULT_MAX_CHUNKS, HARD_MAX_CHUNKS_CAP, DEFAULT_EMBED_BATCH_SIZE, HARD_EMBED_BATCH_SIZE_CAP, DEFAULT_TOP_K, HARD_TOP_K_CAP, DEFAULT_QUERY_CANDIDATES, HARD_QUERY_CANDIDATES_CAP, DEFAULT_MAX_RESULT_BYTES, HARD_MAX_RESULT_BYTES_CAP, DEFAULT_MAX_CONTEXT_TOKENS, HARD_MAX_CONTEXT_TOKENS_CAP, DEFAULT_MAX_METADATA_BYTES, HARD_MAX_METADATA_BYTES_CAP, DEFAULT_MAX_VECTOR_DIMENSIONS, resolveRagLimits, } from "./limits.js";
|
|
2
|
+
export { RagAbortError, RagError, RagLimitError, RagScopeError, RagValidationError } from "./errors.js";
|
|
3
|
+
export { chunkMarkdown, chunkText } from "./chunk.js";
|
|
4
|
+
export { indexChunks } from "./indexing.js";
|
|
5
|
+
export { retrieveContext } from "./retrieve.js";
|
|
6
|
+
export { createRagContextProvider } from "./context.js";
|
|
7
|
+
export const packageName = "@arnilo/prism-rag";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
package/dist/indexing.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { HARD_CHUNK_SIZE_CAP, resolveRagLimits } from "./limits.js";
|
|
2
|
+
import { RagValidationError } from "./errors.js";
|
|
3
|
+
import { assertBytes, assertNotAborted, nonEmpty, requireScope, requireSourceId, resolveRedactor } from "./util.js";
|
|
4
|
+
// ponytail: stable IDs make identical retries idempotent; replacing a source with
|
|
5
|
+
// fewer chunks requires host deletion of stale source IDs before this generic upsert.
|
|
6
|
+
export async function indexChunks(options) {
|
|
7
|
+
const scope = requireScope(options.scope);
|
|
8
|
+
const limits = resolveRagLimits({
|
|
9
|
+
embedBatchSize: options.batchSize,
|
|
10
|
+
maxChunks: options.maxChunks,
|
|
11
|
+
chunkSize: options.maxChunkChars ?? HARD_CHUNK_SIZE_CAP,
|
|
12
|
+
maxVectorDimensions: options.maxVectorDimensions,
|
|
13
|
+
maxMetadataBytes: options.maxMetadataBytes,
|
|
14
|
+
});
|
|
15
|
+
if (!Number.isInteger(options.embedder.dimensions)
|
|
16
|
+
|| options.embedder.dimensions <= 0
|
|
17
|
+
|| options.embedder.dimensions > limits.maxVectorDimensions) {
|
|
18
|
+
throw new RagValidationError(`embedder dimensions must be an integer in 1..${limits.maxVectorDimensions}`);
|
|
19
|
+
}
|
|
20
|
+
if (options.chunks.length > limits.maxChunks) {
|
|
21
|
+
throw new RagValidationError(`chunk count exceeds ${limits.maxChunks}`);
|
|
22
|
+
}
|
|
23
|
+
const redactor = resolveRedactor(options.redactor, options.secrets);
|
|
24
|
+
const sourceIds = new Set();
|
|
25
|
+
const chunkIds = new Set();
|
|
26
|
+
for (const chunk of options.chunks) {
|
|
27
|
+
nonEmpty(chunk.id, "chunk.id");
|
|
28
|
+
requireSourceId(chunk.sourceId);
|
|
29
|
+
if (chunk.id !== chunk.citationId || !chunk.citationId.startsWith(`${chunk.sourceId}#`)) {
|
|
30
|
+
throw new RagValidationError("chunk has inconsistent citation identity");
|
|
31
|
+
}
|
|
32
|
+
if (chunkIds.has(chunk.id))
|
|
33
|
+
throw new RagValidationError(`duplicate chunk id: ${chunk.id}`);
|
|
34
|
+
if (!Number.isInteger(chunk.index)
|
|
35
|
+
|| chunk.index < 0
|
|
36
|
+
|| !Number.isInteger(chunk.start)
|
|
37
|
+
|| chunk.start < 0
|
|
38
|
+
|| !Number.isInteger(chunk.end)
|
|
39
|
+
|| chunk.end < chunk.start) {
|
|
40
|
+
throw new RagValidationError("chunk has invalid index or offsets");
|
|
41
|
+
}
|
|
42
|
+
if (chunk.text.length > limits.chunkSize)
|
|
43
|
+
throw new RagValidationError(`chunk text exceeds ${limits.chunkSize} characters`);
|
|
44
|
+
chunkIds.add(chunk.id);
|
|
45
|
+
}
|
|
46
|
+
for (let offset = 0; offset < options.chunks.length; offset += limits.embedBatchSize) {
|
|
47
|
+
assertNotAborted(options.signal);
|
|
48
|
+
const batch = options.chunks.slice(offset, offset + limits.embedBatchSize);
|
|
49
|
+
const texts = batch.map((chunk) => redactor?.redact(chunk.text) ?? chunk.text);
|
|
50
|
+
const vectors = await options.embedder.embed(texts, { signal: options.signal });
|
|
51
|
+
if (vectors.length !== batch.length)
|
|
52
|
+
throw new RagValidationError("embedder returned unexpected vector count");
|
|
53
|
+
const records = batch.map((chunk, index) => {
|
|
54
|
+
const embedding = vectors[index];
|
|
55
|
+
if (embedding.length !== options.embedder.dimensions
|
|
56
|
+
|| embedding.some((value) => !Number.isFinite(value))) {
|
|
57
|
+
throw new RagValidationError(`embedder returned invalid vector; expected ${options.embedder.dimensions} finite values`);
|
|
58
|
+
}
|
|
59
|
+
const safeMetadata = redactor?.redact(chunk.metadata ?? {}) ?? (chunk.metadata ?? {});
|
|
60
|
+
const metadata = {
|
|
61
|
+
...safeMetadata,
|
|
62
|
+
_rag: {
|
|
63
|
+
sourceId: chunk.sourceId,
|
|
64
|
+
citationId: chunk.citationId,
|
|
65
|
+
chunkIndex: chunk.index,
|
|
66
|
+
start: chunk.start,
|
|
67
|
+
end: chunk.end,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
assertBytes(metadata, limits.maxMetadataBytes, "chunk metadata");
|
|
71
|
+
sourceIds.add(chunk.sourceId);
|
|
72
|
+
return {
|
|
73
|
+
id: chunk.id,
|
|
74
|
+
tenantId: scope.tenantId,
|
|
75
|
+
resourceId: scope.resourceId,
|
|
76
|
+
threadId: scope.corpusId,
|
|
77
|
+
text: texts[index],
|
|
78
|
+
embedding,
|
|
79
|
+
sequence: chunk.index,
|
|
80
|
+
metadata: metadata,
|
|
81
|
+
createdAt: new Date(0).toISOString(),
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
assertNotAborted(options.signal);
|
|
85
|
+
await options.store.upsert(records, { signal: options.signal });
|
|
86
|
+
}
|
|
87
|
+
return Object.freeze({ indexed: options.chunks.length, sourceIds: Object.freeze([...sourceIds].sort()) });
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=indexing.js.map
|
package/dist/limits.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare const DEFAULT_CHUNK_SIZE = 1000;
|
|
2
|
+
export declare const HARD_CHUNK_SIZE_CAP = 16384;
|
|
3
|
+
export declare const DEFAULT_CHUNK_OVERLAP = 100;
|
|
4
|
+
export declare const HARD_CHUNK_OVERLAP_CAP = 4096;
|
|
5
|
+
export declare const DEFAULT_MAX_DOCUMENT_CHARS = 1048576;
|
|
6
|
+
export declare const HARD_MAX_DOCUMENT_CHARS_CAP = 8388608;
|
|
7
|
+
export declare const DEFAULT_MAX_CHUNKS = 2048;
|
|
8
|
+
export declare const HARD_MAX_CHUNKS_CAP = 8192;
|
|
9
|
+
export declare const DEFAULT_EMBED_BATCH_SIZE = 32;
|
|
10
|
+
export declare const HARD_EMBED_BATCH_SIZE_CAP = 128;
|
|
11
|
+
export declare const DEFAULT_TOP_K = 5;
|
|
12
|
+
export declare const HARD_TOP_K_CAP = 32;
|
|
13
|
+
export declare const DEFAULT_QUERY_CANDIDATES = 20;
|
|
14
|
+
export declare const HARD_QUERY_CANDIDATES_CAP = 128;
|
|
15
|
+
export declare const DEFAULT_MAX_RESULT_BYTES: number;
|
|
16
|
+
export declare const HARD_MAX_RESULT_BYTES_CAP: number;
|
|
17
|
+
export declare const DEFAULT_MAX_CONTEXT_TOKENS = 2000;
|
|
18
|
+
export declare const HARD_MAX_CONTEXT_TOKENS_CAP = 8000;
|
|
19
|
+
export declare const DEFAULT_MAX_METADATA_BYTES: number;
|
|
20
|
+
export declare const HARD_MAX_METADATA_BYTES_CAP: number;
|
|
21
|
+
export declare const DEFAULT_MAX_VECTOR_DIMENSIONS = 4096;
|
|
22
|
+
export interface RagLimits {
|
|
23
|
+
readonly chunkSize: number;
|
|
24
|
+
readonly chunkOverlap: number;
|
|
25
|
+
readonly maxDocumentChars: number;
|
|
26
|
+
readonly maxChunks: number;
|
|
27
|
+
readonly embedBatchSize: number;
|
|
28
|
+
readonly topK: number;
|
|
29
|
+
readonly queryCandidates: number;
|
|
30
|
+
readonly maxResultBytes: number;
|
|
31
|
+
readonly maxContextTokens: number;
|
|
32
|
+
readonly maxMetadataBytes: number;
|
|
33
|
+
readonly maxVectorDimensions: number;
|
|
34
|
+
}
|
|
35
|
+
export type RagLimitsInput = Partial<RagLimits>;
|
|
36
|
+
export declare function resolveRagLimits(input?: RagLimitsInput): RagLimits;
|
package/dist/limits.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { RagLimitError } from "./errors.js";
|
|
2
|
+
export const DEFAULT_CHUNK_SIZE = 1_000;
|
|
3
|
+
export const HARD_CHUNK_SIZE_CAP = 16_384;
|
|
4
|
+
export const DEFAULT_CHUNK_OVERLAP = 100;
|
|
5
|
+
export const HARD_CHUNK_OVERLAP_CAP = 4_096;
|
|
6
|
+
export const DEFAULT_MAX_DOCUMENT_CHARS = 1_048_576;
|
|
7
|
+
export const HARD_MAX_DOCUMENT_CHARS_CAP = 8_388_608;
|
|
8
|
+
export const DEFAULT_MAX_CHUNKS = 2_048;
|
|
9
|
+
export const HARD_MAX_CHUNKS_CAP = 8_192;
|
|
10
|
+
export const DEFAULT_EMBED_BATCH_SIZE = 32;
|
|
11
|
+
export const HARD_EMBED_BATCH_SIZE_CAP = 128;
|
|
12
|
+
export const DEFAULT_TOP_K = 5;
|
|
13
|
+
export const HARD_TOP_K_CAP = 32;
|
|
14
|
+
export const DEFAULT_QUERY_CANDIDATES = 20;
|
|
15
|
+
export const HARD_QUERY_CANDIDATES_CAP = 128;
|
|
16
|
+
export const DEFAULT_MAX_RESULT_BYTES = 64 * 1024;
|
|
17
|
+
export const HARD_MAX_RESULT_BYTES_CAP = 512 * 1024;
|
|
18
|
+
export const DEFAULT_MAX_CONTEXT_TOKENS = 2_000;
|
|
19
|
+
export const HARD_MAX_CONTEXT_TOKENS_CAP = 8_000;
|
|
20
|
+
export const DEFAULT_MAX_METADATA_BYTES = 16 * 1024;
|
|
21
|
+
export const HARD_MAX_METADATA_BYTES_CAP = 64 * 1024;
|
|
22
|
+
export const DEFAULT_MAX_VECTOR_DIMENSIONS = 4_096;
|
|
23
|
+
function integer(value, fallback, cap, label, minimum = 1) {
|
|
24
|
+
const resolved = value ?? fallback;
|
|
25
|
+
if (!Number.isInteger(resolved) || resolved < minimum) {
|
|
26
|
+
throw new RagLimitError(`${label} must be an integer >= ${minimum}`);
|
|
27
|
+
}
|
|
28
|
+
if (resolved > cap)
|
|
29
|
+
throw new RagLimitError(`${label} exceeds hard cap ${cap}`);
|
|
30
|
+
return resolved;
|
|
31
|
+
}
|
|
32
|
+
export function resolveRagLimits(input = {}) {
|
|
33
|
+
const chunkSize = integer(input.chunkSize, DEFAULT_CHUNK_SIZE, HARD_CHUNK_SIZE_CAP, "chunkSize");
|
|
34
|
+
const chunkOverlap = integer(input.chunkOverlap, DEFAULT_CHUNK_OVERLAP, HARD_CHUNK_OVERLAP_CAP, "chunkOverlap", 0);
|
|
35
|
+
if (chunkOverlap >= chunkSize)
|
|
36
|
+
throw new RagLimitError("chunkOverlap must be smaller than chunkSize");
|
|
37
|
+
const topK = integer(input.topK, DEFAULT_TOP_K, HARD_TOP_K_CAP, "topK");
|
|
38
|
+
const queryCandidates = integer(input.queryCandidates, Math.max(DEFAULT_QUERY_CANDIDATES, topK), HARD_QUERY_CANDIDATES_CAP, "queryCandidates");
|
|
39
|
+
if (queryCandidates < topK)
|
|
40
|
+
throw new RagLimitError("queryCandidates must be >= topK");
|
|
41
|
+
return Object.freeze({
|
|
42
|
+
chunkSize,
|
|
43
|
+
chunkOverlap,
|
|
44
|
+
maxDocumentChars: integer(input.maxDocumentChars, DEFAULT_MAX_DOCUMENT_CHARS, HARD_MAX_DOCUMENT_CHARS_CAP, "maxDocumentChars"),
|
|
45
|
+
maxChunks: integer(input.maxChunks, DEFAULT_MAX_CHUNKS, HARD_MAX_CHUNKS_CAP, "maxChunks"),
|
|
46
|
+
embedBatchSize: integer(input.embedBatchSize, DEFAULT_EMBED_BATCH_SIZE, HARD_EMBED_BATCH_SIZE_CAP, "embedBatchSize"),
|
|
47
|
+
topK,
|
|
48
|
+
queryCandidates,
|
|
49
|
+
maxResultBytes: integer(input.maxResultBytes, DEFAULT_MAX_RESULT_BYTES, HARD_MAX_RESULT_BYTES_CAP, "maxResultBytes"),
|
|
50
|
+
maxContextTokens: integer(input.maxContextTokens, DEFAULT_MAX_CONTEXT_TOKENS, HARD_MAX_CONTEXT_TOKENS_CAP, "maxContextTokens"),
|
|
51
|
+
maxMetadataBytes: integer(input.maxMetadataBytes, DEFAULT_MAX_METADATA_BYTES, HARD_MAX_METADATA_BYTES_CAP, "maxMetadataBytes"),
|
|
52
|
+
maxVectorDimensions: integer(input.maxVectorDimensions, DEFAULT_MAX_VECTOR_DIMENSIONS, DEFAULT_MAX_VECTOR_DIMENSIONS, "maxVectorDimensions"),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=limits.js.map
|
package/dist/retrieve.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { HARD_CHUNK_SIZE_CAP, resolveRagLimits } from "./limits.js";
|
|
2
|
+
import { RagScopeError, RagValidationError } from "./errors.js";
|
|
3
|
+
import { assertBytes, assertNotAborted, assertScope, byteLength, isJsonObject, matchesFilter, nonEmpty, requireScope, requireSourceId, resolveRedactor, truncateUtf8, } from "./util.js";
|
|
4
|
+
export async function retrieveContext(query, options) {
|
|
5
|
+
nonEmpty(query, "query");
|
|
6
|
+
if (query.length > HARD_CHUNK_SIZE_CAP) {
|
|
7
|
+
throw new RagValidationError(`query exceeds ${HARD_CHUNK_SIZE_CAP} characters`);
|
|
8
|
+
}
|
|
9
|
+
const scope = requireScope(options.scope);
|
|
10
|
+
const limits = resolveRagLimits({
|
|
11
|
+
topK: options.topK,
|
|
12
|
+
queryCandidates: options.queryCandidates,
|
|
13
|
+
maxResultBytes: options.maxResultBytes,
|
|
14
|
+
maxContextTokens: options.maxContextTokens,
|
|
15
|
+
maxMetadataBytes: options.maxMetadataBytes,
|
|
16
|
+
maxVectorDimensions: options.maxVectorDimensions,
|
|
17
|
+
});
|
|
18
|
+
if (!Number.isInteger(options.embedder.dimensions)
|
|
19
|
+
|| options.embedder.dimensions <= 0
|
|
20
|
+
|| options.embedder.dimensions > limits.maxVectorDimensions) {
|
|
21
|
+
throw new RagValidationError(`embedder dimensions must be an integer in 1..${limits.maxVectorDimensions}`);
|
|
22
|
+
}
|
|
23
|
+
if (options.filter)
|
|
24
|
+
assertBytes(options.filter, limits.maxMetadataBytes, "metadata filter");
|
|
25
|
+
const redactor = resolveRedactor(options.redactor, options.secrets);
|
|
26
|
+
const safeQuery = redactor?.redact(query) ?? query;
|
|
27
|
+
assertNotAborted(options.signal);
|
|
28
|
+
const vectors = await options.embedder.embed([safeQuery], { signal: options.signal });
|
|
29
|
+
const embedding = vectors[0];
|
|
30
|
+
if (vectors.length !== 1
|
|
31
|
+
|| !embedding
|
|
32
|
+
|| embedding.length !== options.embedder.dimensions
|
|
33
|
+
|| embedding.some((value) => !Number.isFinite(value))) {
|
|
34
|
+
throw new RagValidationError("embedder returned invalid query vector");
|
|
35
|
+
}
|
|
36
|
+
const candidates = await options.store.query({
|
|
37
|
+
tenantId: scope.tenantId,
|
|
38
|
+
resourceId: scope.resourceId,
|
|
39
|
+
threadId: scope.corpusId,
|
|
40
|
+
embedding,
|
|
41
|
+
topK: limits.queryCandidates,
|
|
42
|
+
signal: options.signal,
|
|
43
|
+
});
|
|
44
|
+
assertNotAborted(options.signal);
|
|
45
|
+
const hits = [];
|
|
46
|
+
const citations = [];
|
|
47
|
+
const rendered = [];
|
|
48
|
+
const maxChars = limits.maxContextTokens * 4;
|
|
49
|
+
let usedBytes = 0;
|
|
50
|
+
let usedChars = 0;
|
|
51
|
+
let truncated = false;
|
|
52
|
+
for (const candidate of candidates.slice(0, limits.queryCandidates)) {
|
|
53
|
+
assertScope(scope, candidate);
|
|
54
|
+
const parsed = parseHit(candidate);
|
|
55
|
+
if (!matchesFilter(parsed.metadata, options.filter))
|
|
56
|
+
continue;
|
|
57
|
+
if (hits.length >= limits.topK)
|
|
58
|
+
break;
|
|
59
|
+
const safe = redactor?.redact(parsed) ?? parsed;
|
|
60
|
+
const prefix = `[${parsed.citationId}] `;
|
|
61
|
+
const separator = rendered.length ? "\n\n" : "";
|
|
62
|
+
const availableBytes = limits.maxResultBytes - usedBytes - byteLength(separator + prefix);
|
|
63
|
+
const availableChars = maxChars - usedChars - separator.length - prefix.length;
|
|
64
|
+
if (availableBytes <= 0 || availableChars <= 0) {
|
|
65
|
+
truncated = true;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
let text = safe.text.slice(0, availableChars);
|
|
69
|
+
text = truncateUtf8(text, availableBytes);
|
|
70
|
+
if (!text) {
|
|
71
|
+
truncated = true;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
if (text.length < safe.text.length)
|
|
75
|
+
truncated = true;
|
|
76
|
+
const hit = Object.freeze({ ...safe, text });
|
|
77
|
+
const citation = Object.freeze({
|
|
78
|
+
id: hit.citationId,
|
|
79
|
+
sourceId: hit.sourceId,
|
|
80
|
+
chunkId: hit.id,
|
|
81
|
+
...(hit.metadata ? { metadata: hit.metadata } : {}),
|
|
82
|
+
});
|
|
83
|
+
const block = `${separator}${prefix}${text}`;
|
|
84
|
+
rendered.push(block);
|
|
85
|
+
usedBytes += byteLength(block);
|
|
86
|
+
usedChars += block.length;
|
|
87
|
+
hits.push(hit);
|
|
88
|
+
citations.push(citation);
|
|
89
|
+
if (truncated)
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
return Object.freeze({
|
|
93
|
+
query: safeQuery,
|
|
94
|
+
text: rendered.join(""),
|
|
95
|
+
hits: Object.freeze(hits),
|
|
96
|
+
citations: Object.freeze(citations),
|
|
97
|
+
truncated,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function parseHit(hit) {
|
|
101
|
+
const metadata = hit.metadata;
|
|
102
|
+
const rag = metadata?._rag;
|
|
103
|
+
if (!isJsonObject(rag))
|
|
104
|
+
throw new RagScopeError("vector hit is missing RAG source metadata");
|
|
105
|
+
const sourceId = requireSourceId(rag.sourceId);
|
|
106
|
+
const citationId = nonEmpty(rag.citationId, "metadata._rag.citationId");
|
|
107
|
+
if (!Number.isInteger(rag.chunkIndex)
|
|
108
|
+
|| Number(rag.chunkIndex) < 0
|
|
109
|
+
|| !Number.isInteger(rag.start)
|
|
110
|
+
|| Number(rag.start) < 0
|
|
111
|
+
|| !Number.isInteger(rag.end)
|
|
112
|
+
|| Number(rag.end) < Number(rag.start)
|
|
113
|
+
|| !Number.isFinite(hit.score)) {
|
|
114
|
+
throw new RagValidationError("vector hit has invalid RAG offsets");
|
|
115
|
+
}
|
|
116
|
+
if (hit.id !== citationId || !citationId.startsWith(`${sourceId}#`)) {
|
|
117
|
+
throw new RagValidationError("vector hit has inconsistent citation identity");
|
|
118
|
+
}
|
|
119
|
+
const userMetadata = {};
|
|
120
|
+
for (const [key, value] of Object.entries(metadata ?? {}))
|
|
121
|
+
if (key !== "_rag")
|
|
122
|
+
userMetadata[key] = value;
|
|
123
|
+
return {
|
|
124
|
+
id: hit.id,
|
|
125
|
+
citationId,
|
|
126
|
+
sourceId,
|
|
127
|
+
index: rag.chunkIndex,
|
|
128
|
+
start: rag.start,
|
|
129
|
+
end: rag.end,
|
|
130
|
+
text: hit.text,
|
|
131
|
+
score: hit.score,
|
|
132
|
+
...(Object.keys(userMetadata).length ? { metadata: userMetadata } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=retrieve.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ContextProvider, JsonObject, Message, SecretRedactor } from "@arnilo/prism";
|
|
2
|
+
import type { Embedder, VectorStore } from "@arnilo/prism-memory";
|
|
3
|
+
export interface RagScope {
|
|
4
|
+
readonly tenantId: string;
|
|
5
|
+
readonly resourceId: string;
|
|
6
|
+
readonly corpusId: string;
|
|
7
|
+
}
|
|
8
|
+
export interface RagChunk {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly citationId: string;
|
|
11
|
+
readonly sourceId: string;
|
|
12
|
+
readonly index: number;
|
|
13
|
+
readonly start: number;
|
|
14
|
+
readonly end: number;
|
|
15
|
+
readonly text: string;
|
|
16
|
+
readonly metadata?: JsonObject;
|
|
17
|
+
}
|
|
18
|
+
export interface ChunkOptions {
|
|
19
|
+
readonly sourceId: string;
|
|
20
|
+
readonly metadata?: JsonObject;
|
|
21
|
+
readonly size?: number;
|
|
22
|
+
readonly overlap?: number;
|
|
23
|
+
readonly maxDocumentChars?: number;
|
|
24
|
+
readonly maxChunks?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface IndexChunksOptions {
|
|
27
|
+
readonly chunks: readonly RagChunk[];
|
|
28
|
+
readonly embedder: Embedder;
|
|
29
|
+
readonly store: VectorStore;
|
|
30
|
+
readonly scope: RagScope;
|
|
31
|
+
readonly batchSize?: number;
|
|
32
|
+
readonly maxChunks?: number;
|
|
33
|
+
readonly maxChunkChars?: number;
|
|
34
|
+
readonly maxVectorDimensions?: number;
|
|
35
|
+
readonly maxMetadataBytes?: number;
|
|
36
|
+
readonly redactor?: SecretRedactor;
|
|
37
|
+
readonly secrets?: readonly (string | undefined)[];
|
|
38
|
+
readonly signal?: AbortSignal;
|
|
39
|
+
}
|
|
40
|
+
export interface IndexChunksResult {
|
|
41
|
+
readonly indexed: number;
|
|
42
|
+
readonly sourceIds: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
export interface RagCitation {
|
|
45
|
+
readonly id: string;
|
|
46
|
+
readonly sourceId: string;
|
|
47
|
+
readonly chunkId: string;
|
|
48
|
+
readonly metadata?: JsonObject;
|
|
49
|
+
}
|
|
50
|
+
export interface RagHit extends RagChunk {
|
|
51
|
+
readonly score: number;
|
|
52
|
+
}
|
|
53
|
+
export interface RetrieveContextOptions {
|
|
54
|
+
readonly embedder: Embedder;
|
|
55
|
+
readonly store: VectorStore;
|
|
56
|
+
readonly scope: RagScope;
|
|
57
|
+
readonly topK?: number;
|
|
58
|
+
readonly queryCandidates?: number;
|
|
59
|
+
readonly filter?: JsonObject;
|
|
60
|
+
readonly maxResultBytes?: number;
|
|
61
|
+
readonly maxContextTokens?: number;
|
|
62
|
+
readonly maxMetadataBytes?: number;
|
|
63
|
+
readonly maxVectorDimensions?: number;
|
|
64
|
+
readonly redactor?: SecretRedactor;
|
|
65
|
+
readonly secrets?: readonly (string | undefined)[];
|
|
66
|
+
readonly signal?: AbortSignal;
|
|
67
|
+
}
|
|
68
|
+
export interface RagContextResult {
|
|
69
|
+
readonly query: string;
|
|
70
|
+
readonly text: string;
|
|
71
|
+
readonly hits: readonly RagHit[];
|
|
72
|
+
readonly citations: readonly RagCitation[];
|
|
73
|
+
readonly truncated: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface RagContextProviderOptions extends Omit<RetrieveContextOptions, "signal"> {
|
|
76
|
+
readonly name?: string;
|
|
77
|
+
readonly title?: string;
|
|
78
|
+
readonly query?: string | ((context: {
|
|
79
|
+
readonly messages: readonly Message[];
|
|
80
|
+
}) => string | undefined);
|
|
81
|
+
}
|
|
82
|
+
export type RagContextProvider = ContextProvider;
|
package/dist/types.js
ADDED
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type JsonObject, type JsonValue, type Message, type SecretRedactor } from "@arnilo/prism";
|
|
2
|
+
import type { RagScope } from "./types.js";
|
|
3
|
+
export declare function assertNotAborted(signal?: AbortSignal): void;
|
|
4
|
+
export declare function nonEmpty(value: unknown, label: string): string;
|
|
5
|
+
export declare function requireSourceId(value: unknown): string;
|
|
6
|
+
export declare function requireScope(scope: RagScope): RagScope;
|
|
7
|
+
export declare function assertScope(expected: RagScope, actual: {
|
|
8
|
+
tenantId: string;
|
|
9
|
+
resourceId: string;
|
|
10
|
+
threadId: string;
|
|
11
|
+
}): void;
|
|
12
|
+
export declare function resolveRedactor(redactor?: SecretRedactor, secrets?: readonly (string | undefined)[]): SecretRedactor | undefined;
|
|
13
|
+
export declare function byteLength(value: unknown): number;
|
|
14
|
+
export declare function assertBytes(value: unknown, limit: number, label: string): void;
|
|
15
|
+
export declare function latestUserText(messages: readonly Message[]): string | undefined;
|
|
16
|
+
export declare function jsonEqual(left: JsonValue | undefined, right: JsonValue | undefined): boolean;
|
|
17
|
+
export declare function matchesFilter(metadata: JsonObject | undefined, filter: JsonObject | undefined): boolean;
|
|
18
|
+
export declare function isJsonObject(value: unknown): value is JsonObject;
|
|
19
|
+
export declare function truncateUtf8(text: string, maxBytes: number): string;
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createSecretRedactor } from "@arnilo/prism";
|
|
2
|
+
import { RagAbortError, RagLimitError, RagScopeError, RagValidationError } from "./errors.js";
|
|
3
|
+
export function assertNotAborted(signal) {
|
|
4
|
+
if (signal?.aborted)
|
|
5
|
+
throw new RagAbortError();
|
|
6
|
+
}
|
|
7
|
+
export function nonEmpty(value, label) {
|
|
8
|
+
if (typeof value !== "string" || !value.trim())
|
|
9
|
+
throw new RagValidationError(`${label} must be a non-empty string`);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
export function requireSourceId(value) {
|
|
13
|
+
const sourceId = nonEmpty(value, "sourceId");
|
|
14
|
+
if (sourceId.length > 256 || !/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/.test(sourceId)) {
|
|
15
|
+
throw new RagValidationError("sourceId must be <= 256 URL-safe identifier characters");
|
|
16
|
+
}
|
|
17
|
+
return sourceId;
|
|
18
|
+
}
|
|
19
|
+
export function requireScope(scope) {
|
|
20
|
+
return {
|
|
21
|
+
tenantId: nonEmpty(scope.tenantId, "tenantId"),
|
|
22
|
+
resourceId: nonEmpty(scope.resourceId, "resourceId"),
|
|
23
|
+
corpusId: nonEmpty(scope.corpusId, "corpusId"),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function assertScope(expected, actual) {
|
|
27
|
+
if (actual.tenantId !== expected.tenantId
|
|
28
|
+
|| actual.resourceId !== expected.resourceId
|
|
29
|
+
|| actual.threadId !== expected.corpusId) {
|
|
30
|
+
throw new RagScopeError("vector hit crossed tenant/resource/corpus boundary");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function resolveRedactor(redactor, secrets) {
|
|
34
|
+
return redactor ?? (secrets?.length ? createSecretRedactor(secrets) : undefined);
|
|
35
|
+
}
|
|
36
|
+
export function byteLength(value) {
|
|
37
|
+
return Buffer.byteLength(typeof value === "string" ? value : JSON.stringify(value), "utf8");
|
|
38
|
+
}
|
|
39
|
+
export function assertBytes(value, limit, label) {
|
|
40
|
+
const bytes = byteLength(value);
|
|
41
|
+
if (bytes > limit)
|
|
42
|
+
throw new RagLimitError(`${label} exceeds ${limit} bytes (${bytes})`);
|
|
43
|
+
}
|
|
44
|
+
export function latestUserText(messages) {
|
|
45
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
46
|
+
const message = messages[index];
|
|
47
|
+
if (message.role !== "user")
|
|
48
|
+
continue;
|
|
49
|
+
const text = message.content
|
|
50
|
+
.filter((block) => block.type === "text")
|
|
51
|
+
.map((block) => block.text)
|
|
52
|
+
.join("\n");
|
|
53
|
+
if (text.trim())
|
|
54
|
+
return text;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
export function jsonEqual(left, right) {
|
|
59
|
+
if (left === right)
|
|
60
|
+
return true;
|
|
61
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
62
|
+
return left.length === right.length && left.every((item, index) => jsonEqual(item, right[index]));
|
|
63
|
+
}
|
|
64
|
+
if (isJsonObject(left) && isJsonObject(right)) {
|
|
65
|
+
const keys = Object.keys(left);
|
|
66
|
+
return keys.length === Object.keys(right).length && keys.every((key) => jsonEqual(left[key], right[key]));
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
export function matchesFilter(metadata, filter) {
|
|
71
|
+
if (!filter)
|
|
72
|
+
return true;
|
|
73
|
+
if (!metadata)
|
|
74
|
+
return false;
|
|
75
|
+
return Object.entries(filter).every(([key, value]) => jsonEqual(metadata[key], value));
|
|
76
|
+
}
|
|
77
|
+
export function isJsonObject(value) {
|
|
78
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
79
|
+
}
|
|
80
|
+
export function truncateUtf8(text, maxBytes) {
|
|
81
|
+
if (byteLength(text) <= maxBytes)
|
|
82
|
+
return text;
|
|
83
|
+
let output = "";
|
|
84
|
+
let bytes = 0;
|
|
85
|
+
for (const character of text) {
|
|
86
|
+
const size = byteLength(character);
|
|
87
|
+
if (bytes + size > maxBytes)
|
|
88
|
+
break;
|
|
89
|
+
output += character;
|
|
90
|
+
bytes += size;
|
|
91
|
+
}
|
|
92
|
+
return output;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=util.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arnilo/prism-rag",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "Optional bounded text and Markdown RAG primitives for Prism.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"!dist/__tests__",
|
|
17
|
+
"!dist/**/*.map",
|
|
18
|
+
"README.md",
|
|
19
|
+
"CHANGELOG.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
24
|
+
"test": "node --test dist/__tests__/rag.test.js",
|
|
25
|
+
"pack:dry-run": "npm pack --dry-run"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@arnilo/prism": "0.0.5",
|
|
29
|
+
"@arnilo/prism-memory": "0.0.5"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@arnilo/prism": "file:../..",
|
|
33
|
+
"@arnilo/prism-memory": "file:../memory"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20"
|
|
37
|
+
},
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/ashiqrniloy/prism.git",
|
|
42
|
+
"directory": "packages/rag"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/ashiqrniloy/prism/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/ashiqrniloy/prism/tree/main/packages/rag#readme",
|
|
48
|
+
"keywords": [
|
|
49
|
+
"prism",
|
|
50
|
+
"rag",
|
|
51
|
+
"retrieval",
|
|
52
|
+
"chunking",
|
|
53
|
+
"citations",
|
|
54
|
+
"vector-store"
|
|
55
|
+
],
|
|
56
|
+
"sideEffects": false,
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|