@retrivora-ai/rag-engine 1.9.2 → 1.9.6

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 (60) hide show
  1. package/README.md +27 -0
  2. package/dist/{ILLMProvider-Bw2A28nU.d.mts → ILLMProvider-DNhyOYoK.d.mts} +41 -1
  3. package/dist/{ILLMProvider-Bw2A28nU.d.ts → ILLMProvider-DNhyOYoK.d.ts} +41 -1
  4. package/dist/handlers/index.d.mts +2 -2
  5. package/dist/handlers/index.d.ts +2 -2
  6. package/dist/handlers/index.js +563 -205
  7. package/dist/handlers/index.mjs +563 -205
  8. package/dist/index-C9v7-tWd.d.mts +183 -0
  9. package/dist/{index-B70ZLkfG.d.mts → index-CHL1jdYm.d.mts} +1 -1
  10. package/dist/index-CjQdL0cX.d.ts +183 -0
  11. package/dist/{index-DVu-mkAM.d.ts → index-Hgbwl9X4.d.ts} +1 -1
  12. package/dist/index.css +40 -0
  13. package/dist/index.d.mts +3 -3
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.js +326 -158
  16. package/dist/index.mjs +312 -151
  17. package/dist/server.d.mts +15 -94
  18. package/dist/server.d.ts +15 -94
  19. package/dist/server.js +623 -205
  20. package/dist/server.mjs +615 -205
  21. package/package.json +1 -1
  22. package/src/app/api/chat/route.ts +2 -2
  23. package/src/app/api/health/route.ts +3 -2
  24. package/src/app/api/ingest/route.ts +3 -2
  25. package/src/app/api/suggestions/route.ts +3 -2
  26. package/src/app/api/upload/route.ts +3 -2
  27. package/src/app/constants.tsx +85 -30
  28. package/src/app/layout.tsx +18 -7
  29. package/src/components/MessageBubble.tsx +39 -2
  30. package/src/components/ThinkingBlock.tsx +75 -0
  31. package/src/components/VisualizationRenderer.tsx +27 -1
  32. package/src/config/RagConfig.ts +47 -0
  33. package/src/config/serverConfig.ts +25 -0
  34. package/src/core/ConfigResolver.ts +56 -4
  35. package/src/core/ConfigValidator.ts +2 -1
  36. package/src/core/Pipeline.ts +226 -68
  37. package/src/core/ProviderRegistry.ts +11 -2
  38. package/src/core/QueryProcessor.ts +38 -4
  39. package/src/core/Retrivora.ts +51 -0
  40. package/src/exceptions/index.ts +59 -0
  41. package/src/handlers/index.ts +2 -0
  42. package/src/hooks/useRagChat.ts +26 -4
  43. package/src/index.ts +25 -1
  44. package/src/lib/plugin.ts +24 -0
  45. package/src/llm/LLMFactory.ts +4 -1
  46. package/src/llm/providers/AnthropicProvider.ts +70 -20
  47. package/src/llm/providers/GeminiProvider.ts +14 -15
  48. package/src/llm/providers/OllamaProvider.ts +13 -16
  49. package/src/llm/providers/OpenAIProvider.ts +9 -14
  50. package/src/llm/providers/UniversalLLMAdapter.ts +5 -5
  51. package/src/llm/utils.ts +46 -0
  52. package/src/providers/vectordb/MongoDBProvider.ts +19 -7
  53. package/src/providers/vectordb/MultiTablePostgresProvider.ts +45 -13
  54. package/src/rag/EntityExtractor.ts +2 -2
  55. package/src/rag/Reranker.ts +9 -16
  56. package/src/server.ts +27 -1
  57. package/src/types/chat.ts +7 -0
  58. package/src/utils/UITransformer.ts +73 -4
  59. package/dist/DocumentChunker-Dh9TvmGG.d.mts +0 -45
  60. package/dist/DocumentChunker-Dh9TvmGG.d.ts +0 -45
@@ -72,6 +72,11 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
72
72
  ON ${this.uploadTable}
73
73
  USING hnsw (embedding vector_cosine_ops)
