@arnilo/prism-rag 0.0.14 → 0.0.15
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 +8 -0
- package/README.md +11 -5
- package/dist/context.js +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.js +5 -1
- package/dist/indexing.d.ts +2 -0
- package/dist/indexing.js +68 -57
- package/dist/ingestion-status.d.ts +7 -0
- package/dist/ingestion-status.js +85 -0
- package/dist/limits.d.ts +21 -0
- package/dist/limits.js +21 -0
- package/dist/loaders.d.ts +24 -0
- package/dist/loaders.js +78 -0
- package/dist/parsers.d.ts +5 -0
- package/dist/parsers.js +99 -0
- package/dist/rerank.d.ts +11 -0
- package/dist/rerank.js +59 -0
- package/dist/retrieve.js +50 -34
- package/dist/sources.d.ts +9 -0
- package/dist/sources.js +113 -0
- package/dist/types.d.ts +128 -1
- package/package.json +11 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.0.15] - 2026-07-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added atomic scoped `replaceSource()` / `deleteSource()` and `replaceDocument()` over source-aware transactional vector storage; the in-memory reference store implements `getBySource()` plus transactions.
|
|
8
|
+
- Added bounded `DocumentLoader` / `Parser` seams, reference text/Markdown/HTML/PDF parsers, and host resource/web-tools document loaders with citation/trust preservation.
|
|
9
|
+
- Added host-owned bounded reranking, canonical citation provenance/content-trust metadata, and scoped capped ingestion-status storage/querying with a process-local reference adapter.
|
|
10
|
+
|
|
3
11
|
## [0.0.14] - 2026-07-26
|
|
4
12
|
|
|
5
13
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @arnilo/prism-rag
|
|
2
2
|
|
|
3
|
-
Optional bounded
|
|
3
|
+
Optional bounded source lifecycle, document parsing, host reranking, ingestion status, and retrieval-augmented generation primitives for Prism. Reuses `Embedder` and `VectorStore` from `@arnilo/prism-memory`; no document framework, network loader, or core activation.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ npm install @arnilo/prism-rag @arnilo/prism-memory @arnilo/prism
|
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
14
|
import { createHashEmbedder, createMemoryVectorStore } from "@arnilo/prism-memory";
|
|
15
|
-
import { chunkMarkdown, createRagContextProvider,
|
|
15
|
+
import { chunkMarkdown, createMemoryIngestionStatusStore, createRagContextProvider, listIngestionStatus, replaceSource } from "@arnilo/prism-rag";
|
|
16
16
|
|
|
17
17
|
const embedder = createHashEmbedder(); // demo/test only
|
|
18
18
|
const store = createMemoryVectorStore();
|
|
@@ -20,7 +20,9 @@ const scope = { tenantId: "t1", resourceId: "docs", corpusId: "handbook" };
|
|
|
20
20
|
const chunks = chunkMarkdown("# Approval\n\nRecheck policy before side effects.", {
|
|
21
21
|
sourceId: "security-guide",
|
|
22
22
|
});
|
|
23
|
-
|
|
23
|
+
const statusStore = createMemoryIngestionStatusStore();
|
|
24
|
+
await replaceSource({ sourceId: "security-guide", chunks, embedder, store, scope, statusStore }); // atomic with the reference store
|
|
25
|
+
console.log(await listIngestionStatus({ store: statusStore, scope }));
|
|
24
26
|
const context = createRagContextProvider({ embedder, store, scope, topK: 4 });
|
|
25
27
|
```
|
|
26
28
|
|
|
@@ -28,11 +30,15 @@ const context = createRagContextProvider({ embedder, store, scope, topK: 4 });
|
|
|
28
30
|
|
|
29
31
|
- `chunkText()` / `chunkMarkdown()` — deterministic boundary-aware character chunks with overlap and stable citations.
|
|
30
32
|
- `indexChunks()` — bounded batch embedding and scoped vector upsert.
|
|
31
|
-
- `
|
|
33
|
+
- `replaceSource()` / `deleteSource()` — exact-scope source lifecycle; replacement requires transactional `getBySource()` storage.
|
|
34
|
+
- `replaceDocument()` + `DocumentLoader` / `Parser` — host-authorized load, bounded parse, chunk, and replacement.
|
|
35
|
+
- `textParser` / `markdownParser` / `htmlParser` / `pdfParser` — focused reference parsers (`./parsers`); `createResourceDocumentLoader` / `createWebFetchDocumentLoader` are in `./loaders`.
|
|
36
|
+
- `retrieveContext()` — bounded candidate query, optional host `Reranker`, shallow metadata filter, top-K hits, attributable citations, and untrusted/inert/injection-capable trust metadata.
|
|
37
|
+
- `createMemoryIngestionStatusStore()` / `listIngestionStatus()` — optional capped exact-scope pending/indexed/failed/partial source progress; implement `IngestionStatusStore` for durable status.
|
|
32
38
|
- `createRagContextProvider()` — explicit inert context injection through Prism's existing seam.
|
|
33
39
|
|
|
34
40
|
## Security
|
|
35
41
|
|
|
36
|
-
Every operation requires tenant/resource/corpus scope. Configure `redactor` or `secrets` before external embedding/persistence. Package performs no I/O
|
|
42
|
+
Every operation requires tenant/resource/corpus scope. Configure `redactor` or `secrets` before external embedding/persistence. Package performs no I/O: resource loading delegates to a host `ResourceLoader`, and web loading delegates to an existing web-tools adapter. Replacement fails closed without a scoped transaction; rerankers get redacted finite candidates and cannot alter canonical provenance/trust; HTML/PDF/web output is untrusted inert context and grants no tools or permissions.
|
|
37
43
|
|
|
38
44
|
See [RAG](../../docs/rag.md).
|
package/dist/context.js
CHANGED
|
@@ -17,7 +17,7 @@ export function createRagContextProvider(options) {
|
|
|
17
17
|
id: `${options.name ?? "rag"}:context`,
|
|
18
18
|
title: options.title ?? "Retrieved context",
|
|
19
19
|
content: result.text,
|
|
20
|
-
metadata: { citations: result.citations, inert: true },
|
|
20
|
+
metadata: { citations: result.citations, trust: result.trust, inert: true, untrusted: true, injectionCapable: true },
|
|
21
21
|
}];
|
|
22
22
|
},
|
|
23
23
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
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";
|
|
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_DOCUMENT_BYTES, HARD_MAX_DOCUMENT_BYTES_CAP, DEFAULT_MAX_PARSE_MS, HARD_MAX_PARSE_MS_CAP, DEFAULT_MAX_PDF_PAGES, HARD_MAX_PDF_PAGES_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, DEFAULT_MAX_RERANK_BYTES, HARD_MAX_RERANK_BYTES_CAP, DEFAULT_MAX_RERANK_MS, HARD_MAX_RERANK_MS_CAP, DEFAULT_RERANK_CONCURRENCY, HARD_RERANK_CONCURRENCY_CAP, DEFAULT_INGESTION_STATUS_PAGE_SIZE, HARD_INGESTION_STATUS_PAGE_SIZE_CAP, resolveRagLimits, } from "./limits.js";
|
|
2
2
|
export type { RagLimits, RagLimitsInput } from "./limits.js";
|
|
3
3
|
export { RagAbortError, RagError, RagLimitError, RagScopeError, RagValidationError } from "./errors.js";
|
|
4
4
|
export { chunkMarkdown, chunkText } from "./chunk.js";
|
|
5
|
+
export { createResourceDocumentLoader, createWebFetchDocumentLoader } from "./loaders.js";
|
|
6
|
+
export { htmlParser, markdownParser, pdfParser, textParser } from "./parsers.js";
|
|
5
7
|
export { indexChunks } from "./indexing.js";
|
|
8
|
+
export { deleteSource, replaceDocument, replaceSource } from "./sources.js";
|
|
9
|
+
export type { SourceMutationResult } from "./sources.js";
|
|
10
|
+
export { createMemoryIngestionStatusStore, listIngestionStatus } from "./ingestion-status.js";
|
|
6
11
|
export { retrieveContext } from "./retrieve.js";
|
|
7
12
|
export { createRagContextProvider } from "./context.js";
|
|
8
|
-
export type { ChunkOptions, IndexChunksOptions, IndexChunksResult, RagChunk, RagCitation, RagContextProvider, RagContextProviderOptions, RagContextResult, RagHit, RagScope, RetrieveContextOptions, } from "./types.js";
|
|
13
|
+
export type { ChunkOptions, Chunker, DeleteSourceOptions, DocumentLoader, DocumentLoadOptions, DocumentParseOptions, IndexChunksOptions, IndexChunksResult, IngestionState, IngestionStatus, IngestionStatusQuery, IngestionStatusStore, RagChunk, LoadedDocument, ParsedDocument, Parser, RagCitation, RagContextProvider, RagContextProviderOptions, RagContextResult, RagContentTrust, RagHit, RagProvenance, Reranker, RagScope, ReplaceDocumentOptions, ReplaceSourceOptions, RetrieveContextOptions, SourceVectorStore, TransactionalVectorStore, } from "./types.js";
|
|
9
14
|
export declare const packageName = "@arnilo/prism-rag";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
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";
|
|
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_DOCUMENT_BYTES, HARD_MAX_DOCUMENT_BYTES_CAP, DEFAULT_MAX_PARSE_MS, HARD_MAX_PARSE_MS_CAP, DEFAULT_MAX_PDF_PAGES, HARD_MAX_PDF_PAGES_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, DEFAULT_MAX_RERANK_BYTES, HARD_MAX_RERANK_BYTES_CAP, DEFAULT_MAX_RERANK_MS, HARD_MAX_RERANK_MS_CAP, DEFAULT_RERANK_CONCURRENCY, HARD_RERANK_CONCURRENCY_CAP, DEFAULT_INGESTION_STATUS_PAGE_SIZE, HARD_INGESTION_STATUS_PAGE_SIZE_CAP, resolveRagLimits, } from "./limits.js";
|
|
2
2
|
export { RagAbortError, RagError, RagLimitError, RagScopeError, RagValidationError } from "./errors.js";
|
|
3
3
|
export { chunkMarkdown, chunkText } from "./chunk.js";
|
|
4
|
+
export { createResourceDocumentLoader, createWebFetchDocumentLoader } from "./loaders.js";
|
|
5
|
+
export { htmlParser, markdownParser, pdfParser, textParser } from "./parsers.js";
|
|
4
6
|
export { indexChunks } from "./indexing.js";
|
|
7
|
+
export { deleteSource, replaceDocument, replaceSource } from "./sources.js";
|
|
8
|
+
export { createMemoryIngestionStatusStore, listIngestionStatus } from "./ingestion-status.js";
|
|
5
9
|
export { retrieveContext } from "./retrieve.js";
|
|
6
10
|
export { createRagContextProvider } from "./context.js";
|
|
7
11
|
export const packageName = "@arnilo/prism-rag";
|
package/dist/indexing.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
import type { MemoryVectorRecord } from "@arnilo/prism-memory";
|
|
1
2
|
import type { IndexChunksOptions, IndexChunksResult } from "./types.js";
|
|
2
3
|
export declare function indexChunks(options: IndexChunksOptions): Promise<IndexChunksResult>;
|
|
4
|
+
export declare function indexChunkBatches(options: IndexChunksOptions, write: (records: readonly MemoryVectorRecord[]) => Promise<void>): Promise<IndexChunksResult>;
|
package/dist/indexing.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { HARD_CHUNK_SIZE_CAP, resolveRagLimits } from "./limits.js";
|
|
2
1
|
import { RagValidationError } from "./errors.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { ingestionStatus } from "./ingestion-status.js";
|
|
3
|
+
import { HARD_CHUNK_SIZE_CAP, resolveRagLimits } from "./limits.js";
|
|
4
|
+
import { assertBytes, assertNotAborted, byteLength, nonEmpty, requireScope, requireSourceId, resolveRedactor } from "./util.js";
|
|
6
5
|
export async function indexChunks(options) {
|
|
6
|
+
return indexChunkBatches(options, async (records) => options.store.upsert(records, { signal: options.signal }));
|
|
7
|
+
}
|
|
8
|
+
export async function indexChunkBatches(options, write) {
|
|
7
9
|
const scope = requireScope(options.scope);
|
|
8
10
|
const limits = resolveRagLimits({
|
|
9
11
|
embedBatchSize: options.batchSize,
|
|
@@ -12,78 +14,87 @@ export async function indexChunks(options) {
|
|
|
12
14
|
maxVectorDimensions: options.maxVectorDimensions,
|
|
13
15
|
maxMetadataBytes: options.maxMetadataBytes,
|
|
14
16
|
});
|
|
15
|
-
if (!Number.isInteger(options.embedder.dimensions)
|
|
16
|
-
|| options.embedder.dimensions <= 0
|
|
17
|
-
|| options.embedder.dimensions > limits.maxVectorDimensions) {
|
|
17
|
+
if (!Number.isInteger(options.embedder.dimensions) || options.embedder.dimensions <= 0 || options.embedder.dimensions > limits.maxVectorDimensions) {
|
|
18
18
|
throw new RagValidationError(`embedder dimensions must be an integer in 1..${limits.maxVectorDimensions}`);
|
|
19
19
|
}
|
|
20
|
-
if (options.chunks.length > limits.maxChunks)
|
|
20
|
+
if (options.chunks.length > limits.maxChunks)
|
|
21
21
|
throw new RagValidationError(`chunk count exceeds ${limits.maxChunks}`);
|
|
22
|
-
}
|
|
23
22
|
const redactor = resolveRedactor(options.redactor, options.secrets);
|
|
24
23
|
const sourceIds = new Set();
|
|
25
24
|
const chunkIds = new Set();
|
|
25
|
+
const total = new Map();
|
|
26
|
+
const written = new Map();
|
|
26
27
|
for (const chunk of options.chunks) {
|
|
27
28
|
nonEmpty(chunk.id, "chunk.id");
|
|
28
29
|
requireSourceId(chunk.sourceId);
|
|
29
|
-
if (chunk.id !== chunk.citationId || !chunk.citationId.startsWith(`${chunk.sourceId}#`))
|
|
30
|
+
if (chunk.id !== chunk.citationId || !chunk.citationId.startsWith(`${chunk.sourceId}#`))
|
|
30
31
|
throw new RagValidationError("chunk has inconsistent citation identity");
|
|
31
|
-
}
|
|
32
32
|
if (chunkIds.has(chunk.id))
|
|
33
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) {
|
|
34
|
+
if (!Number.isInteger(chunk.index) || chunk.index < 0 || !Number.isInteger(chunk.start) || chunk.start < 0 || !Number.isInteger(chunk.end) || chunk.end < chunk.start) {
|
|
40
35
|
throw new RagValidationError("chunk has invalid index or offsets");
|
|
41
36
|
}
|
|
42
37
|
if (chunk.text.length > limits.chunkSize)
|
|
43
38
|
throw new RagValidationError(`chunk text exceeds ${limits.chunkSize} characters`);
|
|
44
39
|
chunkIds.add(chunk.id);
|
|
40
|
+
sourceIds.add(chunk.sourceId);
|
|
41
|
+
add(total, chunk.sourceId, byteLength(redactor?.redact(chunk.text) ?? chunk.text), 1);
|
|
45
42
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
43
|
+
const setStatus = async (state, error) => {
|
|
44
|
+
if (!options.statusStore)
|
|
45
|
+
return;
|
|
46
|
+
const message = error instanceof Error ? error.message : error === undefined ? undefined : "indexing failed";
|
|
47
|
+
const safeError = message ? redactor?.redact(message) ?? message : undefined;
|
|
48
|
+
for (const sourceId of sourceIds) {
|
|
49
|
+
const progress = state === "indexed" ? total.get(sourceId) : written.get(sourceId) ?? { bytes: 0, chunks: 0 };
|
|
50
|
+
await options.statusStore.set(ingestionStatus(scope, sourceId, state, progress.bytes, progress.chunks, safeError));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
await setStatus("pending");
|
|
54
|
+
try {
|
|
55
|
+
for (let offset = 0; offset < options.chunks.length; offset += limits.embedBatchSize) {
|
|
56
|
+
assertNotAborted(options.signal);
|
|
57
|
+
const batch = options.chunks.slice(offset, offset + limits.embedBatchSize);
|
|
58
|
+
const texts = batch.map((chunk) => redactor?.redact(chunk.text) ?? chunk.text);
|
|
59
|
+
const vectors = await options.embedder.embed(texts, { signal: options.signal });
|
|
60
|
+
if (vectors.length !== batch.length)
|
|
61
|
+
throw new RagValidationError("embedder returned unexpected vector count");
|
|
62
|
+
const records = batch.map((chunk, index) => {
|
|
63
|
+
const embedding = vectors[index];
|
|
64
|
+
if (embedding.length !== options.embedder.dimensions || embedding.some((value) => !Number.isFinite(value))) {
|
|
65
|
+
throw new RagValidationError(`embedder returned invalid vector; expected ${options.embedder.dimensions} finite values`);
|
|
66
|
+
}
|
|
67
|
+
const safeMetadata = redactor?.redact(chunk.metadata ?? {}) ?? (chunk.metadata ?? {});
|
|
68
|
+
const metadata = { ...safeMetadata, _rag: { sourceId: chunk.sourceId, citationId: chunk.citationId, chunkIndex: chunk.index, start: chunk.start, end: chunk.end } };
|
|
69
|
+
assertBytes(metadata, limits.maxMetadataBytes, "chunk metadata");
|
|
70
|
+
return {
|
|
71
|
+
id: chunk.id,
|
|
72
|
+
tenantId: scope.tenantId,
|
|
73
|
+
resourceId: scope.resourceId,
|
|
74
|
+
threadId: scope.corpusId,
|
|
75
|
+
text: texts[index],
|
|
76
|
+
embedding,
|
|
77
|
+
sequence: chunk.index,
|
|
78
|
+
metadata: metadata,
|
|
79
|
+
createdAt: new Date(0).toISOString(),
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
assertNotAborted(options.signal);
|
|
83
|
+
await write(records);
|
|
84
|
+
for (const chunk of batch)
|
|
85
|
+
add(written, chunk.sourceId, byteLength(redactor?.redact(chunk.text) ?? chunk.text), 1);
|
|
86
|
+
await setStatus("pending");
|
|
87
|
+
}
|
|
88
|
+
await setStatus("indexed");
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
await setStatus([...written.values()].some((progress) => progress.chunks > 0) ? "partial" : "failed", error);
|
|
92
|
+
throw error;
|
|
86
93
|
}
|
|
87
94
|
return Object.freeze({ indexed: options.chunks.length, sourceIds: Object.freeze([...sourceIds].sort()) });
|
|
88
95
|
}
|
|
96
|
+
function add(target, sourceId, bytes, chunks) {
|
|
97
|
+
const current = target.get(sourceId) ?? { bytes: 0, chunks: 0 };
|
|
98
|
+
target.set(sourceId, { bytes: current.bytes + bytes, chunks: current.chunks + chunks });
|
|
99
|
+
}
|
|
89
100
|
//# sourceMappingURL=indexing.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { IngestionStatus, IngestionStatusQuery, IngestionStatusStore, RagScope } from "./types.js";
|
|
2
|
+
export declare function listIngestionStatus(options: IngestionStatusQuery): Promise<{
|
|
3
|
+
readonly entries: readonly IngestionStatus[];
|
|
4
|
+
readonly nextCursor?: string;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function createMemoryIngestionStatusStore(): IngestionStatusStore;
|
|
7
|
+
export declare function ingestionStatus(scope: RagScope, sourceId: string, state: IngestionStatus["state"], bytes: number, chunks: number, error?: string): IngestionStatus;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { RagScopeError, RagValidationError } from "./errors.js";
|
|
2
|
+
import { resolveRagLimits } from "./limits.js";
|
|
3
|
+
import { assertNotAborted, byteLength, requireScope, requireSourceId } from "./util.js";
|
|
4
|
+
export async function listIngestionStatus(options) {
|
|
5
|
+
const scope = requireScope(options.scope);
|
|
6
|
+
const limit = resolveRagLimits({ ingestionStatusPageSize: options.limit }).ingestionStatusPageSize;
|
|
7
|
+
if (options.cursor !== undefined)
|
|
8
|
+
requireSourceId(options.cursor);
|
|
9
|
+
assertNotAborted(options.signal);
|
|
10
|
+
const page = await options.store.list(scope, { limit, cursor: options.cursor, signal: options.signal });
|
|
11
|
+
if (page.entries.length > limit)
|
|
12
|
+
throw new RagValidationError("ingestion status store exceeded requested page limit");
|
|
13
|
+
for (const entry of page.entries)
|
|
14
|
+
assertStatus(entry, scope);
|
|
15
|
+
if (page.nextCursor !== undefined)
|
|
16
|
+
requireSourceId(page.nextCursor);
|
|
17
|
+
return Object.freeze({ entries: Object.freeze([...page.entries]), ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}) });
|
|
18
|
+
}
|
|
19
|
+
export function createMemoryIngestionStatusStore() {
|
|
20
|
+
const scopes = new Map();
|
|
21
|
+
return {
|
|
22
|
+
async set(status, options = {}) {
|
|
23
|
+
assertNotAborted(options.signal);
|
|
24
|
+
const scope = requireScope(status.scope);
|
|
25
|
+
assertStatus(status, scope);
|
|
26
|
+
const key = scopeKey(scope);
|
|
27
|
+
const entries = scopes.get(key) ?? new Map();
|
|
28
|
+
entries.set(status.sourceId, Object.freeze({ ...status, scope }));
|
|
29
|
+
scopes.set(key, entries);
|
|
30
|
+
},
|
|
31
|
+
async delete(scope, sourceId, options = {}) {
|
|
32
|
+
assertNotAborted(options.signal);
|
|
33
|
+
const entries = scopes.get(scopeKey(requireScope(scope)));
|
|
34
|
+
entries?.delete(requireSourceId(sourceId));
|
|
35
|
+
},
|
|
36
|
+
async list(scope, options) {
|
|
37
|
+
assertNotAborted(options.signal);
|
|
38
|
+
const required = requireScope(scope);
|
|
39
|
+
const limit = options.limit;
|
|
40
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
41
|
+
throw new RagValidationError("ingestion status limit must be a positive integer");
|
|
42
|
+
if (options.cursor !== undefined)
|
|
43
|
+
requireSourceId(options.cursor);
|
|
44
|
+
const entries = [...(scopes.get(scopeKey(required))?.values() ?? [])]
|
|
45
|
+
.filter((entry) => entry.sourceId > (options.cursor ?? ""))
|
|
46
|
+
.sort((left, right) => left.sourceId.localeCompare(right.sourceId));
|
|
47
|
+
const page = entries.slice(0, limit);
|
|
48
|
+
const next = entries.length > page.length ? page.at(-1)?.sourceId : undefined;
|
|
49
|
+
return Object.freeze({ entries: Object.freeze(page), ...(next ? { nextCursor: next } : {}) });
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function ingestionStatus(scope, sourceId, state, bytes, chunks, error) {
|
|
54
|
+
const status = {
|
|
55
|
+
sourceId: requireSourceId(sourceId),
|
|
56
|
+
scope: requireScope(scope),
|
|
57
|
+
state,
|
|
58
|
+
bytes,
|
|
59
|
+
chunks,
|
|
60
|
+
...(error ? { error } : {}),
|
|
61
|
+
updatedAt: new Date().toISOString(),
|
|
62
|
+
};
|
|
63
|
+
assertStatus(status, status.scope);
|
|
64
|
+
return Object.freeze(status);
|
|
65
|
+
}
|
|
66
|
+
function assertStatus(status, scope) {
|
|
67
|
+
requireSourceId(status.sourceId);
|
|
68
|
+
if (status.scope.tenantId !== scope.tenantId || status.scope.resourceId !== scope.resourceId || status.scope.corpusId !== scope.corpusId) {
|
|
69
|
+
throw new RagScopeError("ingestion status crossed tenant/resource/corpus boundary");
|
|
70
|
+
}
|
|
71
|
+
if (!["pending", "indexed", "failed", "partial"].includes(status.state))
|
|
72
|
+
throw new RagValidationError("ingestion status state is invalid");
|
|
73
|
+
if (!Number.isSafeInteger(status.bytes) || status.bytes < 0 || !Number.isSafeInteger(status.chunks) || status.chunks < 0) {
|
|
74
|
+
throw new RagValidationError("ingestion status bytes and chunks must be non-negative safe integers");
|
|
75
|
+
}
|
|
76
|
+
if (!Number.isFinite(Date.parse(status.updatedAt)))
|
|
77
|
+
throw new RagValidationError("ingestion status updatedAt must be an ISO timestamp");
|
|
78
|
+
if (status.error !== undefined && (typeof status.error !== "string" || byteLength(status.error) > 4_096)) {
|
|
79
|
+
throw new RagValidationError("ingestion status error must be a string <= 4096 bytes");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function scopeKey(scope) {
|
|
83
|
+
return `${scope.tenantId}\0${scope.resourceId}\0${scope.corpusId}`;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=ingestion-status.js.map
|
package/dist/limits.d.ts
CHANGED
|
@@ -4,6 +4,12 @@ export declare const DEFAULT_CHUNK_OVERLAP = 100;
|
|
|
4
4
|
export declare const HARD_CHUNK_OVERLAP_CAP = 4096;
|
|
5
5
|
export declare const DEFAULT_MAX_DOCUMENT_CHARS = 1048576;
|
|
6
6
|
export declare const HARD_MAX_DOCUMENT_CHARS_CAP = 8388608;
|
|
7
|
+
export declare const DEFAULT_MAX_DOCUMENT_BYTES = 1048576;
|
|
8
|
+
export declare const HARD_MAX_DOCUMENT_BYTES_CAP: number;
|
|
9
|
+
export declare const DEFAULT_MAX_PARSE_MS = 30000;
|
|
10
|
+
export declare const HARD_MAX_PARSE_MS_CAP = 30000;
|
|
11
|
+
export declare const DEFAULT_MAX_PDF_PAGES = 256;
|
|
12
|
+
export declare const HARD_MAX_PDF_PAGES_CAP = 256;
|
|
7
13
|
export declare const DEFAULT_MAX_CHUNKS = 2048;
|
|
8
14
|
export declare const HARD_MAX_CHUNKS_CAP = 8192;
|
|
9
15
|
export declare const DEFAULT_EMBED_BATCH_SIZE = 32;
|
|
@@ -19,10 +25,21 @@ export declare const HARD_MAX_CONTEXT_TOKENS_CAP = 8000;
|
|
|
19
25
|
export declare const DEFAULT_MAX_METADATA_BYTES: number;
|
|
20
26
|
export declare const HARD_MAX_METADATA_BYTES_CAP: number;
|
|
21
27
|
export declare const DEFAULT_MAX_VECTOR_DIMENSIONS = 4096;
|
|
28
|
+
export declare const DEFAULT_MAX_RERANK_BYTES: number;
|
|
29
|
+
export declare const HARD_MAX_RERANK_BYTES_CAP: number;
|
|
30
|
+
export declare const DEFAULT_MAX_RERANK_MS = 2000;
|
|
31
|
+
export declare const HARD_MAX_RERANK_MS_CAP = 10000;
|
|
32
|
+
export declare const DEFAULT_RERANK_CONCURRENCY = 2;
|
|
33
|
+
export declare const HARD_RERANK_CONCURRENCY_CAP = 8;
|
|
34
|
+
export declare const DEFAULT_INGESTION_STATUS_PAGE_SIZE = 50;
|
|
35
|
+
export declare const HARD_INGESTION_STATUS_PAGE_SIZE_CAP = 200;
|
|
22
36
|
export interface RagLimits {
|
|
23
37
|
readonly chunkSize: number;
|
|
24
38
|
readonly chunkOverlap: number;
|
|
25
39
|
readonly maxDocumentChars: number;
|
|
40
|
+
readonly maxDocumentBytes: number;
|
|
41
|
+
readonly maxParseMs: number;
|
|
42
|
+
readonly maxPdfPages: number;
|
|
26
43
|
readonly maxChunks: number;
|
|
27
44
|
readonly embedBatchSize: number;
|
|
28
45
|
readonly topK: number;
|
|
@@ -31,6 +48,10 @@ export interface RagLimits {
|
|
|
31
48
|
readonly maxContextTokens: number;
|
|
32
49
|
readonly maxMetadataBytes: number;
|
|
33
50
|
readonly maxVectorDimensions: number;
|
|
51
|
+
readonly maxRerankBytes: number;
|
|
52
|
+
readonly maxRerankMs: number;
|
|
53
|
+
readonly rerankConcurrency: number;
|
|
54
|
+
readonly ingestionStatusPageSize: number;
|
|
34
55
|
}
|
|
35
56
|
export type RagLimitsInput = Partial<RagLimits>;
|
|
36
57
|
export declare function resolveRagLimits(input?: RagLimitsInput): RagLimits;
|
package/dist/limits.js
CHANGED
|
@@ -5,6 +5,12 @@ export const DEFAULT_CHUNK_OVERLAP = 100;
|
|
|
5
5
|
export const HARD_CHUNK_OVERLAP_CAP = 4_096;
|
|
6
6
|
export const DEFAULT_MAX_DOCUMENT_CHARS = 1_048_576;
|
|
7
7
|
export const HARD_MAX_DOCUMENT_CHARS_CAP = 8_388_608;
|
|
8
|
+
export const DEFAULT_MAX_DOCUMENT_BYTES = 1_048_576;
|
|
9
|
+
export const HARD_MAX_DOCUMENT_BYTES_CAP = 8 * 1024 * 1024;
|
|
10
|
+
export const DEFAULT_MAX_PARSE_MS = 30_000;
|
|
11
|
+
export const HARD_MAX_PARSE_MS_CAP = 30_000;
|
|
12
|
+
export const DEFAULT_MAX_PDF_PAGES = 256;
|
|
13
|
+
export const HARD_MAX_PDF_PAGES_CAP = 256;
|
|
8
14
|
export const DEFAULT_MAX_CHUNKS = 2_048;
|
|
9
15
|
export const HARD_MAX_CHUNKS_CAP = 8_192;
|
|
10
16
|
export const DEFAULT_EMBED_BATCH_SIZE = 32;
|
|
@@ -20,6 +26,14 @@ export const HARD_MAX_CONTEXT_TOKENS_CAP = 8_000;
|
|
|
20
26
|
export const DEFAULT_MAX_METADATA_BYTES = 16 * 1024;
|
|
21
27
|
export const HARD_MAX_METADATA_BYTES_CAP = 64 * 1024;
|
|
22
28
|
export const DEFAULT_MAX_VECTOR_DIMENSIONS = 4_096;
|
|
29
|
+
export const DEFAULT_MAX_RERANK_BYTES = 64 * 1024;
|
|
30
|
+
export const HARD_MAX_RERANK_BYTES_CAP = 256 * 1024;
|
|
31
|
+
export const DEFAULT_MAX_RERANK_MS = 2_000;
|
|
32
|
+
export const HARD_MAX_RERANK_MS_CAP = 10_000;
|
|
33
|
+
export const DEFAULT_RERANK_CONCURRENCY = 2;
|
|
34
|
+
export const HARD_RERANK_CONCURRENCY_CAP = 8;
|
|
35
|
+
export const DEFAULT_INGESTION_STATUS_PAGE_SIZE = 50;
|
|
36
|
+
export const HARD_INGESTION_STATUS_PAGE_SIZE_CAP = 200;
|
|
23
37
|
function integer(value, fallback, cap, label, minimum = 1) {
|
|
24
38
|
const resolved = value ?? fallback;
|
|
25
39
|
if (!Number.isInteger(resolved) || resolved < minimum) {
|
|
@@ -42,6 +56,9 @@ export function resolveRagLimits(input = {}) {
|
|
|
42
56
|
chunkSize,
|
|
43
57
|
chunkOverlap,
|
|
44
58
|
maxDocumentChars: integer(input.maxDocumentChars, DEFAULT_MAX_DOCUMENT_CHARS, HARD_MAX_DOCUMENT_CHARS_CAP, "maxDocumentChars"),
|
|
59
|
+
maxDocumentBytes: integer(input.maxDocumentBytes, DEFAULT_MAX_DOCUMENT_BYTES, HARD_MAX_DOCUMENT_BYTES_CAP, "maxDocumentBytes"),
|
|
60
|
+
maxParseMs: integer(input.maxParseMs, DEFAULT_MAX_PARSE_MS, HARD_MAX_PARSE_MS_CAP, "maxParseMs"),
|
|
61
|
+
maxPdfPages: integer(input.maxPdfPages, DEFAULT_MAX_PDF_PAGES, HARD_MAX_PDF_PAGES_CAP, "maxPdfPages"),
|
|
45
62
|
maxChunks: integer(input.maxChunks, DEFAULT_MAX_CHUNKS, HARD_MAX_CHUNKS_CAP, "maxChunks"),
|
|
46
63
|
embedBatchSize: integer(input.embedBatchSize, DEFAULT_EMBED_BATCH_SIZE, HARD_EMBED_BATCH_SIZE_CAP, "embedBatchSize"),
|
|
47
64
|
topK,
|
|
@@ -50,6 +67,10 @@ export function resolveRagLimits(input = {}) {
|
|
|
50
67
|
maxContextTokens: integer(input.maxContextTokens, DEFAULT_MAX_CONTEXT_TOKENS, HARD_MAX_CONTEXT_TOKENS_CAP, "maxContextTokens"),
|
|
51
68
|
maxMetadataBytes: integer(input.maxMetadataBytes, DEFAULT_MAX_METADATA_BYTES, HARD_MAX_METADATA_BYTES_CAP, "maxMetadataBytes"),
|
|
52
69
|
maxVectorDimensions: integer(input.maxVectorDimensions, DEFAULT_MAX_VECTOR_DIMENSIONS, DEFAULT_MAX_VECTOR_DIMENSIONS, "maxVectorDimensions"),
|
|
70
|
+
maxRerankBytes: integer(input.maxRerankBytes, DEFAULT_MAX_RERANK_BYTES, HARD_MAX_RERANK_BYTES_CAP, "maxRerankBytes"),
|
|
71
|
+
maxRerankMs: integer(input.maxRerankMs, DEFAULT_MAX_RERANK_MS, HARD_MAX_RERANK_MS_CAP, "maxRerankMs"),
|
|
72
|
+
rerankConcurrency: integer(input.rerankConcurrency, DEFAULT_RERANK_CONCURRENCY, HARD_RERANK_CONCURRENCY_CAP, "rerankConcurrency"),
|
|
73
|
+
ingestionStatusPageSize: integer(input.ingestionStatusPageSize, DEFAULT_INGESTION_STATUS_PAGE_SIZE, HARD_INGESTION_STATUS_PAGE_SIZE_CAP, "ingestionStatusPageSize"),
|
|
53
74
|
});
|
|
54
75
|
}
|
|
55
76
|
//# sourceMappingURL=limits.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ResourceLoadContext, ResourceLoader } from "@arnilo/prism";
|
|
2
|
+
import type { DocumentLoader } from "./types.js";
|
|
3
|
+
export interface ResourceDocumentLoaderOptions {
|
|
4
|
+
readonly loader: ResourceLoader;
|
|
5
|
+
readonly context?: ResourceLoadContext;
|
|
6
|
+
}
|
|
7
|
+
export interface WebFetchDocument {
|
|
8
|
+
readonly citationId: string;
|
|
9
|
+
readonly provider: string;
|
|
10
|
+
readonly url: string;
|
|
11
|
+
readonly markdown: string;
|
|
12
|
+
readonly retrievedAt: string;
|
|
13
|
+
readonly untrusted: true;
|
|
14
|
+
}
|
|
15
|
+
export interface WebFetchAdapter {
|
|
16
|
+
fetch(url: string, options?: {
|
|
17
|
+
readonly signal?: AbortSignal;
|
|
18
|
+
}): Promise<WebFetchDocument>;
|
|
19
|
+
}
|
|
20
|
+
export interface WebFetchDocumentLoaderOptions {
|
|
21
|
+
readonly fetcher: WebFetchAdapter;
|
|
22
|
+
}
|
|
23
|
+
export declare function createResourceDocumentLoader(options: ResourceDocumentLoaderOptions): DocumentLoader;
|
|
24
|
+
export declare function createWebFetchDocumentLoader(options: WebFetchDocumentLoaderOptions): DocumentLoader;
|
package/dist/loaders.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { RagLimitError, RagValidationError } from "./errors.js";
|
|
2
|
+
import { resolveRagLimits } from "./limits.js";
|
|
3
|
+
import { nonEmpty } from "./util.js";
|
|
4
|
+
export function createResourceDocumentLoader(options) {
|
|
5
|
+
return {
|
|
6
|
+
async load(uri, loadOptions = {}) {
|
|
7
|
+
nonEmpty(uri, "uri");
|
|
8
|
+
const context = { ...options.context, ...(loadOptions.signal ? { signal: loadOptions.signal } : {}) };
|
|
9
|
+
const resource = await options.loader.load(uri, context);
|
|
10
|
+
if (resource.text === undefined && resource.data === undefined)
|
|
11
|
+
throw new RagValidationError("resource has no text or data");
|
|
12
|
+
const document = Object.freeze({
|
|
13
|
+
uri: resource.uri,
|
|
14
|
+
...(resource.mediaType ? { mediaType: resource.mediaType } : {}),
|
|
15
|
+
...(resource.text !== undefined ? { text: resource.text } : { data: new Uint8Array(resource.data) }),
|
|
16
|
+
});
|
|
17
|
+
assertDocumentBytes(document, loadOptions.maxBytes);
|
|
18
|
+
return document;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function createWebFetchDocumentLoader(options) {
|
|
23
|
+
return {
|
|
24
|
+
async load(uri, loadOptions = {}) {
|
|
25
|
+
const url = publicWebUrl(uri);
|
|
26
|
+
const document = await options.fetcher.fetch(url, { signal: loadOptions.signal });
|
|
27
|
+
publicWebUrl(document.url);
|
|
28
|
+
if (!document.untrusted || !document.markdown)
|
|
29
|
+
throw new RagValidationError("web fetcher returned an invalid untrusted document");
|
|
30
|
+
const loaded = Object.freeze({
|
|
31
|
+
uri: document.url,
|
|
32
|
+
sourceId: document.citationId,
|
|
33
|
+
mediaType: "text/markdown",
|
|
34
|
+
text: document.markdown,
|
|
35
|
+
metadata: {
|
|
36
|
+
web: {
|
|
37
|
+
citationId: document.citationId,
|
|
38
|
+
provider: document.provider,
|
|
39
|
+
url: document.url,
|
|
40
|
+
retrievedAt: document.retrievedAt,
|
|
41
|
+
},
|
|
42
|
+
untrusted: true,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
assertDocumentBytes(loaded, loadOptions.maxBytes);
|
|
46
|
+
return loaded;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function assertDocumentBytes(document, maxBytes) {
|
|
51
|
+
const limit = resolveRagLimits({ maxDocumentBytes: maxBytes }).maxDocumentBytes;
|
|
52
|
+
const bytes = document.data ?? Buffer.from(document.text ?? "", "utf8");
|
|
53
|
+
if (bytes.byteLength > limit)
|
|
54
|
+
throw new RagLimitError(`document exceeds ${limit} bytes`);
|
|
55
|
+
}
|
|
56
|
+
function publicWebUrl(input) {
|
|
57
|
+
let url;
|
|
58
|
+
try {
|
|
59
|
+
url = new URL(input);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new RagValidationError("web document URI must be an absolute HTTP(S) URL");
|
|
63
|
+
}
|
|
64
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
65
|
+
if (!/^https?:$/u.test(url.protocol) || url.username || url.password || privateOrLocalHost(host)) {
|
|
66
|
+
throw new RagValidationError("web document URI must use a public hostname without credentials");
|
|
67
|
+
}
|
|
68
|
+
url.hash = "";
|
|
69
|
+
return url.toString();
|
|
70
|
+
}
|
|
71
|
+
function privateOrLocalHost(host) {
|
|
72
|
+
if (!host || host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local"))
|
|
73
|
+
return true;
|
|
74
|
+
if (/^\d{1,3}(?:\.\d{1,3}){3}$/u.test(host) || host.includes(":"))
|
|
75
|
+
return true; // Require fetcher DNS policy for hostnames; never fetch IP literals.
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=loaders.js.map
|
package/dist/parsers.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { RagLimitError, RagValidationError } from "./errors.js";
|
|
2
|
+
import { resolveRagLimits } from "./limits.js";
|
|
3
|
+
import { assertNotAborted } from "./util.js";
|
|
4
|
+
export const textParser = { parse: (document, options) => parseText(document, options, ["text/plain"]) };
|
|
5
|
+
export const markdownParser = { parse: (document, options) => parseText(document, options, ["text/markdown", "text/x-markdown"]) };
|
|
6
|
+
export const htmlParser = { parse: (document, options) => parseText(document, options, ["text/html", "application/xhtml+xml"], htmlToText) };
|
|
7
|
+
export const pdfParser = { parse: parsePdf };
|
|
8
|
+
async function parseText(document, options = {}, mediaTypes, transform = (text) => text) {
|
|
9
|
+
assertMediaType(document, mediaTypes);
|
|
10
|
+
const { bytes, limits, started } = boundedDocument(document, options);
|
|
11
|
+
assertNotAborted(options.signal);
|
|
12
|
+
let text;
|
|
13
|
+
try {
|
|
14
|
+
text = document.text ?? new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new RagValidationError("document is not valid UTF-8 text");
|
|
18
|
+
}
|
|
19
|
+
const parsed = transform(text);
|
|
20
|
+
assertParseTime(started, limits.maxParseMs);
|
|
21
|
+
assertNotAborted(options.signal);
|
|
22
|
+
return Object.freeze({ text: parsed, ...(document.metadata ? { metadata: document.metadata } : {}) });
|
|
23
|
+
}
|
|
24
|
+
async function parsePdf(document, options = {}) {
|
|
25
|
+
assertMediaType(document, ["application/pdf"]);
|
|
26
|
+
const { bytes, limits, started } = boundedDocument(document, options);
|
|
27
|
+
if (document.text !== undefined)
|
|
28
|
+
throw new RagValidationError("PDF parser requires binary document data");
|
|
29
|
+
const pdf = Buffer.from(bytes).toString("latin1");
|
|
30
|
+
if (!pdf.startsWith("%PDF-"))
|
|
31
|
+
throw new RagValidationError("document is not a PDF");
|
|
32
|
+
if (/\/Filter\s*\//u.test(pdf))
|
|
33
|
+
throw new RagValidationError("compressed PDFs require a host parser");
|
|
34
|
+
const pages = [...pdf.matchAll(/\/Type\s*\/Page\b/gu)].length;
|
|
35
|
+
if (pages > limits.maxPdfPages)
|
|
36
|
+
throw new RagLimitError(`PDF exceeds ${limits.maxPdfPages} pages`);
|
|
37
|
+
const blocks = pdf.match(/BT[\s\S]*?ET/gu) ?? [];
|
|
38
|
+
const text = blocks.flatMap(pdfBlockText).join("\n").trim();
|
|
39
|
+
if (!text)
|
|
40
|
+
throw new RagValidationError("PDF has no uncompressed text");
|
|
41
|
+
assertParseTime(started, limits.maxParseMs);
|
|
42
|
+
assertNotAborted(options.signal);
|
|
43
|
+
return Object.freeze({ text, ...(document.metadata ? { metadata: document.metadata } : {}) });
|
|
44
|
+
}
|
|
45
|
+
function boundedDocument(document, options) {
|
|
46
|
+
assertNotAborted(options.signal);
|
|
47
|
+
const limits = resolveRagLimits({
|
|
48
|
+
maxDocumentBytes: options.maxBytes,
|
|
49
|
+
maxParseMs: options.maxParseMs,
|
|
50
|
+
maxPdfPages: options.maxPages,
|
|
51
|
+
});
|
|
52
|
+
const bytes = document.data ?? Buffer.from(document.text ?? "", "utf8");
|
|
53
|
+
if (bytes.byteLength > limits.maxDocumentBytes)
|
|
54
|
+
throw new RagLimitError(`document exceeds ${limits.maxDocumentBytes} bytes`);
|
|
55
|
+
return { bytes, limits, started: Date.now() };
|
|
56
|
+
}
|
|
57
|
+
function assertMediaType(document, expected) {
|
|
58
|
+
if (document.mediaType && !expected.includes(document.mediaType.toLowerCase())) {
|
|
59
|
+
throw new RagValidationError(`parser does not accept ${document.mediaType}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function assertParseTime(started, maxParseMs) {
|
|
63
|
+
if (Date.now() - started > maxParseMs)
|
|
64
|
+
throw new RagLimitError(`document parsing exceeded ${maxParseMs}ms`);
|
|
65
|
+
}
|
|
66
|
+
function htmlToText(html) {
|
|
67
|
+
return decodeEntities(html
|
|
68
|
+
.replace(/<!--[\s\S]*?-->/gu, "")
|
|
69
|
+
.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/giu, "")
|
|
70
|
+
.replace(/<(?:br|p|div|li|h[1-6]|tr)\b[^>]*>/giu, "\n")
|
|
71
|
+
.replace(/<[^>]*>/gu, " "))
|
|
72
|
+
.replace(/[ \t]{2,}/gu, " ")
|
|
73
|
+
.replace(/[ \t]+\n/gu, "\n")
|
|
74
|
+
.replace(/\n[ \t]+/gu, "\n")
|
|
75
|
+
.replace(/\n{3,}/gu, "\n\n")
|
|
76
|
+
.trim();
|
|
77
|
+
}
|
|
78
|
+
function decodeEntities(text) {
|
|
79
|
+
const entities = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", "#39": "'" };
|
|
80
|
+
return text.replace(/&([a-z]+|#39);/giu, (_match, name) => entities[name.toLowerCase()] ?? `&${name};`);
|
|
81
|
+
}
|
|
82
|
+
function pdfBlockText(block) {
|
|
83
|
+
const strings = [];
|
|
84
|
+
for (const match of block.matchAll(/\((?:\\.|[^\\)])*\)\s*(?:Tj|['"])/gu))
|
|
85
|
+
strings.push(decodePdfString(match[0].replace(/\s*(?:Tj|['"])$/u, "")));
|
|
86
|
+
for (const match of block.matchAll(/\[([\s\S]*?)\]\s*TJ/gu)) {
|
|
87
|
+
for (const value of match[1].matchAll(/\((?:\\.|[^\\)])*\)/gu))
|
|
88
|
+
strings.push(decodePdfString(value[0]));
|
|
89
|
+
}
|
|
90
|
+
return strings.filter(Boolean);
|
|
91
|
+
}
|
|
92
|
+
function decodePdfString(value) {
|
|
93
|
+
return value.slice(1, -1).replace(/\\([0-7]{1,3}|[nrtbf()\\])/gu, (_match, escape) => {
|
|
94
|
+
if (/^[0-7]+$/u.test(escape))
|
|
95
|
+
return String.fromCharCode(Number.parseInt(escape, 8));
|
|
96
|
+
return { n: "\n", r: "\r", t: "\t", b: "\b", f: "\f", "(": "(", ")": ")", "\\": "\\" }[escape];
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=parsers.js.map
|
package/dist/rerank.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RagHit, Reranker } from "./types.js";
|
|
2
|
+
import { resolveRedactor } from "./util.js";
|
|
3
|
+
export declare function rerankHits(query: string, hits: readonly RagHit[], options: {
|
|
4
|
+
readonly reranker: Reranker;
|
|
5
|
+
readonly maxBytes: number;
|
|
6
|
+
readonly maxMs: number;
|
|
7
|
+
readonly concurrency: number;
|
|
8
|
+
readonly signal?: AbortSignal;
|
|
9
|
+
readonly redactor?: Parameters<typeof resolveRedactor>[0];
|
|
10
|
+
readonly secrets?: Parameters<typeof resolveRedactor>[1];
|
|
11
|
+
}): Promise<readonly RagHit[]>;
|
package/dist/rerank.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { RagAbortError, RagLimitError, RagValidationError } from "./errors.js";
|
|
2
|
+
import { assertNotAborted, byteLength, resolveRedactor } from "./util.js";
|
|
3
|
+
const active = new WeakMap();
|
|
4
|
+
export async function rerankHits(query, hits, options) {
|
|
5
|
+
assertNotAborted(options.signal);
|
|
6
|
+
const redactor = resolveRedactor(options.redactor, options.secrets);
|
|
7
|
+
const safeQuery = redactor?.redact(query) ?? query;
|
|
8
|
+
const safeHits = hits.map((hit) => redactor?.redact(hit) ?? hit);
|
|
9
|
+
if (byteLength({ query: safeQuery, hits: safeHits }) > options.maxBytes) {
|
|
10
|
+
throw new RagLimitError(`reranker input exceeds ${options.maxBytes} bytes`);
|
|
11
|
+
}
|
|
12
|
+
const running = active.get(options.reranker) ?? 0;
|
|
13
|
+
if (running >= options.concurrency)
|
|
14
|
+
throw new RagLimitError(`reranker concurrency exceeds ${options.concurrency}`);
|
|
15
|
+
active.set(options.reranker, running + 1);
|
|
16
|
+
const release = () => {
|
|
17
|
+
const remaining = (active.get(options.reranker) ?? 1) - 1;
|
|
18
|
+
if (remaining)
|
|
19
|
+
active.set(options.reranker, remaining);
|
|
20
|
+
else
|
|
21
|
+
active.delete(options.reranker);
|
|
22
|
+
};
|
|
23
|
+
try {
|
|
24
|
+
const ordered = await boundedRerank((signal) => options.reranker.rerank({ query: safeQuery, hits: safeHits, signal }), (operation) => { void operation.then(release, release); }, options.maxMs, options.signal);
|
|
25
|
+
const originals = new Map(hits.map((hit) => [hit.id, hit]));
|
|
26
|
+
if (ordered.length !== hits.length || new Set(ordered.map((hit) => hit.id)).size !== hits.length) {
|
|
27
|
+
throw new RagValidationError("reranker must return each retrieved hit exactly once");
|
|
28
|
+
}
|
|
29
|
+
const output = ordered.map((hit) => originals.get(hit.id));
|
|
30
|
+
if (output.some((hit) => !hit))
|
|
31
|
+
throw new RagValidationError("reranker returned an unknown hit");
|
|
32
|
+
return Object.freeze(output);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error instanceof RagAbortError || error instanceof RagLimitError || error instanceof RagValidationError)
|
|
36
|
+
throw error;
|
|
37
|
+
const message = error instanceof Error ? error.message : "reranker failed";
|
|
38
|
+
throw new RagValidationError(`reranker failed: ${redactor?.redact(message) ?? message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function boundedRerank(run, onStart, maxMs, signal) {
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const onAbort = () => controller.abort();
|
|
44
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
45
|
+
const timeout = setTimeout(() => controller.abort(), maxMs);
|
|
46
|
+
const aborted = new Promise((_, reject) => controller.signal.addEventListener("abort", () => {
|
|
47
|
+
reject(signal?.aborted ? new RagAbortError() : new RagLimitError(`reranker exceeded ${maxMs}ms`));
|
|
48
|
+
}, { once: true }));
|
|
49
|
+
const operation = Promise.resolve().then(() => run(controller.signal));
|
|
50
|
+
onStart(operation);
|
|
51
|
+
try {
|
|
52
|
+
return await Promise.race([operation, aborted]);
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
clearTimeout(timeout);
|
|
56
|
+
signal?.removeEventListener("abort", onAbort);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=rerank.js.map
|
package/dist/retrieve.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { HARD_CHUNK_SIZE_CAP, resolveRagLimits } from "./limits.js";
|
|
2
2
|
import { RagScopeError, RagValidationError } from "./errors.js";
|
|
3
|
+
import { rerankHits } from "./rerank.js";
|
|
3
4
|
import { assertBytes, assertNotAborted, assertScope, byteLength, isJsonObject, matchesFilter, nonEmpty, requireScope, requireSourceId, resolveRedactor, truncateUtf8, } from "./util.js";
|
|
5
|
+
const RETRIEVED_CONTENT_TRUST = Object.freeze({ untrusted: true, inert: true, injectionCapable: true });
|
|
4
6
|
export async function retrieveContext(query, options) {
|
|
5
7
|
nonEmpty(query, "query");
|
|
6
|
-
if (query.length > HARD_CHUNK_SIZE_CAP)
|
|
8
|
+
if (query.length > HARD_CHUNK_SIZE_CAP)
|
|
7
9
|
throw new RagValidationError(`query exceeds ${HARD_CHUNK_SIZE_CAP} characters`);
|
|
8
|
-
}
|
|
9
10
|
const scope = requireScope(options.scope);
|
|
10
11
|
const limits = resolveRagLimits({
|
|
11
12
|
topK: options.topK,
|
|
@@ -14,10 +15,11 @@ export async function retrieveContext(query, options) {
|
|
|
14
15
|
maxContextTokens: options.maxContextTokens,
|
|
15
16
|
maxMetadataBytes: options.maxMetadataBytes,
|
|
16
17
|
maxVectorDimensions: options.maxVectorDimensions,
|
|
18
|
+
maxRerankBytes: options.maxRerankBytes,
|
|
19
|
+
maxRerankMs: options.maxRerankMs,
|
|
20
|
+
rerankConcurrency: options.rerankConcurrency,
|
|
17
21
|
});
|
|
18
|
-
if (!Number.isInteger(options.embedder.dimensions)
|
|
19
|
-
|| options.embedder.dimensions <= 0
|
|
20
|
-
|| options.embedder.dimensions > limits.maxVectorDimensions) {
|
|
22
|
+
if (!Number.isInteger(options.embedder.dimensions) || options.embedder.dimensions <= 0 || options.embedder.dimensions > limits.maxVectorDimensions) {
|
|
21
23
|
throw new RagValidationError(`embedder dimensions must be an integer in 1..${limits.maxVectorDimensions}`);
|
|
22
24
|
}
|
|
23
25
|
if (options.filter)
|
|
@@ -27,10 +29,7 @@ export async function retrieveContext(query, options) {
|
|
|
27
29
|
assertNotAborted(options.signal);
|
|
28
30
|
const vectors = await options.embedder.embed([safeQuery], { signal: options.signal });
|
|
29
31
|
const embedding = vectors[0];
|
|
30
|
-
if (vectors.length !== 1
|
|
31
|
-
|| !embedding
|
|
32
|
-
|| embedding.length !== options.embedder.dimensions
|
|
33
|
-
|| embedding.some((value) => !Number.isFinite(value))) {
|
|
32
|
+
if (vectors.length !== 1 || !embedding || embedding.length !== options.embedder.dimensions || embedding.some((value) => !Number.isFinite(value))) {
|
|
34
33
|
throw new RagValidationError("embedder returned invalid query vector");
|
|
35
34
|
}
|
|
36
35
|
const candidates = await options.store.query({
|
|
@@ -42,6 +41,26 @@ export async function retrieveContext(query, options) {
|
|
|
42
41
|
signal: options.signal,
|
|
43
42
|
});
|
|
44
43
|
assertNotAborted(options.signal);
|
|
44
|
+
const retrievedAt = new Date().toISOString();
|
|
45
|
+
const retrieved = [];
|
|
46
|
+
for (const candidate of candidates.slice(0, limits.queryCandidates)) {
|
|
47
|
+
assertScope(scope, candidate);
|
|
48
|
+
const parsed = parseHit(candidate, retrieved.length, retrievedAt);
|
|
49
|
+
if (!matchesFilter(parsed.metadata, options.filter))
|
|
50
|
+
continue;
|
|
51
|
+
retrieved.push(Object.freeze(redactor?.redact(parsed) ?? parsed));
|
|
52
|
+
}
|
|
53
|
+
const ranked = options.reranker
|
|
54
|
+
? await rerankHits(safeQuery, retrieved, {
|
|
55
|
+
reranker: options.reranker,
|
|
56
|
+
maxBytes: limits.maxRerankBytes,
|
|
57
|
+
maxMs: limits.maxRerankMs,
|
|
58
|
+
concurrency: limits.rerankConcurrency,
|
|
59
|
+
signal: options.signal,
|
|
60
|
+
redactor: options.redactor,
|
|
61
|
+
secrets: options.secrets,
|
|
62
|
+
})
|
|
63
|
+
: retrieved;
|
|
45
64
|
const hits = [];
|
|
46
65
|
const citations = [];
|
|
47
66
|
const rendered = [];
|
|
@@ -49,15 +68,10 @@ export async function retrieveContext(query, options) {
|
|
|
49
68
|
let usedBytes = 0;
|
|
50
69
|
let usedChars = 0;
|
|
51
70
|
let truncated = false;
|
|
52
|
-
for (const
|
|
53
|
-
assertScope(scope, candidate);
|
|
54
|
-
const parsed = parseHit(candidate);
|
|
55
|
-
if (!matchesFilter(parsed.metadata, options.filter))
|
|
56
|
-
continue;
|
|
71
|
+
for (const hit of ranked) {
|
|
57
72
|
if (hits.length >= limits.topK)
|
|
58
73
|
break;
|
|
59
|
-
const
|
|
60
|
-
const prefix = `[${parsed.citationId}] `;
|
|
74
|
+
const prefix = `[${hit.citationId}] `;
|
|
61
75
|
const separator = rendered.length ? "\n\n" : "";
|
|
62
76
|
const availableBytes = limits.maxResultBytes - usedBytes - byteLength(separator + prefix);
|
|
63
77
|
const availableChars = maxChars - usedChars - separator.length - prefix.length;
|
|
@@ -65,61 +79,60 @@ export async function retrieveContext(query, options) {
|
|
|
65
79
|
truncated = true;
|
|
66
80
|
break;
|
|
67
81
|
}
|
|
68
|
-
let text =
|
|
82
|
+
let text = hit.text.slice(0, availableChars);
|
|
69
83
|
text = truncateUtf8(text, availableBytes);
|
|
70
84
|
if (!text) {
|
|
71
85
|
truncated = true;
|
|
72
86
|
break;
|
|
73
87
|
}
|
|
74
|
-
if (text.length <
|
|
88
|
+
if (text.length < hit.text.length)
|
|
75
89
|
truncated = true;
|
|
76
|
-
const
|
|
90
|
+
const renderedHit = Object.freeze({ ...hit, text });
|
|
77
91
|
const citation = Object.freeze({
|
|
78
|
-
id:
|
|
79
|
-
sourceId:
|
|
80
|
-
chunkId:
|
|
81
|
-
|
|
92
|
+
id: renderedHit.citationId,
|
|
93
|
+
sourceId: renderedHit.sourceId,
|
|
94
|
+
chunkId: renderedHit.id,
|
|
95
|
+
provenance: renderedHit.provenance,
|
|
96
|
+
trust: renderedHit.trust,
|
|
97
|
+
...(renderedHit.metadata ? { metadata: renderedHit.metadata } : {}),
|
|
82
98
|
});
|
|
83
99
|
const block = `${separator}${prefix}${text}`;
|
|
84
100
|
rendered.push(block);
|
|
85
101
|
usedBytes += byteLength(block);
|
|
86
102
|
usedChars += block.length;
|
|
87
|
-
hits.push(
|
|
103
|
+
hits.push(renderedHit);
|
|
88
104
|
citations.push(citation);
|
|
89
105
|
if (truncated)
|
|
90
106
|
break;
|
|
91
107
|
}
|
|
92
108
|
return Object.freeze({
|
|
93
109
|
query: safeQuery,
|
|
110
|
+
trust: RETRIEVED_CONTENT_TRUST,
|
|
94
111
|
text: rendered.join(""),
|
|
95
112
|
hits: Object.freeze(hits),
|
|
96
113
|
citations: Object.freeze(citations),
|
|
97
114
|
truncated,
|
|
98
115
|
});
|
|
99
116
|
}
|
|
100
|
-
function parseHit(hit) {
|
|
117
|
+
function parseHit(hit, retrievalRank, retrievedAt) {
|
|
101
118
|
const metadata = hit.metadata;
|
|
102
119
|
const rag = metadata?._rag;
|
|
103
120
|
if (!isJsonObject(rag))
|
|
104
121
|
throw new RagScopeError("vector hit is missing RAG source metadata");
|
|
105
122
|
const sourceId = requireSourceId(rag.sourceId);
|
|
106
123
|
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)) {
|
|
124
|
+
if (!Number.isInteger(rag.chunkIndex) || Number(rag.chunkIndex) < 0 || !Number.isInteger(rag.start) || Number(rag.start) < 0 || !Number.isInteger(rag.end) || Number(rag.end) < Number(rag.start) || !Number.isFinite(hit.score)) {
|
|
114
125
|
throw new RagValidationError("vector hit has invalid RAG offsets");
|
|
115
126
|
}
|
|
116
|
-
if (hit.id !== citationId || !citationId.startsWith(`${sourceId}#`))
|
|
127
|
+
if (hit.id !== citationId || !citationId.startsWith(`${sourceId}#`))
|
|
117
128
|
throw new RagValidationError("vector hit has inconsistent citation identity");
|
|
118
|
-
}
|
|
119
129
|
const userMetadata = {};
|
|
120
130
|
for (const [key, value] of Object.entries(metadata ?? {}))
|
|
121
131
|
if (key !== "_rag")
|
|
122
132
|
userMetadata[key] = value;
|
|
133
|
+
const web = isJsonObject(userMetadata.web) ? userMetadata.web : undefined;
|
|
134
|
+
const provider = typeof web?.provider === "string" && web.provider.trim() ? web.provider : "host";
|
|
135
|
+
const provenance = Object.freeze({ sourceId, chunkId: hit.id, citationId, provider, retrieval: "vector", retrievedAt });
|
|
123
136
|
return {
|
|
124
137
|
id: hit.id,
|
|
125
138
|
citationId,
|
|
@@ -129,6 +142,9 @@ function parseHit(hit) {
|
|
|
129
142
|
end: rag.end,
|
|
130
143
|
text: hit.text,
|
|
131
144
|
score: hit.score,
|
|
145
|
+
retrievalRank,
|
|
146
|
+
provenance,
|
|
147
|
+
trust: RETRIEVED_CONTENT_TRUST,
|
|
132
148
|
...(Object.keys(userMetadata).length ? { metadata: userMetadata } : {}),
|
|
133
149
|
};
|
|
134
150
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DeleteSourceOptions, ReplaceDocumentOptions, ReplaceSourceOptions } from "./types.js";
|
|
2
|
+
export interface SourceMutationResult {
|
|
3
|
+
readonly sourceId: string;
|
|
4
|
+
readonly deleted: number;
|
|
5
|
+
readonly indexed: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function replaceSource(options: ReplaceSourceOptions): Promise<SourceMutationResult>;
|
|
8
|
+
export declare function deleteSource(options: DeleteSourceOptions): Promise<SourceMutationResult>;
|
|
9
|
+
export declare function replaceDocument(options: ReplaceDocumentOptions): Promise<SourceMutationResult>;
|
package/dist/sources.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { chunkText } from "./chunk.js";
|
|
2
|
+
import { RagScopeError, RagValidationError } from "./errors.js";
|
|
3
|
+
import { ingestionStatus } from "./ingestion-status.js";
|
|
4
|
+
import { indexChunkBatches } from "./indexing.js";
|
|
5
|
+
import { assertNotAborted, byteLength, requireScope, requireSourceId, resolveRedactor } from "./util.js";
|
|
6
|
+
export async function replaceSource(options) {
|
|
7
|
+
const sourceId = requireSourceId(options.sourceId);
|
|
8
|
+
const scope = requireScope(options.scope);
|
|
9
|
+
assertTransactionalStore(options.store);
|
|
10
|
+
if (options.chunks.some((chunk) => chunk.sourceId !== sourceId)) {
|
|
11
|
+
throw new RagValidationError("replaceSource chunks must all belong to sourceId");
|
|
12
|
+
}
|
|
13
|
+
const redactor = resolveRedactor(options.redactor, options.secrets);
|
|
14
|
+
const totalBytes = options.chunks.reduce((total, chunk) => total + byteLength(redactor?.redact(chunk.text) ?? chunk.text), 0);
|
|
15
|
+
const setStatus = async (state, error) => {
|
|
16
|
+
if (!options.statusStore)
|
|
17
|
+
return;
|
|
18
|
+
const message = error instanceof Error ? error.message : error === undefined ? undefined : "source replacement failed";
|
|
19
|
+
await options.statusStore.set(ingestionStatus(scope, sourceId, state, state === "indexed" ? totalBytes : 0, state === "indexed" ? options.chunks.length : 0, message ? redactor?.redact(message) ?? message : undefined));
|
|
20
|
+
};
|
|
21
|
+
await setStatus("pending");
|
|
22
|
+
try {
|
|
23
|
+
const staged = [];
|
|
24
|
+
const indexed = await indexChunkBatches({ ...options, statusStore: undefined }, async (records) => { staged.push(...records); });
|
|
25
|
+
assertNotAborted(options.signal);
|
|
26
|
+
const result = await options.store.transaction(async (store) => {
|
|
27
|
+
const previous = await sourceRecords(store, sourceId, scope, options.signal);
|
|
28
|
+
assertNotAborted(options.signal);
|
|
29
|
+
if (previous.length) {
|
|
30
|
+
await store.delete({ tenantId: scope.tenantId, resourceId: scope.resourceId, threadId: scope.corpusId, ids: previous.map((record) => record.id) }, { signal: options.signal });
|
|
31
|
+
}
|
|
32
|
+
if (staged.length)
|
|
33
|
+
await store.upsert(staged, { signal: options.signal });
|
|
34
|
+
return Object.freeze({ sourceId, deleted: previous.length, indexed: indexed.indexed });
|
|
35
|
+
}, { signal: options.signal });
|
|
36
|
+
await setStatus("indexed");
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
await setStatus("failed", error);
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function deleteSource(options) {
|
|
45
|
+
const sourceId = requireSourceId(options.sourceId);
|
|
46
|
+
const scope = requireScope(options.scope);
|
|
47
|
+
assertSourceStore(options.store);
|
|
48
|
+
const records = await sourceRecords(options.store, sourceId, scope, options.signal);
|
|
49
|
+
assertNotAborted(options.signal);
|
|
50
|
+
const deleted = records.length
|
|
51
|
+
? await options.store.delete({ tenantId: scope.tenantId, resourceId: scope.resourceId, threadId: scope.corpusId, ids: records.map((record) => record.id) }, { signal: options.signal })
|
|
52
|
+
: 0;
|
|
53
|
+
await options.statusStore?.delete(scope, sourceId);
|
|
54
|
+
return Object.freeze({ sourceId, deleted, indexed: 0 });
|
|
55
|
+
}
|
|
56
|
+
export async function replaceDocument(options) {
|
|
57
|
+
const loaded = await options.loader.load(options.uri, {
|
|
58
|
+
...options.loaderOptions,
|
|
59
|
+
signal: options.signal ?? options.loaderOptions?.signal,
|
|
60
|
+
});
|
|
61
|
+
assertNotAborted(options.signal);
|
|
62
|
+
const parsed = await options.parser.parse(loaded, {
|
|
63
|
+
...options.parserOptions,
|
|
64
|
+
signal: options.signal ?? options.parserOptions?.signal,
|
|
65
|
+
});
|
|
66
|
+
assertNotAborted(options.signal);
|
|
67
|
+
const sourceId = options.sourceId ?? loaded.sourceId;
|
|
68
|
+
if (!sourceId)
|
|
69
|
+
throw new RagValidationError("replaceDocument requires sourceId when its loader does not provide one");
|
|
70
|
+
const { metadata, ...chunk } = options.chunk ?? {};
|
|
71
|
+
const mergedMetadata = mergeMetadata(parsed.metadata, metadata);
|
|
72
|
+
const chunks = (options.chunker ?? chunkText)(parsed.text, {
|
|
73
|
+
...chunk,
|
|
74
|
+
sourceId,
|
|
75
|
+
...(mergedMetadata ? { metadata: mergedMetadata } : {}),
|
|
76
|
+
});
|
|
77
|
+
return replaceSource({
|
|
78
|
+
...options,
|
|
79
|
+
sourceId,
|
|
80
|
+
chunks,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function mergeMetadata(parser, supplied) {
|
|
84
|
+
if (!parser && !supplied)
|
|
85
|
+
return undefined;
|
|
86
|
+
return { ...supplied, ...parser }; // Loader trust metadata wins over caller-supplied metadata.
|
|
87
|
+
}
|
|
88
|
+
async function sourceRecords(store, sourceId, scope, signal) {
|
|
89
|
+
assertNotAborted(signal);
|
|
90
|
+
const records = await store.getBySource({ tenantId: scope.tenantId, resourceId: scope.resourceId, threadId: scope.corpusId }, sourceId, { signal });
|
|
91
|
+
for (const record of records) {
|
|
92
|
+
if (record.tenantId !== scope.tenantId || record.resourceId !== scope.resourceId || record.threadId !== scope.corpusId) {
|
|
93
|
+
throw new RagScopeError("source lookup crossed tenant/resource/corpus boundary");
|
|
94
|
+
}
|
|
95
|
+
const rag = record.metadata?._rag;
|
|
96
|
+
if (typeof rag !== "object" || rag === null || Array.isArray(rag) || rag.sourceId !== sourceId) {
|
|
97
|
+
throw new RagScopeError("source lookup returned a different source");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return records;
|
|
101
|
+
}
|
|
102
|
+
function assertSourceStore(store) {
|
|
103
|
+
if (!store || typeof store !== "object" || typeof store.getBySource !== "function") {
|
|
104
|
+
throw new RagValidationError("source deletion requires a scoped source-aware vector store");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function assertTransactionalStore(store) {
|
|
108
|
+
if (!store || typeof store !== "object" || typeof store.transaction !== "function") {
|
|
109
|
+
throw new RagValidationError("atomic source replacement requires a transactional vector store");
|
|
110
|
+
}
|
|
111
|
+
assertSourceStore(store);
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=sources.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ContextProvider, JsonObject, Message, SecretRedactor } from "@arnilo/prism";
|
|
2
|
-
import type { Embedder, VectorStore } from "@arnilo/prism-memory";
|
|
2
|
+
import type { Embedder, MemoryVectorRecord, VectorStore } from "@arnilo/prism-memory";
|
|
3
3
|
export interface RagScope {
|
|
4
4
|
readonly tenantId: string;
|
|
5
5
|
readonly resourceId: string;
|
|
@@ -23,6 +23,80 @@ export interface ChunkOptions {
|
|
|
23
23
|
readonly maxDocumentChars?: number;
|
|
24
24
|
readonly maxChunks?: number;
|
|
25
25
|
}
|
|
26
|
+
export interface LoadedDocument {
|
|
27
|
+
readonly uri: string;
|
|
28
|
+
readonly sourceId?: string;
|
|
29
|
+
readonly mediaType?: string;
|
|
30
|
+
readonly text?: string;
|
|
31
|
+
readonly data?: Uint8Array;
|
|
32
|
+
readonly metadata?: JsonObject;
|
|
33
|
+
}
|
|
34
|
+
export interface ParsedDocument {
|
|
35
|
+
readonly text: string;
|
|
36
|
+
readonly metadata?: JsonObject;
|
|
37
|
+
}
|
|
38
|
+
export interface DocumentLoadOptions {
|
|
39
|
+
readonly signal?: AbortSignal;
|
|
40
|
+
readonly maxBytes?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface DocumentParseOptions extends DocumentLoadOptions {
|
|
43
|
+
readonly maxParseMs?: number;
|
|
44
|
+
readonly maxPages?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface DocumentLoader {
|
|
47
|
+
load(uri: string, options?: DocumentLoadOptions): Promise<LoadedDocument>;
|
|
48
|
+
}
|
|
49
|
+
export interface Parser {
|
|
50
|
+
parse(document: LoadedDocument, options?: DocumentParseOptions): Promise<ParsedDocument>;
|
|
51
|
+
}
|
|
52
|
+
export type Chunker = (text: string, options: ChunkOptions) => readonly RagChunk[];
|
|
53
|
+
export interface SourceVectorStore extends VectorStore {
|
|
54
|
+
getBySource(scope: {
|
|
55
|
+
readonly tenantId: string;
|
|
56
|
+
readonly resourceId: string;
|
|
57
|
+
readonly threadId: string;
|
|
58
|
+
}, sourceId: string, options?: {
|
|
59
|
+
readonly signal?: AbortSignal;
|
|
60
|
+
}): Promise<readonly MemoryVectorRecord[]>;
|
|
61
|
+
}
|
|
62
|
+
export interface TransactionalVectorStore extends SourceVectorStore {
|
|
63
|
+
transaction<T>(operation: (store: SourceVectorStore) => Promise<T>, options?: {
|
|
64
|
+
readonly signal?: AbortSignal;
|
|
65
|
+
}): Promise<T>;
|
|
66
|
+
}
|
|
67
|
+
export type IngestionState = "pending" | "indexed" | "failed" | "partial";
|
|
68
|
+
export interface IngestionStatus {
|
|
69
|
+
readonly sourceId: string;
|
|
70
|
+
readonly scope: RagScope;
|
|
71
|
+
readonly state: IngestionState;
|
|
72
|
+
readonly bytes: number;
|
|
73
|
+
readonly chunks: number;
|
|
74
|
+
readonly error?: string;
|
|
75
|
+
readonly updatedAt: string;
|
|
76
|
+
}
|
|
77
|
+
export interface IngestionStatusStore {
|
|
78
|
+
set(status: IngestionStatus, options?: {
|
|
79
|
+
readonly signal?: AbortSignal;
|
|
80
|
+
}): Promise<void>;
|
|
81
|
+
delete(scope: RagScope, sourceId: string, options?: {
|
|
82
|
+
readonly signal?: AbortSignal;
|
|
83
|
+
}): Promise<void>;
|
|
84
|
+
list(scope: RagScope, options: {
|
|
85
|
+
readonly limit: number;
|
|
86
|
+
readonly cursor?: string;
|
|
87
|
+
readonly signal?: AbortSignal;
|
|
88
|
+
}): Promise<{
|
|
89
|
+
readonly entries: readonly IngestionStatus[];
|
|
90
|
+
readonly nextCursor?: string;
|
|
91
|
+
}>;
|
|
92
|
+
}
|
|
93
|
+
export interface IngestionStatusQuery {
|
|
94
|
+
readonly store: IngestionStatusStore;
|
|
95
|
+
readonly scope: RagScope;
|
|
96
|
+
readonly limit?: number;
|
|
97
|
+
readonly cursor?: string;
|
|
98
|
+
readonly signal?: AbortSignal;
|
|
99
|
+
}
|
|
26
100
|
export interface IndexChunksOptions {
|
|
27
101
|
readonly chunks: readonly RagChunk[];
|
|
28
102
|
readonly embedder: Embedder;
|
|
@@ -35,20 +109,68 @@ export interface IndexChunksOptions {
|
|
|
35
109
|
readonly maxMetadataBytes?: number;
|
|
36
110
|
readonly redactor?: SecretRedactor;
|
|
37
111
|
readonly secrets?: readonly (string | undefined)[];
|
|
112
|
+
readonly statusStore?: IngestionStatusStore;
|
|
38
113
|
readonly signal?: AbortSignal;
|
|
39
114
|
}
|
|
40
115
|
export interface IndexChunksResult {
|
|
41
116
|
readonly indexed: number;
|
|
42
117
|
readonly sourceIds: readonly string[];
|
|
43
118
|
}
|
|
119
|
+
export interface ReplaceSourceOptions extends Omit<IndexChunksOptions, "chunks" | "store"> {
|
|
120
|
+
readonly sourceId: string;
|
|
121
|
+
readonly chunks: readonly RagChunk[];
|
|
122
|
+
readonly store: TransactionalVectorStore;
|
|
123
|
+
}
|
|
124
|
+
export interface DeleteSourceOptions {
|
|
125
|
+
readonly sourceId: string;
|
|
126
|
+
readonly store: SourceVectorStore;
|
|
127
|
+
readonly scope: RagScope;
|
|
128
|
+
readonly statusStore?: IngestionStatusStore;
|
|
129
|
+
readonly signal?: AbortSignal;
|
|
130
|
+
}
|
|
131
|
+
export interface ReplaceDocumentOptions extends Omit<ReplaceSourceOptions, "sourceId" | "chunks"> {
|
|
132
|
+
readonly uri: string;
|
|
133
|
+
readonly sourceId?: string;
|
|
134
|
+
readonly loader: DocumentLoader;
|
|
135
|
+
readonly parser: Parser;
|
|
136
|
+
readonly chunker?: Chunker;
|
|
137
|
+
readonly chunk?: Omit<ChunkOptions, "sourceId">;
|
|
138
|
+
readonly loaderOptions?: DocumentLoadOptions;
|
|
139
|
+
readonly parserOptions?: DocumentParseOptions;
|
|
140
|
+
}
|
|
141
|
+
export interface RagProvenance {
|
|
142
|
+
readonly sourceId: string;
|
|
143
|
+
readonly chunkId: string;
|
|
144
|
+
readonly citationId: string;
|
|
145
|
+
readonly provider: string;
|
|
146
|
+
readonly retrieval: "vector";
|
|
147
|
+
readonly retrievedAt: string;
|
|
148
|
+
}
|
|
149
|
+
export interface RagContentTrust {
|
|
150
|
+
readonly untrusted: true;
|
|
151
|
+
readonly inert: true;
|
|
152
|
+
readonly injectionCapable: true;
|
|
153
|
+
}
|
|
44
154
|
export interface RagCitation {
|
|
45
155
|
readonly id: string;
|
|
46
156
|
readonly sourceId: string;
|
|
47
157
|
readonly chunkId: string;
|
|
158
|
+
readonly provenance: RagProvenance;
|
|
159
|
+
readonly trust: RagContentTrust;
|
|
48
160
|
readonly metadata?: JsonObject;
|
|
49
161
|
}
|
|
50
162
|
export interface RagHit extends RagChunk {
|
|
51
163
|
readonly score: number;
|
|
164
|
+
readonly retrievalRank: number;
|
|
165
|
+
readonly provenance: RagProvenance;
|
|
166
|
+
readonly trust: RagContentTrust;
|
|
167
|
+
}
|
|
168
|
+
export interface Reranker {
|
|
169
|
+
rerank(input: {
|
|
170
|
+
readonly query: string;
|
|
171
|
+
readonly hits: readonly RagHit[];
|
|
172
|
+
readonly signal?: AbortSignal;
|
|
173
|
+
}): Promise<readonly RagHit[]>;
|
|
52
174
|
}
|
|
53
175
|
export interface RetrieveContextOptions {
|
|
54
176
|
readonly embedder: Embedder;
|
|
@@ -61,12 +183,17 @@ export interface RetrieveContextOptions {
|
|
|
61
183
|
readonly maxContextTokens?: number;
|
|
62
184
|
readonly maxMetadataBytes?: number;
|
|
63
185
|
readonly maxVectorDimensions?: number;
|
|
186
|
+
readonly reranker?: Reranker;
|
|
187
|
+
readonly maxRerankBytes?: number;
|
|
188
|
+
readonly maxRerankMs?: number;
|
|
189
|
+
readonly rerankConcurrency?: number;
|
|
64
190
|
readonly redactor?: SecretRedactor;
|
|
65
191
|
readonly secrets?: readonly (string | undefined)[];
|
|
66
192
|
readonly signal?: AbortSignal;
|
|
67
193
|
}
|
|
68
194
|
export interface RagContextResult {
|
|
69
195
|
readonly query: string;
|
|
196
|
+
readonly trust: RagContentTrust;
|
|
70
197
|
readonly text: string;
|
|
71
198
|
readonly hits: readonly RagHit[];
|
|
72
199
|
readonly citations: readonly RagCitation[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arnilo/prism-rag",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"description": "Optional bounded text and Markdown RAG primitives for Prism.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./loaders": {
|
|
14
|
+
"types": "./dist/loaders.d.ts",
|
|
15
|
+
"default": "./dist/loaders.js"
|
|
16
|
+
},
|
|
17
|
+
"./parsers": {
|
|
18
|
+
"types": "./dist/parsers.d.ts",
|
|
19
|
+
"default": "./dist/parsers.js"
|
|
12
20
|
}
|
|
13
21
|
},
|
|
14
22
|
"files": [
|
|
@@ -25,8 +33,8 @@
|
|
|
25
33
|
"pack:dry-run": "npm pack --dry-run"
|
|
26
34
|
},
|
|
27
35
|
"peerDependencies": {
|
|
28
|
-
"@arnilo/prism": "0.0.
|
|
29
|
-
"@arnilo/prism-memory": "0.0.
|
|
36
|
+
"@arnilo/prism": "0.0.15",
|
|
37
|
+
"@arnilo/prism-memory": "0.0.15"
|
|
30
38
|
},
|
|
31
39
|
"devDependencies": {
|
|
32
40
|
"@arnilo/prism": "file:../..",
|