unrag 0.2.2 → 0.2.4

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.
Files changed (31) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/index.js +408 -50
  3. package/package.json +3 -1
  4. package/registry/config/unrag.config.ts +164 -7
  5. package/registry/connectors/notion/render.ts +78 -0
  6. package/registry/connectors/notion/sync.ts +12 -3
  7. package/registry/connectors/notion/types.ts +3 -1
  8. package/registry/core/assets.ts +54 -0
  9. package/registry/core/config.ts +150 -0
  10. package/registry/core/context-engine.ts +69 -1
  11. package/registry/core/index.ts +15 -2
  12. package/registry/core/ingest.ts +743 -17
  13. package/registry/core/types.ts +606 -0
  14. package/registry/docs/unrag.md +6 -0
  15. package/registry/embedding/ai.ts +89 -8
  16. package/registry/extractors/_shared/fetch.ts +113 -0
  17. package/registry/extractors/_shared/media.ts +14 -0
  18. package/registry/extractors/_shared/text.ts +11 -0
  19. package/registry/extractors/audio-transcribe/index.ts +75 -0
  20. package/registry/extractors/file-docx/index.ts +53 -0
  21. package/registry/extractors/file-pptx/index.ts +92 -0
  22. package/registry/extractors/file-text/index.ts +85 -0
  23. package/registry/extractors/file-xlsx/index.ts +58 -0
  24. package/registry/extractors/image-caption-llm/index.ts +60 -0
  25. package/registry/extractors/image-ocr/index.ts +60 -0
  26. package/registry/extractors/pdf-llm/index.ts +84 -0
  27. package/registry/extractors/pdf-ocr/index.ts +125 -0
  28. package/registry/extractors/pdf-text-layer/index.ts +76 -0
  29. package/registry/extractors/video-frames/index.ts +126 -0
  30. package/registry/extractors/video-transcribe/index.ts +78 -0
  31. package/registry/store/drizzle-postgres-pgvector/store.ts +1 -1
@@ -17,6 +17,20 @@ export type Chunk = {
17
17
  documentContent?: string;
18
18
  };
19
19
 