74
74
  `);
75
+ await client.query(`
76
+ CREATE INDEX IF NOT EXISTS "${this.uploadTable}_fts_idx"
77
+ ON "${this.uploadTable}"
78
+ USING gin (to_tsvector('english', content))
79
+ `);
75
80
 
76
81
  const res = await client.query(`
77
82
  SELECT table_name
@@ -158,6 +163,11 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
158
163
  ON "${tableName}"
159
164
  USING hnsw (embedding vector_cosine_ops)
160
165
  `);
166
+ await client.query(`
167
+ CREATE INDEX IF NOT EXISTS "${tableName}_fts_idx"
168
+ ON "${tableName}"
169
+ USING gin (to_tsvector('english', content))
170
+ `);
161
171
 
162
172
  // Ensure this new table is added to our search scope
163
173
  if (!this.tables.includes(tableName)) {
@@ -297,9 +307,12 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
297
307
  const synonyms = FIELD_SYNONYMS[key] ?? [];
298
308
  const keysToCheck = [key, ...synonyms];
299
309
 
300
- // Map each candidate key to LOWER(metadata->>'candidate')
301
- const coalesceExprs = keysToCheck.map(k => `LOWER(metadata->>'${k}')`);
302
- return `COALESCE(${coalesceExprs.join(', ')}) = LOWER($${paramIdx})`;
310
+ // Map each candidate key to LOWER(coalesce(metadata->>'key', to_jsonb(t)->>'key'))
311
+ const coalesceExprs = keysToCheck.flatMap(k => [
312
+ `metadata->>'${k}'`,
313
+ `to_jsonb(t)->>'${k}'`
314
+ ]);
315
+ return `COALESCE(${coalesceExprs.map(expr => `LOWER(${expr})`).join(', ')}) = LOWER($${paramIdx})`;
303
316
  });
304
317
  whereClause = `WHERE ${conditions.join(' AND ')}`;
305
318
  }
@@ -312,31 +325,50 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
312
325
  CASE WHEN LOWER(val) IN (${entityHints.map((h) => `'${h.replace(/'/g, "''")}'`).join(', ')})
313
326
  THEN 1.0 ELSE 0.0 END
314
327
  ), 0)
315
- FROM jsonb_each_text(to_jsonb(t)) AS kv(key, val)
328
+ FROM jsonb_each_text(COALESCE(v.metadata, k.metadata)) AS kv(key, val)
316
329
  WHERE key IN (${this.searchFields.map(f => `'${f}'`).join(', ')})
317
330
  ) * 3.0`
318
331
  : '';
319
332
 
320
- // Actual Hybrid Search: Semantic Vector Score + Full-Text Keyword Rank
333
+ // Optimized Subquery CTE Hybrid Search (Uses HNSW vector index & GIN fulltext index)
321
334
  sqlQuery = `
