@arnilo/prism-rag 0.0.13 → 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 CHANGED
@@ -1,5 +1,19 @@
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
+
11
+ ## [0.0.14] - 2026-07-26
12
+
13
+ ### Changed
14
+
15
+ - Released with exact 0.0.14 graph.
16
+
3
17
  ## [0.0.13] - 2026-07-24
4
18
 
5
19
  ### Changed
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @arnilo/prism-rag
2
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.
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, indexChunks } from "@arnilo/prism-rag";
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
- await indexChunks({ chunks, embedder, store, scope });
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
- - `retrieveContext()` bounded candidate query, shallow metadata filter, top-K hits, and citation rendering.
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; load remote/local sources through host-owned resource/media policies. Retrieved text is untrusted context and grants no tools or permissions.
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";
@@ -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 { 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.
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
- 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 });
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;
@@ -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
@@ -0,0 +1,5 @@
1
+ import type { Parser } from "./types.js";
2
+ export declare const textParser: Parser;
3
+ export declare const markdownParser: Parser;
4
+ export declare const htmlParser: Parser;
5
+ export declare const pdfParser: Parser;
@@ -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
@@ -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 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;
71
+ for (const hit of ranked) {
57
72
  if (hits.length >= limits.topK)
58
73
  break;
59
- const safe = redactor?.redact(parsed) ?? parsed;
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 = safe.text.slice(0, availableChars);
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 < safe.text.length)
88
+ if (text.length < hit.text.length)
75
89
  truncated = true;
76
- const hit = Object.freeze({ ...safe, text });
90
+ const renderedHit = Object.freeze({ ...hit, text });
77
91
  const citation = Object.freeze({
78
- id: hit.citationId,
79
- sourceId: hit.sourceId,
80
- chunkId: hit.id,
81
- ...(hit.metadata ? { metadata: hit.metadata } : {}),
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(hit);
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>;
@@ -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.13",
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.13",
29
- "@arnilo/prism-memory": "0.0.13"
36
+ "@arnilo/prism": "0.0.15",
37
+ "@arnilo/prism-memory": "0.0.15"
30
38
  },
31
39
  "devDependencies": {
32
40
  "@arnilo/prism": "file:../..",