@seanhogg/builderforce-memory 2026.7.14 → 2026.9.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/bridges/AnthropicBridge.d.ts +9 -1
- package/dist/bridges/AnthropicBridge.d.ts.map +1 -1
- package/dist/bridges/AnthropicBridge.js +58 -2
- package/dist/bridges/AnthropicBridge.js.map +1 -1
- package/dist/bridges/CachingBridge.d.ts +9 -1
- package/dist/bridges/CachingBridge.d.ts.map +1 -1
- package/dist/bridges/CachingBridge.js +24 -1
- package/dist/bridges/CachingBridge.js.map +1 -1
- package/dist/bridges/OpenAIBridge.d.ts +4 -1
- package/dist/bridges/OpenAIBridge.d.ts.map +1 -1
- package/dist/bridges/OpenAIBridge.js +40 -2
- package/dist/bridges/OpenAIBridge.js.map +1 -1
- package/dist/bridges/SemanticCachingBridge.d.ts +8 -1
- package/dist/bridges/SemanticCachingBridge.d.ts.map +1 -1
- package/dist/bridges/SemanticCachingBridge.js +26 -1
- package/dist/bridges/SemanticCachingBridge.js.map +1 -1
- package/dist/bridges/TransformerBridge.d.ts +13 -0
- package/dist/bridges/TransformerBridge.d.ts.map +1 -1
- package/dist/bridges/VertexAIBridge.d.ts +102 -0
- package/dist/bridges/VertexAIBridge.d.ts.map +1 -0
- package/dist/bridges/VertexAIBridge.js +224 -0
- package/dist/bridges/VertexAIBridge.js.map +1 -0
- package/dist/bridges/index.d.ts +3 -1
- package/dist/bridges/index.d.ts.map +1 -1
- package/dist/bridges/index.js +1 -0
- package/dist/bridges/index.js.map +1 -1
- package/dist/eval/EvalHarness.d.ts +37 -0
- package/dist/eval/EvalHarness.d.ts.map +1 -0
- package/dist/eval/EvalHarness.js +178 -0
- package/dist/eval/EvalHarness.js.map +1 -0
- package/dist/eval/graders.d.ts +76 -0
- package/dist/eval/graders.d.ts.map +1 -0
- package/dist/eval/graders.js +168 -0
- package/dist/eval/graders.js.map +1 -0
- package/dist/eval/index.d.ts +26 -0
- package/dist/eval/index.d.ts.map +1 -0
- package/dist/eval/index.js +26 -0
- package/dist/eval/index.js.map +1 -0
- package/dist/eval/types.d.ts +97 -0
- package/dist/eval/types.d.ts.map +1 -0
- package/dist/eval/types.js +5 -0
- package/dist/eval/types.js.map +1 -0
- package/dist/evermind/contract.d.ts +40 -0
- package/dist/evermind/contract.d.ts.map +1 -0
- package/dist/evermind/contract.js +13 -0
- package/dist/evermind/contract.js.map +1 -0
- package/dist/evermind/index.d.ts +15 -0
- package/dist/evermind/index.d.ts.map +1 -0
- package/dist/evermind/index.js +4 -0
- package/dist/evermind/index.js.map +1 -0
- package/dist/evermind/memoryBlock.d.ts +7 -0
- package/dist/evermind/memoryBlock.d.ts.map +1 -0
- package/dist/evermind/memoryBlock.js +37 -0
- package/dist/evermind/memoryBlock.js.map +1 -0
- package/dist/evermind/recall.d.ts +55 -0
- package/dist/evermind/recall.d.ts.map +1 -0
- package/dist/evermind/recall.js +105 -0
- package/dist/evermind/recall.js.map +1 -0
- package/dist/index.d.ts +18 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/ingest/IngestionPipeline.d.ts +95 -0
- package/dist/ingest/IngestionPipeline.d.ts.map +1 -0
- package/dist/ingest/IngestionPipeline.js +295 -0
- package/dist/ingest/IngestionPipeline.js.map +1 -0
- package/dist/ingest/index.d.ts +10 -0
- package/dist/ingest/index.d.ts.map +1 -0
- package/dist/ingest/index.js +8 -0
- package/dist/ingest/index.js.map +1 -0
- package/dist/ingest/parsers.d.ts +53 -0
- package/dist/ingest/parsers.d.ts.map +1 -0
- package/dist/ingest/parsers.js +306 -0
- package/dist/ingest/parsers.js.map +1 -0
- package/dist/ingest/types.d.ts +114 -0
- package/dist/ingest/types.d.ts.map +1 -0
- package/dist/ingest/types.js +19 -0
- package/dist/ingest/types.js.map +1 -0
- package/dist/memory/MemoryStore.d.ts +33 -0
- package/dist/memory/MemoryStore.d.ts.map +1 -1
- package/dist/memory/MemoryStore.js +29 -3
- package/dist/memory/MemoryStore.js.map +1 -1
- package/dist/memory/index.d.ts +1 -1
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/orchestration/AgentGraph.d.ts +104 -0
- package/dist/orchestration/AgentGraph.d.ts.map +1 -0
- package/dist/orchestration/AgentGraph.js +364 -0
- package/dist/orchestration/AgentGraph.js.map +1 -0
- package/dist/orchestration/checkpoint.d.ts +58 -0
- package/dist/orchestration/checkpoint.d.ts.map +1 -0
- package/dist/orchestration/checkpoint.js +92 -0
- package/dist/orchestration/checkpoint.js.map +1 -0
- package/dist/orchestration/index.d.ts +14 -0
- package/dist/orchestration/index.d.ts.map +1 -0
- package/dist/orchestration/index.js +10 -0
- package/dist/orchestration/index.js.map +1 -0
- package/dist/orchestration/patterns.d.ts +159 -0
- package/dist/orchestration/patterns.d.ts.map +1 -0
- package/dist/orchestration/patterns.js +371 -0
- package/dist/orchestration/patterns.js.map +1 -0
- package/dist/orchestration/types.d.ts +146 -0
- package/dist/orchestration/types.d.ts.map +1 -0
- package/dist/orchestration/types.js +20 -0
- package/dist/orchestration/types.js.map +1 -0
- package/dist/rag/EnterpriseRetriever.d.ts +114 -0
- package/dist/rag/EnterpriseRetriever.d.ts.map +1 -0
- package/dist/rag/EnterpriseRetriever.js +240 -0
- package/dist/rag/EnterpriseRetriever.js.map +1 -0
- package/dist/rag/index.d.ts +6 -0
- package/dist/rag/index.d.ts.map +1 -0
- package/dist/rag/index.js +5 -0
- package/dist/rag/index.js.map +1 -0
- package/dist/retrieval/bm25.d.ts +48 -1
- package/dist/retrieval/bm25.d.ts.map +1 -1
- package/dist/retrieval/bm25.js +59 -13
- package/dist/retrieval/bm25.js.map +1 -1
- package/dist/retrieval/index.d.ts +2 -2
- package/dist/retrieval/index.d.ts.map +1 -1
- package/dist/retrieval/index.js +1 -1
- package/dist/retrieval/index.js.map +1 -1
- package/dist/telemetry/InstrumentedBridge.d.ts +62 -0
- package/dist/telemetry/InstrumentedBridge.d.ts.map +1 -0
- package/dist/telemetry/InstrumentedBridge.js +120 -0
- package/dist/telemetry/InstrumentedBridge.js.map +1 -0
- package/dist/telemetry/MetricsRegistry.d.ts +88 -0
- package/dist/telemetry/MetricsRegistry.d.ts.map +1 -0
- package/dist/telemetry/MetricsRegistry.js +167 -0
- package/dist/telemetry/MetricsRegistry.js.map +1 -0
- package/dist/telemetry/Tracer.d.ts +137 -0
- package/dist/telemetry/Tracer.d.ts.map +1 -0
- package/dist/telemetry/Tracer.js +244 -0
- package/dist/telemetry/Tracer.js.map +1 -0
- package/dist/telemetry/exporters.d.ts +68 -0
- package/dist/telemetry/exporters.d.ts.map +1 -0
- package/dist/telemetry/exporters.js +145 -0
- package/dist/telemetry/exporters.js.map +1 -0
- package/dist/telemetry/index.d.ts +19 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +12 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/pricing.d.ts +74 -0
- package/dist/telemetry/pricing.d.ts.map +1 -0
- package/dist/telemetry/pricing.js +98 -0
- package/dist/telemetry/pricing.js.map +1 -0
- package/dist/telemetry/traceparent.d.ts +50 -0
- package/dist/telemetry/traceparent.d.ts.map +1 -0
- package/dist/telemetry/traceparent.js +77 -0
- package/dist/telemetry/traceparent.js.map +1 -0
- package/dist/telemetry/types.d.ts +90 -0
- package/dist/telemetry/types.d.ts.map +1 -0
- package/dist/telemetry/types.js +17 -0
- package/dist/telemetry/types.js.map +1 -0
- package/dist/vectorstore/MemoryVectorStore.d.ts +72 -0
- package/dist/vectorstore/MemoryVectorStore.d.ts.map +1 -0
- package/dist/vectorstore/MemoryVectorStore.js +205 -0
- package/dist/vectorstore/MemoryVectorStore.js.map +1 -0
- package/dist/vectorstore/RestVectorStore.d.ts +79 -0
- package/dist/vectorstore/RestVectorStore.d.ts.map +1 -0
- package/dist/vectorstore/RestVectorStore.js +170 -0
- package/dist/vectorstore/RestVectorStore.js.map +1 -0
- package/dist/vectorstore/dialects.d.ts +101 -0
- package/dist/vectorstore/dialects.d.ts.map +1 -0
- package/dist/vectorstore/dialects.js +618 -0
- package/dist/vectorstore/dialects.js.map +1 -0
- package/dist/vectorstore/filter.d.ts +42 -0
- package/dist/vectorstore/filter.d.ts.map +1 -0
- package/dist/vectorstore/filter.js +132 -0
- package/dist/vectorstore/filter.js.map +1 -0
- package/dist/vectorstore/index.d.ts +15 -0
- package/dist/vectorstore/index.d.ts.map +1 -0
- package/dist/vectorstore/index.js +11 -0
- package/dist/vectorstore/index.js.map +1 -0
- package/dist/vectorstore/types.d.ts +151 -0
- package/dist/vectorstore/types.d.ts.map +1 -0
- package/dist/vectorstore/types.js +30 -0
- package/dist/vectorstore/types.js.map +1 -0
- package/package.json +39 -5
- package/src/bridges/AnthropicBridge.ts +68 -3
- package/src/bridges/CachingBridge.ts +28 -2
- package/src/bridges/OpenAIBridge.ts +48 -3
- package/src/bridges/SemanticCachingBridge.ts +29 -2
- package/src/bridges/TransformerBridge.ts +16 -0
- package/src/bridges/VertexAIBridge.ts +297 -0
- package/src/bridges/index.ts +3 -1
- package/src/eval/EvalHarness.ts +214 -0
- package/src/eval/graders.ts +218 -0
- package/src/eval/index.ts +26 -0
- package/src/eval/types.ts +107 -0
- package/src/evermind/contract.ts +39 -0
- package/src/evermind/index.ts +21 -0
- package/src/evermind/memoryBlock.ts +38 -0
- package/src/evermind/recall.ts +122 -0
- package/src/index.ts +232 -0
- package/src/ingest/IngestionPipeline.ts +378 -0
- package/src/ingest/index.ts +46 -0
- package/src/ingest/parsers.ts +318 -0
- package/src/ingest/types.ts +116 -0
- package/src/memory/MemoryStore.ts +55 -3
- package/src/memory/index.ts +1 -1
- package/src/orchestration/AgentGraph.ts +462 -0
- package/src/orchestration/checkpoint.ts +129 -0
- package/src/orchestration/index.ts +63 -0
- package/src/orchestration/patterns.ts +532 -0
- package/src/orchestration/types.ts +128 -0
- package/src/rag/EnterpriseRetriever.ts +348 -0
- package/src/rag/index.ts +18 -0
- package/src/retrieval/bm25.ts +71 -13
- package/src/retrieval/index.ts +2 -2
- package/src/telemetry/InstrumentedBridge.ts +152 -0
- package/src/telemetry/MetricsRegistry.ts +263 -0
- package/src/telemetry/Tracer.ts +310 -0
- package/src/telemetry/exporters.ts +187 -0
- package/src/telemetry/index.ts +56 -0
- package/src/telemetry/pricing.ts +129 -0
- package/src/telemetry/traceparent.ts +89 -0
- package/src/telemetry/types.ts +105 -0
- package/src/vectorstore/MemoryVectorStore.ts +250 -0
- package/src/vectorstore/RestVectorStore.ts +236 -0
- package/src/vectorstore/dialects.ts +702 -0
- package/src/vectorstore/filter.ts +148 -0
- package/src/vectorstore/index.ts +53 -0
- package/src/vectorstore/types.ts +149 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ingest/IngestionPipeline.ts — source → parse → chunk → embed → upsert.
|
|
3
|
+
*
|
|
4
|
+
* The one-off script that loads a corpus into a vector database is easy. What
|
|
5
|
+
* makes a pipeline PRODUCTION-grade is everything that happens on the second run:
|
|
6
|
+
*
|
|
7
|
+
* • **Incremental sync.** Re-ingesting a 200k-document corpus must not re-embed
|
|
8
|
+
* 200k documents because three of them changed. Chunks carry a content hash;
|
|
9
|
+
* with a manifest, unchanged hashes are skipped and vanished chunks deleted.
|
|
10
|
+
* Embedding is the cost line, so this is the difference between a pipeline that
|
|
11
|
+
* runs nightly and one that runs once.
|
|
12
|
+
* • **Deterministic ids.** A chunk's id is derived from its source id and its
|
|
13
|
+
* ordinal, so a re-run overwrites rather than duplicating. Random ids are why
|
|
14
|
+
* corpora silently triple in size.
|
|
15
|
+
* • **Batching and bounded concurrency.** Embedders and vector databases both
|
|
16
|
+
* rate-limit; an unbounded `Promise.all` over a corpus is a self-inflicted 429.
|
|
17
|
+
* • **Per-document failure isolation.** One malformed file fails that document
|
|
18
|
+
* and is reported, rather than aborting the run.
|
|
19
|
+
* • **Governance by construction.** Tenant, ACL and sensitivity are written onto
|
|
20
|
+
* every chunk during ingest, because a retrieval-time ACL that depends on
|
|
21
|
+
* ingest-time diligence is not an ACL.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { chunkText, type ChunkOptions } from '../retrieval/chunk.js';
|
|
25
|
+
import type { Tracer } from '../telemetry/Tracer.js';
|
|
26
|
+
import { estimateTokens } from '../telemetry/pricing.js';
|
|
27
|
+
import {
|
|
28
|
+
ACL_FIELD,
|
|
29
|
+
ACL_PUBLIC,
|
|
30
|
+
SENSITIVITY_FIELD,
|
|
31
|
+
TENANT_FIELD,
|
|
32
|
+
type MetadataRecord,
|
|
33
|
+
type VectorRecord,
|
|
34
|
+
type VectorStore,
|
|
35
|
+
} from '../vectorstore/types.js';
|
|
36
|
+
import { ParserRegistry } from './parsers.js';
|
|
37
|
+
import {
|
|
38
|
+
CHUNK_INDEX_FIELD,
|
|
39
|
+
CONTENT_HASH_FIELD,
|
|
40
|
+
PARSER_FIELD,
|
|
41
|
+
SOURCE_ID_FIELD,
|
|
42
|
+
TITLE_FIELD,
|
|
43
|
+
URI_FIELD,
|
|
44
|
+
type BatchEmbedder,
|
|
45
|
+
type ChunkFingerprint,
|
|
46
|
+
type DocumentParser,
|
|
47
|
+
type IngestManifest,
|
|
48
|
+
type IngestionReport,
|
|
49
|
+
type SourceDocument,
|
|
50
|
+
type SyncStrategy,
|
|
51
|
+
} from './types.js';
|
|
52
|
+
|
|
53
|
+
export interface IngestionPipelineOptions {
|
|
54
|
+
store: VectorStore;
|
|
55
|
+
embed: BatchEmbedder;
|
|
56
|
+
/** Chunking parameters. Defaults to 1000 chars / 200 overlap. */
|
|
57
|
+
chunk?: ChunkOptions;
|
|
58
|
+
/** Custom parser set. Defaults to the builtin registry. */
|
|
59
|
+
parsers?: ParserRegistry | readonly DocumentParser[];
|
|
60
|
+
/** Enables `diff` sync — see {@link IngestManifest}. */
|
|
61
|
+
manifest?: IngestManifest;
|
|
62
|
+
/** Default `'diff'` when a manifest is supplied, `'replace'` otherwise. */
|
|
63
|
+
syncStrategy?: SyncStrategy;
|
|
64
|
+
/** Texts per embedder call. Default 64. */
|
|
65
|
+
embedBatchSize?: number;
|
|
66
|
+
/** Records per store upsert. Default 100. */
|
|
67
|
+
upsertBatchSize?: number;
|
|
68
|
+
/** Documents processed concurrently. Default 4. */
|
|
69
|
+
concurrency?: number;
|
|
70
|
+
/** Model label recorded on embedding spans, for cost attribution. */
|
|
71
|
+
embeddingModel?: string;
|
|
72
|
+
tracer?: Tracer;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class IngestionPipeline {
|
|
76
|
+
private readonly _store: VectorStore;
|
|
77
|
+
private readonly _embed: BatchEmbedder;
|
|
78
|
+
private readonly _chunkOpts: ChunkOptions;
|
|
79
|
+
private readonly _parsers: ParserRegistry;
|
|
80
|
+
private readonly _manifest: IngestManifest | undefined;
|
|
81
|
+
private readonly _strategy: SyncStrategy;
|
|
82
|
+
private readonly _embedBatch: number;
|
|
83
|
+
private readonly _upsertBatch: number;
|
|
84
|
+
private readonly _concurrency: number;
|
|
85
|
+
private readonly _embeddingModel: string;
|
|
86
|
+
private readonly _tracer: Tracer | undefined;
|
|
87
|
+
|
|
88
|
+
constructor(opts: IngestionPipelineOptions) {
|
|
89
|
+
this._store = opts.store;
|
|
90
|
+
this._embed = opts.embed;
|
|
91
|
+
this._chunkOpts = opts.chunk ?? {};
|
|
92
|
+
this._parsers = opts.parsers instanceof ParserRegistry
|
|
93
|
+
? opts.parsers
|
|
94
|
+
: new ParserRegistry(opts.parsers);
|
|
95
|
+
this._manifest = opts.manifest;
|
|
96
|
+
this._strategy = opts.syncStrategy ?? (opts.manifest ? 'diff' : 'replace');
|
|
97
|
+
this._embedBatch = Math.max(1, opts.embedBatchSize ?? 64);
|
|
98
|
+
this._upsertBatch = Math.max(1, opts.upsertBatchSize ?? 100);
|
|
99
|
+
this._concurrency = Math.max(1, opts.concurrency ?? 4);
|
|
100
|
+
this._embeddingModel = opts.embeddingModel ?? 'embedding';
|
|
101
|
+
this._tracer = opts.tracer;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async ingest(sources: readonly SourceDocument[]): Promise<IngestionReport> {
|
|
105
|
+
const started = Date.now();
|
|
106
|
+
const report: IngestionReport = {
|
|
107
|
+
documents: 0, sections: 0, chunks: 0, embedded: 0, upserted: 0,
|
|
108
|
+
skippedUnchanged: 0, deletedStale: 0, failures: [], durationMs: 0,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const root = this._tracer?.startSpan('ingest.run', {
|
|
112
|
+
kind: 'ingest',
|
|
113
|
+
attributes: { 'ingest.sources': sources.length, 'ingest.strategy': this._strategy },
|
|
114
|
+
});
|
|
115
|
+
if (root) report.traceId = root.traceId;
|
|
116
|
+
|
|
117
|
+
await mapWithConcurrency(sources, this._concurrency, async (source) => {
|
|
118
|
+
try {
|
|
119
|
+
const result = await this._ingestOne(source, root);
|
|
120
|
+
report.documents += 1;
|
|
121
|
+
report.sections += result.sections;
|
|
122
|
+
report.chunks += result.chunks;
|
|
123
|
+
report.embedded += result.embedded;
|
|
124
|
+
report.upserted += result.upserted;
|
|
125
|
+
report.skippedUnchanged += result.skippedUnchanged;
|
|
126
|
+
report.deletedStale += result.deletedStale;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
// One bad document must not abort a corpus-wide run; it is reported
|
|
129
|
+
// with its id so the operator can requeue exactly that source.
|
|
130
|
+
report.failures.push({
|
|
131
|
+
sourceId: source.id,
|
|
132
|
+
error: err instanceof Error ? err.message : String(err),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
report.durationMs = Date.now() - started;
|
|
138
|
+
root?.setAttributes({
|
|
139
|
+
'ingest.chunks': report.chunks,
|
|
140
|
+
'ingest.embedded': report.embedded,
|
|
141
|
+
'ingest.skipped_unchanged': report.skippedUnchanged,
|
|
142
|
+
'ingest.failures': report.failures.length,
|
|
143
|
+
});
|
|
144
|
+
root?.end(report.failures.length > 0 && report.documents === 0 ? 'error' : 'ok');
|
|
145
|
+
await this._tracer?.flush();
|
|
146
|
+
return report;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Removes every chunk a source produced — a GDPR/right-to-erasure primitive. */
|
|
150
|
+
async forget(sourceId: string): Promise<number> {
|
|
151
|
+
const deleted = await this._store.deleteByFilter({
|
|
152
|
+
op: 'eq', field: SOURCE_ID_FIELD, value: sourceId,
|
|
153
|
+
});
|
|
154
|
+
await this._manifest?.delete(sourceId);
|
|
155
|
+
return deleted;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async _ingestOne(
|
|
159
|
+
source: SourceDocument,
|
|
160
|
+
parent?: import('../telemetry/Tracer.js').Span,
|
|
161
|
+
): Promise<{
|
|
162
|
+
sections: number; chunks: number; embedded: number;
|
|
163
|
+
upserted: number; skippedUnchanged: number; deletedStale: number;
|
|
164
|
+
}> {
|
|
165
|
+
const span = this._tracer?.startSpan('ingest.document', {
|
|
166
|
+
kind: 'ingest',
|
|
167
|
+
...(parent ? { parent } : {}),
|
|
168
|
+
attributes: { 'ingest.source_id': source.id, 'ingest.tenant': source.tenantId },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const parser = this._parsers.resolve(source);
|
|
173
|
+
if (!parser) throw new Error(`No parser accepts source "${source.id}".`);
|
|
174
|
+
|
|
175
|
+
const sections = parser.parse(source);
|
|
176
|
+
const base = this._baseMetadata(source, parser.id);
|
|
177
|
+
|
|
178
|
+
// Chunk index is global across sections so ids stay stable and unique
|
|
179
|
+
// even when a section is added in the middle of a document.
|
|
180
|
+
const pending: Array<{ record: VectorRecord; hash: string }> = [];
|
|
181
|
+
let index = 0;
|
|
182
|
+
for (const section of sections) {
|
|
183
|
+
for (const chunk of chunkText(section.text, this._chunkOpts)) {
|
|
184
|
+
const hash = hashText(chunk.text);
|
|
185
|
+
const metadata: MetadataRecord = {
|
|
186
|
+
...base,
|
|
187
|
+
...(section.metadata ?? {}),
|
|
188
|
+
[CHUNK_INDEX_FIELD]: index,
|
|
189
|
+
[CONTENT_HASH_FIELD]: hash,
|
|
190
|
+
};
|
|
191
|
+
pending.push({
|
|
192
|
+
record: { id: `${source.id}#${index}`, text: chunk.text, metadata },
|
|
193
|
+
hash,
|
|
194
|
+
});
|
|
195
|
+
index += 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { toWrite, toDelete, skipped } = await this._planSync(source.id, pending);
|
|
200
|
+
|
|
201
|
+
let embedded = 0;
|
|
202
|
+
for (let i = 0; i < toWrite.length; i += this._embedBatch) {
|
|
203
|
+
const batch = toWrite.slice(i, i + this._embedBatch);
|
|
204
|
+
const vectors = await this._embedBatchTraced(batch.map((r) => r.text), span);
|
|
205
|
+
batch.forEach((record, j) => {
|
|
206
|
+
const vector = vectors[j];
|
|
207
|
+
if (vector) record.vector = vector;
|
|
208
|
+
});
|
|
209
|
+
embedded += batch.length;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let upserted = 0;
|
|
213
|
+
for (let i = 0; i < toWrite.length; i += this._upsertBatch) {
|
|
214
|
+
const result = await this._store.upsert(toWrite.slice(i, i + this._upsertBatch));
|
|
215
|
+
upserted += result.upserted;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let deletedStale = 0;
|
|
219
|
+
if (toDelete.length > 0) {
|
|
220
|
+
await this._store.delete(toDelete);
|
|
221
|
+
deletedStale = toDelete.length;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await this._manifest?.set(
|
|
225
|
+
source.id,
|
|
226
|
+
pending.map(({ record, hash }): ChunkFingerprint => ({ id: record.id, hash })),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
span?.setAttributes({
|
|
230
|
+
'ingest.sections': sections.length,
|
|
231
|
+
'ingest.chunks': pending.length,
|
|
232
|
+
'ingest.embedded': embedded,
|
|
233
|
+
'ingest.skipped_unchanged': skipped,
|
|
234
|
+
'ingest.parser': parser.id,
|
|
235
|
+
});
|
|
236
|
+
span?.end('ok');
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
sections: sections.length,
|
|
240
|
+
chunks: pending.length,
|
|
241
|
+
embedded,
|
|
242
|
+
upserted,
|
|
243
|
+
skippedUnchanged: skipped,
|
|
244
|
+
deletedStale,
|
|
245
|
+
};
|
|
246
|
+
} catch (err) {
|
|
247
|
+
span?.fail(err);
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Decides what actually has to be written and deleted.
|
|
254
|
+
*
|
|
255
|
+
* `diff` compares content hashes against the manifest; `replace` clears the
|
|
256
|
+
* source's chunks first (correct without a manifest, at the cost of re-embedding);
|
|
257
|
+
* `append` writes everything and deletes nothing.
|
|
258
|
+
*/
|
|
259
|
+
private async _planSync(
|
|
260
|
+
sourceId: string,
|
|
261
|
+
pending: Array<{ record: VectorRecord; hash: string }>,
|
|
262
|
+
): Promise<{ toWrite: VectorRecord[]; toDelete: string[]; skipped: number }> {
|
|
263
|
+
if (this._strategy === 'diff' && this._manifest) {
|
|
264
|
+
const previous = (await this._manifest.get(sourceId)) ?? [];
|
|
265
|
+
const previousByHash = new Map(previous.map((f) => [f.id, f.hash]));
|
|
266
|
+
const currentIds = new Set(pending.map((p) => p.record.id));
|
|
267
|
+
|
|
268
|
+
const toWrite: VectorRecord[] = [];
|
|
269
|
+
let skipped = 0;
|
|
270
|
+
for (const { record, hash } of pending) {
|
|
271
|
+
if (previousByHash.get(record.id) === hash) skipped += 1;
|
|
272
|
+
else toWrite.push(record);
|
|
273
|
+
}
|
|
274
|
+
const toDelete = previous.map((f) => f.id).filter((id) => !currentIds.has(id));
|
|
275
|
+
return { toWrite, toDelete, skipped };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (this._strategy === 'replace') {
|
|
279
|
+
await this._store.deleteByFilter({ op: 'eq', field: SOURCE_ID_FIELD, value: sourceId });
|
|
280
|
+
}
|
|
281
|
+
return { toWrite: pending.map((p) => p.record), toDelete: [], skipped: 0 };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private async _embedBatchTraced(
|
|
285
|
+
texts: string[],
|
|
286
|
+
parent?: import('../telemetry/Tracer.js').Span,
|
|
287
|
+
): Promise<Float32Array[]> {
|
|
288
|
+
if (!this._tracer) return this._embed(texts);
|
|
289
|
+
|
|
290
|
+
const span = this._tracer.startSpan('embed.batch', {
|
|
291
|
+
kind: 'embedding',
|
|
292
|
+
...(parent ? { parent } : {}),
|
|
293
|
+
attributes: { 'embedding.batch_size': texts.length },
|
|
294
|
+
});
|
|
295
|
+
try {
|
|
296
|
+
const vectors = await this._embed(texts);
|
|
297
|
+
span.recordUsage({
|
|
298
|
+
model: this._embeddingModel,
|
|
299
|
+
inputTokens: texts.reduce((sum, t) => sum + estimateTokens(t), 0),
|
|
300
|
+
outputTokens: 0,
|
|
301
|
+
estimated: true,
|
|
302
|
+
});
|
|
303
|
+
span.end('ok');
|
|
304
|
+
return vectors;
|
|
305
|
+
} catch (err) {
|
|
306
|
+
span.fail(err);
|
|
307
|
+
throw err;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private _baseMetadata(source: SourceDocument, parserId: string): MetadataRecord {
|
|
312
|
+
const metadata: MetadataRecord = {
|
|
313
|
+
...(source.metadata ?? {}),
|
|
314
|
+
[TENANT_FIELD]: source.tenantId,
|
|
315
|
+
// Defaulting to tenant-public keeps the ACL array NON-EMPTY, which is
|
|
316
|
+
// what makes `accessFilter` fail closed: a record with no acl matches
|
|
317
|
+
// nothing rather than everything.
|
|
318
|
+
[ACL_FIELD]: source.acl?.length ? source.acl : [ACL_PUBLIC],
|
|
319
|
+
[SOURCE_ID_FIELD]: source.id,
|
|
320
|
+
[PARSER_FIELD]: parserId,
|
|
321
|
+
};
|
|
322
|
+
if (source.sensitivity !== undefined) metadata[SENSITIVITY_FIELD] = source.sensitivity;
|
|
323
|
+
if (source.title) metadata[TITLE_FIELD] = source.title;
|
|
324
|
+
if (source.uri) metadata[URI_FIELD] = source.uri;
|
|
325
|
+
return metadata;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** In-process manifest — the default for a single-process or test deployment. */
|
|
330
|
+
export class InMemoryIngestManifest implements IngestManifest {
|
|
331
|
+
private readonly _entries = new Map<string, ChunkFingerprint[]>();
|
|
332
|
+
|
|
333
|
+
async get(sourceId: string): Promise<ChunkFingerprint[] | undefined> {
|
|
334
|
+
return this._entries.get(sourceId);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async set(sourceId: string, fingerprints: ChunkFingerprint[]): Promise<void> {
|
|
338
|
+
this._entries.set(sourceId, fingerprints);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async delete(sourceId: string): Promise<void> {
|
|
342
|
+
this._entries.delete(sourceId);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
get size(): number { return this._entries.size; }
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* FNV-1a, 32-bit, rendered as hex.
|
|
350
|
+
*
|
|
351
|
+
* A content hash here answers "did this chunk change", not "was this chunk
|
|
352
|
+
* tampered with", so a fast non-cryptographic hash is the right tool — and it is
|
|
353
|
+
* synchronous, unlike WebCrypto, which keeps chunking a pure function.
|
|
354
|
+
*/
|
|
355
|
+
export function hashText(text: string): string {
|
|
356
|
+
let hash = 0x811c9dc5;
|
|
357
|
+
for (let i = 0; i < text.length; i++) {
|
|
358
|
+
hash ^= text.charCodeAt(i);
|
|
359
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
360
|
+
}
|
|
361
|
+
return hash.toString(16).padStart(8, '0');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Bounded-concurrency map — the reason a large corpus does not become a 429. */
|
|
365
|
+
export async function mapWithConcurrency<T>(
|
|
366
|
+
items: readonly T[],
|
|
367
|
+
limit: number,
|
|
368
|
+
fn: (item: T, index: number) => Promise<void>,
|
|
369
|
+
): Promise<void> {
|
|
370
|
+
let cursor = 0;
|
|
371
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
372
|
+
while (cursor < items.length) {
|
|
373
|
+
const index = cursor++;
|
|
374
|
+
await fn(items[index] as T, index);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
await Promise.all(workers);
|
|
378
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ingest — one pipeline for structured and unstructured sources, with incremental
|
|
3
|
+
* sync so a nightly re-run re-embeds only what changed.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
IngestionPipeline,
|
|
8
|
+
InMemoryIngestManifest,
|
|
9
|
+
hashText,
|
|
10
|
+
mapWithConcurrency,
|
|
11
|
+
} from './IngestionPipeline.js';
|
|
12
|
+
export type { IngestionPipelineOptions } from './IngestionPipeline.js';
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
ParserRegistry,
|
|
16
|
+
BUILTIN_PARSERS,
|
|
17
|
+
rowsParser,
|
|
18
|
+
csvParser,
|
|
19
|
+
jsonParser,
|
|
20
|
+
markdownParser,
|
|
21
|
+
htmlParser,
|
|
22
|
+
plainTextParser,
|
|
23
|
+
parseCsv,
|
|
24
|
+
serializeRow,
|
|
25
|
+
} from './parsers.js';
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
SOURCE_ID_FIELD,
|
|
29
|
+
CHUNK_INDEX_FIELD,
|
|
30
|
+
CONTENT_HASH_FIELD,
|
|
31
|
+
TITLE_FIELD,
|
|
32
|
+
URI_FIELD,
|
|
33
|
+
PARSER_FIELD,
|
|
34
|
+
} from './types.js';
|
|
35
|
+
export type {
|
|
36
|
+
BatchEmbedder,
|
|
37
|
+
ChunkFingerprint,
|
|
38
|
+
DocumentParser,
|
|
39
|
+
IngestManifest,
|
|
40
|
+
IngestionReport,
|
|
41
|
+
ParsedSection,
|
|
42
|
+
SourceContent,
|
|
43
|
+
SourceDocument,
|
|
44
|
+
StructuredRow,
|
|
45
|
+
SyncStrategy,
|
|
46
|
+
} from './types.js';
|