322
- SELECT *,
323
- (1 - (embedding <=> $1::vector)) AS vector_score,
324
- COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
325
- ((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
326
- FROM "${table}" t
327
- ${whereClause}
335
+ WITH vector_search AS (
336
+ SELECT *, (1 - (embedding <=> $1::vector)) AS vector_score
337
+ FROM "${table}" t
338
+ ${whereClause}
339
+ ORDER BY embedding <=> $1::vector ASC
340
+ LIMIT ${tableLimit}
341
+ ),
342
+ keyword_search AS (
343
+ SELECT *, COALESCE(ts_rank(to_tsvector('english', content), to_tsquery('english', $2)), 0) AS keyword_score
344
+ FROM "${table}" t
345
+ WHERE ${whereClause ? whereClause.replace('WHERE', '') : 'TRUE'}
346
+ AND to_tsvector('english', content) @@ to_tsquery('english', $2)
347
+ ORDER BY keyword_score DESC
348
+ LIMIT ${tableLimit}
349
+ )
350
+ SELECT
351
+ COALESCE(v.id, k.id) AS id,
352
+ COALESCE(v.namespace, k.namespace) AS namespace,
353
+ COALESCE(v.content, k.content) AS content,
354
+ COALESCE(v.metadata, k.metadata) AS metadata,
355
+ COALESCE(v.vector_score, 0) AS vector_score,
356
+ COALESCE(k.keyword_score, 0) AS keyword_score,
357
+ (COALESCE(v.vector_score, 0) + COALESCE(k.keyword_score, 0) * 2.0 ${exactNameScoreExpr}) AS hybrid_score
358
+ FROM vector_search v
359
+ FULL OUTER JOIN keyword_search k ON v.id = k.id
328
360
  ORDER BY hybrid_score DESC
329
361
  LIMIT ${tableLimit}
330
362
  `;
331
363
  params = [vectorLiteral, dynamicKeywordQuery, ...filterParams];
332
364
  } else {
333
- // Fallback to Vector-only Search
365
+ // Fallback to Vector-only Search (ordered ASC to use HNSW index)
334
366
  sqlQuery = `
335
367
  SELECT *,
336
368
  (1 - (embedding <=> $1::vector)) AS hybrid_score
337
369
  FROM "${table}" t
338
370
  ${whereClause}
339
- ORDER BY hybrid_score DESC
371
+ ORDER BY embedding <=> $1::vector ASC
340
372
  LIMIT ${tableLimit}
341
373
  `;
342
374
  params = [vectorLiteral, ...filterParams];
@@ -41,7 +41,7 @@ Text to extract from:
41
41
  // 1. Try to find the JSON block using a regex
42
42
  const jsonMatch = response.match(/\{[\s\S]*\}/);
43
43
  const cleanJson = jsonMatch ? jsonMatch[0] : response.trim();
44
-
44
+
45
45
  return JSON.parse(cleanJson);
46
46
  } catch {
47
47
  // 2. If parsing fails, try to repair truncated JSON
@@ -62,7 +62,7 @@ Text to extract from:
62
62
  */
63
63
  private repairTruncatedJson(json: string): string | null {
64
64
  let text = json.trim();
65
-
65
+
66
66
  // Find the first { and work from there
67
67
  const startIdx = text.indexOf('{');
68
68
  if (startIdx === -1) return null;
@@ -6,8 +6,8 @@ export class Reranker {
6
6
  * Re-ranks matches based on a secondary relevance score using an LLM.
7
7
  */
8
8
  async rerank(
9
- matches: VectorMatch[],
10
- query: string,
9
+ matches: VectorMatch[],
10
+ query: string,
11
11
  limit: number = 5,
12
12
  llm?: ILLMProvider
13
13
  ): Promise<VectorMatch[]> {
@@ -27,7 +27,7 @@ export class Reranker {
27
27
  const scoredMatches = matches.map(match => {
28
28
  const contentLower = match.content.toLowerCase();
29
29
  let keywordScore = 0;
30
-
30
+
31
31
  keywords.forEach(keyword => {
32
32
  if (contentLower.includes(keyword)) {
33
33
  keywordScore += 1;
@@ -55,22 +55,15 @@ export class Reranker {
55
55
  try {
56
56
  // Take top 10 matches to avoid large context and high latency
57
57
  const topN = matches.slice(0, 10);
58
-
59
- const prompt = `You are a relevance ranking expert.
60
- Given the following user query and a list of retrieved document chunks, rank the chunks by relevance to the query.
61
- Return the result as a comma-separated list of document indices in order of relevance, from most relevant to least relevant.
62
- Use the indices provided in brackets like [0], [1], etc.
63
- Only return the indices (e.g., "2,0,1"), nothing else. Do not include brackets in the output.
64
-
65
- Query: "${query}"
66
-
67
- Documents:
68
- ${topN.map((m, i) => `[${i}] ${m.content.replace(/\n/g, ' ')}`).join('\n')}`;
69
58
 
70
59
  const response = await llm.chat(
71
- [{ role: 'user', content: prompt }],
60
+ [{ role: 'user', content: `Query: "${query}"\n\nDocuments:\n${topN.map((m, i) => `[${i}] ${m.content.replace(/\n/g, ' ')}`).join('\n')}` }],
72
61
  '',
73
- { temperature: 0, maxTokens: 50 }
62
+ {
63
+ systemPrompt: 'You are a relevance ranking expert. Given the user query and a list of retrieved document chunks, rank the chunks by relevance to the query. Return the result as a comma-separated list of document indices in order of relevance, from most relevant to least relevant. Use the indices provided in brackets like [0], [1], etc. Only return the indices (e.g., "2,0,1"), nothing else. Do not include brackets in the output.',
64
+ temperature: 0,
65
+ maxTokens: 50
66
+ }
74
67
  );
75
68
 
76
69
  const cleanedResponse = response.trim().replace(/[\[\]\s]/g, '');
package/src/server.ts CHANGED
@@ -6,7 +6,20 @@
6
6
  */
7
7
 
8
8
  // ── Shared Types ──────────────────────────────────────────────
9
- export type { RagConfig, VectorDBConfig, LLMConfig, EmbeddingConfig, UIConfig, RAGConfig, VectorDBProvider, LLMProvider, EmbeddingProvider } from './config/RagConfig';
9
+ export type {
10
+ RagConfig,
11
+ UniversalRagConfig,
12
+ RetrievalConfig,
13
+ WorkflowConfig,
14
+ VectorDBConfig,
15
+ LLMConfig,
16
+ EmbeddingConfig,
17
+ UIConfig,
18
+ RAGConfig,
19
+ VectorDBProvider,
20
+ LLMProvider,
21
+ EmbeddingProvider,
22
+ } from './config/RagConfig';
10
23
  export type { VectorMatch, UpsertDocument, IngestDocument, ChatResponse, ChatMessage, ChatOptions } from './types';
11
24
  export type { ILLMProvider, EmbedOptions } from './llm/ILLMProvider';
12
25
  export type { Chunk, ChunkOptions } from './rag/DocumentChunker';
@@ -16,6 +29,7 @@ export type { BatchOptions, BatchResult } from './core/BatchProcessor';
16
29
 
17
30
  // ── Core Orchestration ─────────────────────────────────────────
18
31
  export { VectorPlugin } from './core/VectorPlugin';
32
+ export { Retrivora } from './core/Retrivora';
19
33
  export { Pipeline } from './core/Pipeline';
20
34
  export { ConfigResolver } from './core/ConfigResolver';
21
35
  export { ProviderRegistry } from './core/ProviderRegistry';
@@ -23,6 +37,18 @@ export { ConfigValidator } from './core/ConfigValidator';
23
37
  export { ProviderHealthCheck } from './core/ProviderHealthCheck';
24
38
  export { BatchProcessor } from './core/BatchProcessor';
25
39
 
40
+ // ── Exceptions ────────────────────────────────────────────────
41
+ export {
42
+ RetrivoraError,
43
+ ProviderNotFoundException,
44
+ EmbeddingFailedException,
45
+ RetrievalException,
46
+ RateLimitException,
47
+ ConfigurationException,
48
+ AuthenticationException,
49
+ } from './exceptions';
50
+ export type { RetrivoraErrorCode } from './exceptions';
51
+
26
52
  // ── Configuration Helpers ──────────────────────────────────────
27
53
  export { getRagConfig } from './config/serverConfig';
28
54
  export { ConfigBuilder, PRESETS, createFromPreset } from './config/ConfigBuilder';
package/src/types/chat.ts CHANGED
@@ -14,6 +14,13 @@ export interface RagMessage extends ChatMessage {
14
14
  /** Full observability trace emitted by the backend. Only present on assistant messages. */
15
15
  trace?: import('./index').ObservabilityTrace;
16
16
  createdAt: string;
17
+ /**
18
+ * Chain-of-thought reasoning from the LLM.
19
+ * Populated by native Anthropic extended thinking or simulated <think> tag stripping.
20
+ */
21
+ thinking?: string;
22
+ /** Wall-clock ms the model spent in the thinking phase. */
23
+ thinkingMs?: number;
17
24
  }
18
25
 
19
26
  export interface ChatOptions {
@@ -217,7 +217,7 @@ export class UITransformer {
217
217
  // ── Step 1: Detect intent ───────────────────────────────────────────────
218
218
  let intent: QueryIntent;
219
219
  try {
220
- intent = await this.detectIntent(query, llm);
220
+ intent = await this.detectIntent(query, llm, sources);
221
221
  console.debug('[UITransformer] Detected intent:', intent);
222
222
  } catch (err) {
223
223
  console.warn('[UITransformer] Intent detection failed; using heuristic.', err);
@@ -239,11 +239,24 @@ export class UITransformer {
239
239
  const context = this.buildContextSummary(sources);
240
240
  const systemPrompt = this.buildVisualizationSystemPrompt();
241
241
 
242
+ const profile = this.profileData(sources);
243
+ const schemaContext = `
244
+ RETRIEVED DATA SCHEMA:
245
+ - Numeric fields (measures): ${profile.numericFields.map(f => `${f.key} (unique values: ${f.uniqueCount})`).join(', ') || 'None'}
246
+ - Category fields (dimensions): ${profile.categoricalFields.map(f => `${f.key} (unique values: ${f.uniqueCount})`).join(', ') || 'None'}
247
+ - Date fields (temporal): ${profile.dateFields.map(f => `${f.key} (unique values: ${f.uniqueCount})`).join(', ') || 'None'}
248
+ - Boolean fields: ${profile.booleanFields.map(f => `${f.key} (unique values: ${f.uniqueCount})`).join(', ') || 'None'}
249
+ - Text/Other fields: ${profile.fields.filter(f => f.kind === 'text').map(f => f.key).join(', ') || 'None'}
250
+ `;
251
+
242
252
  const userPrompt = [
243
253
  `USER QUESTION: ${query}`,
244
254
  '',
245
255
  `DETECTED INTENT (JSON): ${JSON.stringify(intent)}`,
246
256
  '',
257
+ `RETRIEVED DATA SCHEMA:`,
258
+ schemaContext,
259
+ '',
247
260
  'RETRIEVED DATA (JSON):',
248
261
  context,
249
262
  ].join('\n');
@@ -256,6 +269,26 @@ export class UITransformer {
256
269
 
257
270
  const parsed = this.parseTransformationResponse(rawResponse);
258
271
  if (parsed) {
272
+ // Validate suitability of selected chart type based on schema
273
+ let isCompatible = true;
274
+ if (parsed.type === 'line_chart' && (profile.dateFields.length === 0 || profile.numericFields.length === 0)) {
275
+ isCompatible = false;
276
+ } else if (
277
+ ['bar_chart', 'horizontal_bar', 'pie_chart', 'donut_chart'].includes(parsed.type) &&
278
+ (profile.categoricalFields.length === 0 && profile.numericFields.length === 0)
279
+ ) {
280
+ isCompatible = false;
281
+ } else if (parsed.type === 'scatter_plot' && profile.numericFields.length < 2) {
282
+ isCompatible = false;
283
+ } else if (parsed.type === 'metric_card' && profile.numericFields.length === 0) {
284
+ isCompatible = false;
285
+ }
286
+
287
+ if (!isCompatible) {
288
+ console.warn(`[UITransformer] LLM chose incompatible visualization type "${parsed.type}" for retrieved schema. Falling back to heuristic.`);
289
+ return this.transform(query, sources, undefined, undefined, intent);
290
+ }
291
+
259
292
  const intentAllowsTable =
260
293
  intent.wantsExplicitTable ||
261
294
  ['tabular', 'table', 'geographic'].includes(intent.visualizationHint);
@@ -295,7 +328,27 @@ export class UITransformer {
295
328
  * - The intent object can be reused across both the heuristic and LLM paths.
296
329
  * - It is easy to unit-test intent detection in isolation.
297
330
  */
298
- static async detectIntent(query: string, llm: ILLMProvider): Promise<QueryIntent> {
331
+ static async detectIntent(query: string, llm: ILLMProvider, sources?: VectorMatch[]): Promise<QueryIntent> {
332
+ let schemaProfileText = '';
333
+ if (sources && sources.length > 0) {
334
+ const profile = this.profileData(sources);
335
+ const hasProducts = sources.some(item => this.isProductData(item));
336
+ schemaProfileText = `
337
+ RETRIEVED DATA SCHEMA:
338
+ - Numeric fields (measures): ${profile.numericFields.map(f => `${f.key} (unique values: ${f.uniqueCount})`).join(', ') || 'None'}
339
+ - Category fields (dimensions): ${profile.categoricalFields.map(f => `${f.key} (unique values: ${f.uniqueCount})`).join(', ') || 'None'}
340
+ - Date fields (temporal): ${profile.dateFields.map(f => `${f.key} (unique values: ${f.uniqueCount})`).join(', ') || 'None'}
341
+ - Boolean fields: ${profile.booleanFields.map(f => `${f.key} (unique values: ${f.uniqueCount})`).join(', ') || 'None'}
342
+ - Text fields: ${profile.fields.filter(f => f.kind === 'text').map(f => f.key).join(', ') || 'None'}
343
+
344
+ Please choose visualizationHint and recommendedChart dynamically based on the available schema.
345
+ - Do NOT recommend "trend" or "line_chart" if there are no Date fields.
346
+ - Do NOT recommend numeric-dependent visualizations (like bar_chart, line_chart, pie_chart, scatter_plot, histogram, metric_card) if there are no Numeric fields. Fall back to tabular/table or text.
347
+ - If there are multiple metadata fields but no clear categories or numbers, "tabular"/"table" is a highly accurate default.
348
+ ${hasProducts ? `- Note: The retrieved data matches product/catalog structure. If the user query is looking for, exploring, searching, browsing, or listing products/items, you MUST recommend visualizationHint: "product_browse" and recommendedChart: "text".` : ''}
349
+ `;
350
+ }
351
+
299
352
  const systemPrompt = `You are an intent classifier for a product-search RAG system.
300
353
  Given a user query, return ONLY a valid JSON object with this exact shape — no prose, no markdown:
301
354
 
@@ -332,8 +385,10 @@ RULES:
332
385
  - isComparison: true if user compares, ranks, or contrasts entities or categories.
333
386
  - language: detect from the query text itself; default "en" if uncertain.`;
334
387
 
388
+ const userPrompt = `QUERY: ${query}${schemaProfileText ? `\n\n${schemaProfileText}` : ''}`;
389
+
335
390
  const rawResponse = await llm.chat(
336
- [{ role: 'user', content: `QUERY: ${query}` }],
391
+ [{ role: 'user', content: userPrompt }],
337
392
  '',
338
393
  { systemPrompt, temperature: 0 },
339
394
  );
@@ -894,14 +949,28 @@ RULES:
894
949
  return asksForRecords && (hasNumericPredicate || hasEntityTerms);
895
950
  }
896
951
 
952
+ /** @internal kept private for transform() internal logic */
897
953
  private static isProductQuery(query: string): boolean {
954
+ return UITransformer._productQueryTest(query);
955
+ }
956
+
957
+ /**
958
+ * Public entry-point for external callers (e.g. Pipeline)
959
+ * to check whether a query maps to product_browse without triggering an LLM call.
960
+ */
961
+ static isProductQueryPublic(query: string): boolean {
962
+ return UITransformer._productQueryTest(query);
963
+ }
964
+
965
+ private static _productQueryTest(query: string): boolean {
898
966
  const q = query.toLowerCase();
899
967
  const productTerms = /\b(product|products|item|items|sku|catalog|catalogue|price|prices|brand|model|stock|inventory|in stock|out of stock|buy|shop)\b/.test(q);
900
- const productAction = /\b(recommend|suggest|describe|description|detail|details|about|show|find|search|browse|list)\b/.test(q);
968
+ const productAction = /\b(recommend|suggest|describe|description|detail|details|about|show|find|search|browse|list|explore|exploring)\b/.test(q);
901
969
  const nonProductEntityTerms = /\b(organization|organisations?|organizations?|company|companies|employee|employees|staff|headcount|workforce)\b/.test(q);
902
970
  return productTerms && productAction && !nonProductEntityTerms;
903
971
  }
904
972
 
973
+
905
974
  private static isProductData(item: VectorMatch): boolean {
906
975
  const content = (item.content || '').toLowerCase();
907
976
  const productKeywords = [
@@ -1,45 +0,0 @@
1
- /**
2
- * DocumentChunker — splits long text into overlapping chunks
3
- * suitable for vector embedding and retrieval.
4
- *
5
- * Strategy: sentence-aware sliding window
6
- * 1. Split on sentence boundaries
7
- * 2. Accumulate sentences into chunks up to `chunkSize` characters
8
- * 3. Carry over `chunkOverlap` characters from the previous chunk
9
- */
10
- interface Chunk {
11
- id: string;
12
- content: string;
13
- metadata?: Record<string, unknown>;
14
- }
15
- interface ChunkOptions {
16
- /** Target chunk size in characters (default 1000) */
17
- chunkSize?: number;
18
- /** Overlap between consecutive chunks in characters (default 200) */
19
- chunkOverlap?: number;
20
- /** Source document identifier used as ID prefix */
21
- docId?: string | number;
22
- /** Extra metadata to attach to every chunk */
23
- metadata?: Record<string, unknown>;
24
- /** Characters used to split text, in order of priority */
25
- separators?: string[];
26
- }
27
- declare class DocumentChunker {
28
- private readonly chunkSize;
29
- private readonly chunkOverlap;
30
- private readonly separators;
31
- constructor(chunkSize?: number, chunkOverlap?: number, separators?: string[]);
32
- /**
33
- * Split a single text string into overlapping chunks using a recursive strategy.
34
- * Preserves structural boundaries (Markdown headers) where possible.
35
- */
36
- chunk(text: string, options?: ChunkOptions): Chunk[];
37
- private recursiveSplit;
38
- chunkMany(documents: Array<{
39
- content: string;
40
- docId?: string | number;
41
- metadata?: Record<string, unknown>;
42
- }>): Chunk[];
43
- }
44
-
45
- export { type Chunk as C, DocumentChunker as D, type ChunkOptions as a };
@@ -1,45 +0,0 @@
1
- /**
2
- * DocumentChunker — splits long text into overlapping chunks
3
- * suitable for vector embedding and retrieval.
4
- *
5
- * Strategy: sentence-aware sliding window
6
- * 1. Split on sentence boundaries
7
- * 2. Accumulate sentences into chunks up to `chunkSize` characters
8
- * 3. Carry over `chunkOverlap` characters from the previous chunk
9
- */
10
- interface Chunk {
11
- id: string;
12
- content: string;
13
- metadata?: Record<string, unknown>;
14
- }
15
- interface ChunkOptions {
16
- /** Target chunk size in characters (default 1000) */
17
- chunkSize?: number;
18
- /** Overlap between consecutive chunks in characters (default 200) */
19
- chunkOverlap?: number;
20
- /** Source document identifier used as ID prefix */
21
- docId?: string | number;
22
- /** Extra metadata to attach to every chunk */
23
- metadata?: Record<string, unknown>;
24
- /** Characters used to split text, in order of priority */
25
- separators?: string[];
26
- }
27
- declare class DocumentChunker {
28
- private readonly chunkSize;
29
- private readonly chunkOverlap;
30
- private readonly separators;
31
- constructor(chunkSize?: number, chunkOverlap?: number, separators?: string[]);
32
- /**
33
- * Split a single text string into overlapping chunks using a recursive strategy.
34
- * Preserves structural boundaries (Markdown headers) where possible.
35
- */
36
- chunk(text: string, options?: ChunkOptions): Chunk[];
37
- private recursiveSplit;
38
- chunkMany(documents: Array<{
39
- content: string;
40
- docId?: string | number;
41
- metadata?: Record<string, unknown>;
42
- }>): Chunk[];
43
- }
44
-
45
- export { type Chunk as C, DocumentChunker as D, type ChunkOptions as a };