20
+ /**
21
+ * Controls what text Unrag persists to the backing store.
22
+ *
23
+ * - `storeChunkContent`: whether to persist `chunks.content` (what you get back as `chunk.content` in retrieval).
24
+ * - `storeDocumentContent`: whether to persist the full original document text (`documents.content`).
25
+ *
26
+ * Disabling these can be useful for privacy/compliance or when you have an external
27
+ * content store and want Unrag to keep only embeddings + identifiers/metadata.
28
+ */
29
+ export type ContentStorageConfig = {
30
+ storeChunkContent: boolean;
31
+ storeDocumentContent: boolean;
32
+ };
33
+
20
34
  export type ChunkText = {
21
35
  index: number;
22
36
  content: string;
@@ -30,6 +44,429 @@ export type ChunkingOptions = {
30
44
 
31
45
  export type Chunker = (content: string, options: ChunkingOptions) => ChunkText[];
32
46
 
47
+ /**
48
+ * Data reference for an ingested asset.
49
+ *
50
+ * Prefer `bytes` when possible (most reliable). URLs are convenient for connectors
51
+ * but require network fetch at ingest time (see assetProcessing.fetch safety settings).
52
+ */
53
+ export type AssetData =
54
+ | {
55
+ kind: "url";
56
+ /** HTTPS URL to fetch the bytes from. */
57
+ url: string;
58
+ /** Optional request headers (e.g. signed URLs). */
59
+ headers?: Record<string, string>;
60
+ /**
61
+ * Optional media type hint.
62
+ * Useful when the URL doesn't have a stable extension (e.g. signed URLs).
63
+ */
64
+ mediaType?: string;
65
+ /** Optional filename hint. */
66
+ filename?: string;
67
+ }
68
+ | {
69
+ kind: "bytes";
70
+ /** Raw bytes of the asset. */
71
+ bytes: Uint8Array;
72
+ /** IANA media type (e.g. "application/pdf", "image/png"). */
73
+ mediaType: string;
74
+ /** Optional filename hint. */
75
+ filename?: string;
76
+ };
77
+
78
+ export type AssetKind = "image" | "pdf" | "audio" | "video" | "file";
79
+
80
+ /**
81
+ * Non-text input attached to an ingested document.
82
+ *
83
+ * Connectors should emit stable `assetId`s (e.g. Notion block id) so downstream
84
+ * systems can associate chunks back to their originating rich media.
85
+ */
86
+ export type AssetInput = {
87
+ /** Stable identifier within the document/source (e.g. block id). */
88
+ assetId: string;
89
+ kind: AssetKind;
90
+ data: AssetData;
91
+ /**
92
+ * Optional stable-ish URI for debugging/display (may be the same as data.url).
93
+ * This value is stored in metadata; do not assume it will be fetchable later.
94
+ */
95
+ uri?: string;
96
+ /**
97
+ * Optional text already known for the asset (caption/alt text).
98
+ * This can be embedded as normal text chunks and can also be passed to extractors.
99
+ */
100
+ text?: string;
101
+ /** Optional per-asset metadata (merged into chunk metadata). */
102
+ metadata?: Metadata;
103
+ };
104
+
105
+ export type AssetPolicy = "skip" | "fail";
106
+
107
+ export type AssetFetchConfig = {
108
+ /**
109
+ * When true, the engine may fetch asset bytes from URLs during ingest.
110
+ * Disable in high-security environments; provide `bytes` instead.
111
+ */
112
+ enabled: boolean;
113
+ /**
114
+ * Optional allowlist of hostnames. When set, only these hosts can be fetched.
115
+ * Recommended to mitigate SSRF.
116
+ */
117
+ allowedHosts?: string[];
118
+ /** Hard cap on fetched bytes. */
119
+ maxBytes: number;
120
+ /** Fetch timeout in milliseconds. */
121
+ timeoutMs: number;
122
+ /** Extra headers to attach to all fetches (merged with per-asset headers). */
123
+ headers?: Record<string, string>;
124
+ };
125
+
126
+ export type PdfLlmExtractionConfig = {
127
+ /**
128
+ * When enabled, PDFs are sent to an LLM to extract text, which is then chunked
129
+ * and embedded as normal text.
130
+ *
131
+ * Library default: false (cost-safe).
132
+ * Generated config template may set this to true for convenience.
133
+ */
134
+ enabled: boolean;
135
+ /**
136
+ * AI Gateway model id (Vercel AI SDK), e.g. "google/gemini-2.0-flash".
137
+ * This must be a model that supports file inputs for PDF extraction.
138
+ */
139
+ model: string;
140
+ /**
141
+ * Prompt used for extraction. Keep it deterministic: \"extract faithfully\".
142
+ * The output is later chunked and embedded.
143
+ */
144
+ prompt: string;
145
+ /** LLM call timeout in milliseconds. */
146
+ timeoutMs: number;
147
+ /** Hard cap on input PDF bytes. */
148
+ maxBytes: number;
149
+ /** Hard cap on extracted text length (characters). */
150
+ maxOutputChars: number;
151
+ };
152
+
153
+ export type PdfTextLayerConfig = {
154
+ /**
155
+ * When enabled, PDFs are processed by extracting the built-in text layer (when present).
156
+ * This is fast/cheap but won't work well for scanned/image-only PDFs.
157
+ */
158
+ enabled: boolean;
159
+ /** Max PDF bytes to attempt text-layer extraction on. */
160
+ maxBytes: number;
161
+ /** Hard cap on extracted text length (characters). */
162
+ maxOutputChars: number;
163
+ /**
164
+ * Minimum extracted characters required to accept the result. If fewer chars are extracted,
165
+ * the extractor should return empty output so the pipeline can fall back to another extractor.
166
+ */
167
+ minChars: number;
168
+ /**
169
+ * Optional cap on pages to read (defense-in-depth for huge PDFs).
170
+ * Extractors may ignore this when they can't reliably compute page count.
171
+ */
172
+ maxPages?: number;
173
+ };
174
+
175
+ export type PdfOcrConfig = {
176
+ /**
177
+ * When enabled, PDFs are rendered to images and OCR'd.
178
+ * This is typically worker-only (needs binaries like poppler/tesseract or external services).
179
+ */
180
+ enabled: boolean;
181
+ /** Max PDF bytes to attempt OCR on. */
182
+ maxBytes: number;
183
+ /** Hard cap on extracted text length (characters). */
184
+ maxOutputChars: number;
185
+ /** Minimum extracted characters required to accept the OCR output. */
186
+ minChars: number;
187
+ /** Optional max pages to OCR (defense-in-depth). */
188
+ maxPages?: number;
189
+ /** Optional path to `pdftoppm` (Poppler). */
190
+ pdftoppmPath?: string;
191
+ /** Optional path to `tesseract`. */
192
+ tesseractPath?: string;
193
+ /** DPI for rasterization (higher = better OCR, slower/larger). */
194
+ dpi?: number;
195
+ /** Tesseract language code (e.g. "eng"). */
196
+ lang?: string;
197
+ };
198
+
199
+ export type ImageOcrConfig = {
200
+ /** When enabled, images can be OCR'd into text chunks. */
201
+ enabled: boolean;
202
+ /** Model id (AI Gateway) for vision OCR. */
203
+ model: string;
204
+ /** Prompt used for deterministic OCR extraction. */
205
+ prompt: string;
206
+ timeoutMs: number;
207
+ /** Hard cap on input bytes (enforced by fetch + extractor). */
208
+ maxBytes: number;
209
+ /** Hard cap on extracted text length (characters). */
210
+ maxOutputChars: number;
211
+ };
212
+
213
+ export type ImageCaptionLlmConfig = {
214
+ /** When enabled, images can have captions generated via a vision-capable LLM. */
215
+ enabled: boolean;
216
+ model: string;
217
+ prompt: string;
218
+ timeoutMs: number;
219
+ maxBytes: number;
220
+ maxOutputChars: number;
221
+ };
222
+
223
+ export type AudioTranscriptionConfig = {
224
+ /** When enabled, audio assets can be transcribed into text chunks. */
225
+ enabled: boolean;
226
+ /** Provider/model id (AI Gateway) for transcription. */
227
+ model: string;
228
+ timeoutMs: number;
229
+ maxBytes: number;
230
+ };
231
+
232
+ export type VideoTranscriptionConfig = {
233
+ /** When enabled, video assets can be transcribed (audio track) into text chunks. */
234
+ enabled: boolean;
235
+ model: string;
236
+ timeoutMs: number;
237
+ maxBytes: number;
238
+ };
239
+
240
+ export type VideoFramesConfig = {
241
+ /**
242
+ * When enabled, video frames can be sampled and processed (OCR/caption).
243
+ * This is typically worker-only (requires ffmpeg and significant runtime).
244
+ */
245
+ enabled: boolean;
246
+ sampleFps: number;
247
+ maxFrames: number;
248
+ /** Optional path to ffmpeg binary (worker environments). */
249
+ ffmpegPath?: string;
250
+ /** Hard cap on video bytes for frame sampling. */
251
+ maxBytes: number;
252
+ /** Vision-capable model id (AI Gateway) for per-frame processing. */
253
+ model: string;
254
+ /** Prompt to apply to each sampled frame. */
255
+ prompt: string;
256
+ /** Timeout per frame analysis call. */
257
+ timeoutMs: number;
258
+ /** Hard cap on total extracted text length (characters). */
259
+ maxOutputChars: number;
260
+ };
261
+
262
+ export type FileTextConfig = {
263
+ /** When enabled, text-ish files (txt/md/html) can be extracted into chunks. */
264
+ enabled: boolean;
265
+ maxBytes: number;
266
+ maxOutputChars: number;
267
+ minChars: number;
268
+ };
269
+
270
+ export type FileDocxConfig = {
271
+ enabled: boolean;
272
+ maxBytes: number;
273
+ maxOutputChars: number;
274
+ minChars: number;
275
+ };
276
+
277
+ export type FilePptxConfig = {
278
+ enabled: boolean;
279
+ maxBytes: number;
280
+ maxOutputChars: number;
281
+ minChars: number;
282
+ };
283
+
284
+ export type FileXlsxConfig = {
285
+ enabled: boolean;
286
+ maxBytes: number;
287
+ maxOutputChars: number;
288
+ minChars: number;
289
+ };
290
+
291
+ export type AssetProcessingConfig = {
292
+ /**
293
+ * What to do when an asset kind is present but unsupported (e.g. audio in v1).
294
+ * Recommended default: \"skip\".
295
+ */
296
+ onUnsupportedAsset: AssetPolicy;
297
+ /** What to do when processing an asset fails (fetch/LLM errors). */
298
+ onError: AssetPolicy;
299
+ /**
300
+ * Bounded concurrency for asset processing (extraction + any I/O).
301
+ * This does not affect text chunking/embedding batching.
302
+ */
303
+ concurrency: number;
304
+ /**
305
+ * Optional hooks for observability (structured events).
306
+ * Prefer this over ad-hoc logging inside extractors.
307
+ */
308
+ hooks?: {
309
+ onEvent?: (event: AssetProcessingEvent) => void;
310
+ };
311
+ /** Network fetch settings for URL-based assets. */
312
+ fetch: AssetFetchConfig;
313
+ pdf: {
314
+ textLayer: PdfTextLayerConfig;
315
+ llmExtraction: PdfLlmExtractionConfig;
316
+ ocr: PdfOcrConfig;
317
+ };
318
+ image: {
319
+ ocr: ImageOcrConfig;
320
+ captionLlm: ImageCaptionLlmConfig;
321
+ };
322
+ audio: {
323
+ transcription: AudioTranscriptionConfig;
324
+ };
325
+ video: {
326
+ transcription: VideoTranscriptionConfig;
327
+ frames: VideoFramesConfig;
328
+ };
329
+ file: {
330
+ text: FileTextConfig;
331
+ docx: FileDocxConfig;
332
+ pptx: FilePptxConfig;
333
+ xlsx: FileXlsxConfig;
334
+ };
335
+ };
336
+
337
+ export type AssetProcessingEvent =
338
+ | {
339
+ type: "asset:start";
340
+ sourceId: string;
341
+ documentId: string;
342
+ assetId: string;
343
+ assetKind: AssetKind;
344
+ assetUri?: string;
345
+ assetMediaType?: string;
346
+ }
347
+ | ({
348
+ type: "asset:skipped";
349
+ sourceId: string;
350
+ documentId: string;
351
+ } & IngestWarning)
352
+ | {
353
+ type: "extractor:start";
354
+ sourceId: string;
355
+ documentId: string;
356
+ assetId: string;
357
+ assetKind: AssetKind;
358
+ extractor: string;
359
+ }
360
+ | {
361
+ type: "extractor:success";
362
+ sourceId: string;
363
+ documentId: string;
364
+ assetId: string;
365
+ assetKind: AssetKind;
366
+ extractor: string;
367
+ durationMs: number;
368
+ textItemCount: number;
369
+ }
370
+ | {
371
+ type: "extractor:error";
372
+ sourceId: string;
373
+ documentId: string;
374
+ assetId: string;
375
+ assetKind: AssetKind;
376
+ extractor: string;
377
+ durationMs: number;
378
+ errorMessage: string;
379
+ };
380
+
381
+ export type ExtractedTextItem = {
382
+ /**
383
+ * A label describing the extraction output (e.g. \"fulltext\", \"ocr\", \"transcript\").
384
+ * Used only for metadata/debugging.
385
+ */
386
+ label: string;
387
+ /** Extracted text content. This will be chunked and embedded as normal text. */
388
+ content: string;
389
+ confidence?: number;
390
+ /**
391
+ * Optional range metadata produced by the extractor.
392
+ * This is stored in chunk metadata (if provided) for traceability.
393
+ */
394
+ pageRange?: [number, number];
395
+ timeRangeSec?: [number, number];
396
+ };
397
+
398
+ export type AssetExtractorResult = {
399
+ texts: ExtractedTextItem[];
400
+ /**
401
+ * Optional structured skip reason. Prefer returning `texts: []` + `skipped` when the
402
+ * extractor is configured off or cannot operate under current limits, without treating
403
+ * it as an error (so the pipeline can fall back to other extractors).
404
+ */
405
+ skipped?: {
406
+ code: string;
407
+ message: string;
408
+ };
409
+ /**
410
+ * Extractor-produced metadata merged into chunk metadata.
411
+ * Useful for things like detected language, page count, etc.
412
+ */
413
+ metadata?: Metadata;
414
+ diagnostics?: {
415
+ model?: string;
416
+ tokens?: number;
417
+ seconds?: number;
418
+ };
419
+ };
420
+
421
+ export type AssetExtractorContext = {
422
+ sourceId: string;
423
+ documentId: string;
424
+ documentMetadata: Metadata;
425
+ /** Engine-resolved asset processing config (defaults + overrides). */
426
+ assetProcessing: AssetProcessingConfig;
427
+ };
428
+
429
+ export type AssetExtractor = {
430
+ /** Stable name used in metadata and routing (e.g. \"pdf:llm\"). */
431
+ name: string;
432
+ /** Whether this extractor can handle a given asset input. */
433
+ supports: (args: { asset: AssetInput; ctx: AssetExtractorContext }) => boolean;
434
+ /** Extract text outputs from the asset. */
435
+ extract: (args: {
436
+ asset: AssetInput;
437
+ ctx: AssetExtractorContext;
438
+ }) => Promise<AssetExtractorResult>;
439
+ };
440
+
441
+ export type AssetProcessingPlanItem =
442
+ | ({
443
+ status: "will_process";
444
+ extractors: string[];
445
+ } & Pick<AssetInput, "assetId" | "kind" | "uri">)
446
+ | ({
447
+ status: "will_skip";
448
+ reason: IngestWarning["code"];
449
+ } & Pick<AssetInput, "assetId" | "kind" | "uri">);
450
+
451
+ export type IngestPlanResult = {
452
+ documentId: string;
453
+ sourceId: string;
454
+ assets: AssetProcessingPlanItem[];
455
+ warnings: IngestWarning[];
456
+ };
457
+
458
+ /**
459
+ * Deep partial for ergonomic overrides.
460
+ * Used for engine defaults and per-ingest overrides.
461
+ */
462
+ export type DeepPartial<T> = {
463
+ [K in keyof T]?: T[K] extends Array<infer U>
464
+ ? Array<U>
465
+ : T[K] extends object
466
+ ? DeepPartial<T[K]>
467
+ : T[K];
468
+ };
469
+
33
470
  export type EmbeddingInput = {
34
471
  text: string;
35
472
  metadata: Metadata;
@@ -38,10 +475,32 @@ export type EmbeddingInput = {
38
475
  documentId: string;
39
476
  };
40
477
 
478
+ export type ImageEmbeddingInput = {
479
+ /** Image bytes or URL. */
480
+ data: Uint8Array | string;
481
+ /** IANA media type (recommended when data is bytes). */
482
+ mediaType?: string;
483
+ metadata: Metadata;
484
+ position: number;
485
+ sourceId: string;
486
+ documentId: string;
487
+ assetId?: string;
488
+ };
489
+
41
490
  export type EmbeddingProvider = {
42
491
  name: string;
43
492
  dimensions?: number;
44
493
  embed: (input: EmbeddingInput) => Promise<number[]>;
494
+ /**
495
+ * Optional batch embedding for performance.
496
+ * When present, the engine may embed text chunks in a single call.
497
+ */
498
+ embedMany?: (inputs: EmbeddingInput[]) => Promise<number[][]>;
499
+ /**
500
+ * Optional image embedding for unified multimodal retrieval.
501
+ * Only used when the configured provider supports it.
502
+ */
503
+ embedImage?: (input: ImageEmbeddingInput) => Promise<number[]>;
45
504
  };
46
505
 
47
506
  export type DeleteInput =
@@ -83,12 +542,83 @@ export type IngestInput = {
83
542
  content: string;
84
543
  metadata?: Metadata;
85
544
  chunking?: Partial<ChunkingOptions>;
545
+ /** Optional rich media attached to the document. */
546
+ assets?: AssetInput[];
547
+ /**
548
+ * Per-ingest overrides for asset processing. Merged with engine defaults.
549
+ * Use this to toggle expensive features (like PDF LLM extraction) per run.
550
+ */
551
+ assetProcessing?: DeepPartial<AssetProcessingConfig>;
86
552
  };
87
553
 
554
+ type IngestWarningBase<K extends AssetKind> = {
555
+ message: string;
556
+ assetId: string;
557
+ assetKind: K;
558
+ assetUri?: string;
559
+ assetMediaType?: string;
560
+ };
561
+
562
+ export type IngestWarning =
563
+ | (IngestWarningBase<AssetKind> & {
564
+ /**
565
+ * A rich media asset was encountered but no extractor exists for its kind.
566
+ * (Example: audio/video in v1.)
567
+ */
568
+ code: "asset_skipped_unsupported_kind";
569
+ })
570
+ | (IngestWarningBase<AssetKind> & {
571
+ /**
572
+ * An asset kind was encountered, but extraction for that kind is disabled by config.
573
+ * (Example: audio transcription disabled.)
574
+ */
575
+ code: "asset_skipped_extraction_disabled";
576
+ })
577
+ | (IngestWarningBase<"pdf"> & {
578
+ /**
579
+ * A PDF was encountered but PDF LLM extraction is disabled.
580
+ * Enable `assetProcessing.pdf.llmExtraction.enabled` to process PDFs.
581
+ */
582
+ code: "asset_skipped_pdf_llm_extraction_disabled";
583
+ })
584
+ | (IngestWarningBase<"image"> & {
585
+ /**
586
+ * An image was encountered but the embedding provider does not support image embedding
587
+ * AND the asset did not include a non-empty caption/alt text (`assets[].text`).
588
+ */
589
+ code: "asset_skipped_image_no_multimodal_and_no_caption";
590
+ })
591
+ | (IngestWarningBase<"pdf"> & {
592
+ /**
593
+ * PDF LLM extraction ran but produced no usable text.
594
+ * This is typically due to empty/scanned PDFs or model limitations.
595
+ */
596
+ code: "asset_skipped_pdf_empty_extraction";
597
+ })
598
+ | (IngestWarningBase<AssetKind> & {
599
+ /**
600
+ * Extraction ran but produced no usable text for the asset (non-PDF kinds).
601
+ * For PDFs, use `asset_skipped_pdf_empty_extraction`.
602
+ */
603
+ code: "asset_skipped_extraction_empty";
604
+ })
605
+ | (IngestWarningBase<AssetKind> & {
606
+ /**
607
+ * Asset processing failed, but policy allowed continuing (`assetProcessing.onError: "skip"`).
608
+ */
609
+ code: "asset_processing_error";
610
+ stage: "fetch" | "extract" | "embed" | "unknown";
611
+ });
612
+
88
613
  export type IngestResult = {
89
614
  documentId: string;
90
615
  chunkCount: number;
91
616
  embeddingModel: string;
617
+ /**
618
+ * Structured warnings emitted during ingestion.
619
+ * Use this to detect skipped rich media (unsupported kinds, disabled extraction, best-effort failures).
620
+ */
621
+ warnings: IngestWarning[];
92
622
  durations: {
93
623
  totalMs: number;
94
624
  chunkingMs: number;
@@ -115,12 +645,85 @@ export type RetrieveResult = {
115
645
  };
116
646
  };
117
647
 
648
+ /**
649
+ * Higher-level (ergonomic) Unrag config wrapper.
650
+ *
651
+ * This is intentionally separate from `ContextEngineConfig`:
652
+ * - `defaults.retrieval` is not part of the engine config; it's a convenience default for callers.
653
+ * - `defaults.chunking` maps to the engine's `defaults` field.
654
+ * - `embedding` is configured declaratively and can be turned into an `EmbeddingProvider`.
655
+ */
656
+ export type UnragDefaultsConfig = {
657
+ chunking?: Partial<ChunkingOptions>;
658
+ retrieval?: {
659
+ topK?: number;
660
+ };
661
+ };
662
+
663
+ export type UnragEngineConfig = Omit<
664
+ ContextEngineConfig,
665
+ "embedding" | "store" | "defaults"
666
+ >;
667
+
668
+ export type UnragEmbeddingConfig =
669
+ | {
670
+ provider: "ai";
671
+ config?: import("../embedding/ai").AiEmbeddingConfig;
672
+ }
673
+ | {
674
+ provider: "custom";
675
+ /**
676
+ * Escape hatch for bringing your own embedding provider.
677
+ * Use this when you need a provider that is not backed by the AI SDK.
678
+ */
679
+ create: () => EmbeddingProvider;
680
+ };
681
+
682
+ export type DefineUnragConfigInput = {
683
+ defaults?: UnragDefaultsConfig;
684
+ /**
685
+ * Engine configuration (everything except embedding/store/defaults).
686
+ * This is where you configure storage, asset processing, chunker/idGenerator, etc.
687
+ */
688
+ engine?: UnragEngineConfig;
689
+ /**
690
+ * Embedding configuration. The engine's embedding provider is derived from this.
691
+ */
692
+ embedding: UnragEmbeddingConfig;
693
+ };
694
+
695
+ export type UnragCreateEngineRuntime = {
696
+ store: VectorStore;
697
+ /**
698
+ * Optional runtime override/extension of extractors.
699
+ * - If you pass an array, it replaces the base extractors from `engine.extractors`.
700
+ * - If you pass a function, it receives the base extractors and should return the final array.
701
+ */
702
+ extractors?: AssetExtractor[] | ((base: AssetExtractor[]) => AssetExtractor[]);
703
+ };
704
+
118
705
  export type ContextEngineConfig = {
119
706
  embedding: EmbeddingProvider;
120
707
  store: VectorStore;
121
708
  defaults?: Partial<ChunkingOptions>;
122
709
  chunker?: Chunker;
123
710
  idGenerator?: () => string;
711
+ /**
712
+ * Optional extractor modules that can process non-text assets into text outputs.
713
+ * These are typically installed via `unrag add extractor <name>` and imported
714
+ * from your vendored module directory.
715
+ */
716
+ extractors?: AssetExtractor[];
717
+ /**
718
+ * Controls whether Unrag persists chunk/document text into the database.
719
+ * Defaults to storing both.
720
+ */
721
+ storage?: Partial<ContentStorageConfig>;
722
+ /**
723
+ * Asset processing defaults. If omitted, rich media is ignored (except image
724
+ * captions, which can still be ingested via `assets[].text` if you choose).
725
+ */
726
+ assetProcessing?: DeepPartial<AssetProcessingConfig>;
124
727
  };
125
728
 
126
729
  export type ResolvedContextEngineConfig = {
@@ -129,6 +732,9 @@ export type ResolvedContextEngineConfig = {
129
732
  defaults: ChunkingOptions;
130
733
  chunker: Chunker;
131
734
  idGenerator: () => string;
735
+ extractors: AssetExtractor[];
736
+ storage: ContentStorageConfig;
737
+ assetProcessing: AssetProcessingConfig;
132
738
  };
133
739
 
134
740
 
@@ -27,6 +27,7 @@ You are responsible for migrations. Create these tables:
27
27
  create table documents (
28
28
  id uuid primary key,
29
29
  source_id text not null,
30
+ content text not null,
30
31
  metadata jsonb,
31
32
  created_at timestamp default now()
32
33
  );
@@ -50,6 +51,11 @@ create table embeddings (
50
51
  );
51
52
  ```
52
53
 
54
+ Notes:
55
+ - `documents.content` stores the full original document text (used for debugging/re-chunking).
56
+ - `chunks.content` stores the chunk text returned by retrieval (`chunk.content`).
57
+ - You can disable persisting either/both via the engine config (`storage.storeDocumentContent` / `storage.storeChunkContent`). The schema still requires `text not null`, so Unrag stores empty strings when disabled.
58
+
53
59
  Recommended indexes:
54
60
 
55
61
  ```sql