@msbayindir/context-rag 1.0.0-beta.1

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/dist/index.js ADDED
@@ -0,0 +1,2842 @@
1
+ import { z } from 'zod';
2
+ import { createHash } from 'crypto';
3
+ import { GoogleGenerativeAI, TaskType } from '@google/generative-ai';
4
+ import { GoogleAIFileManager } from '@google/generative-ai/server';
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import pdf from 'pdf-parse';
8
+ import pLimit from 'p-limit';
9
+
10
+ // src/types/config.types.ts
11
+ var DEFAULT_BATCH_CONFIG = {
12
+ pagesPerBatch: 15,
13
+ maxConcurrency: 3,
14
+ maxRetries: 3,
15
+ retryDelayMs: 1e3,
16
+ backoffMultiplier: 2
17
+ };
18
+ var DEFAULT_CHUNK_CONFIG = {
19
+ maxTokens: 500,
20
+ overlapTokens: 50
21
+ };
22
+ var DEFAULT_RATE_LIMIT_CONFIG = {
23
+ requestsPerMinute: 60,
24
+ adaptive: true
25
+ };
26
+ var DEFAULT_GENERATION_CONFIG = {
27
+ temperature: 0.3,
28
+ maxOutputTokens: 8192
29
+ };
30
+ var DEFAULT_LOG_CONFIG = {
31
+ level: "info",
32
+ structured: true
33
+ };
34
+ var configSchema = z.object({
35
+ geminiApiKey: z.string().min(1, "Gemini API key is required"),
36
+ model: z.enum([
37
+ "gemini-1.5-pro",
38
+ "gemini-1.5-flash",
39
+ "gemini-2.0-flash-exp",
40
+ "gemini-pro",
41
+ "gemini-2.5-pro",
42
+ "gemini-3-pro-preview",
43
+ "gemini-3-flash-preview"
44
+ ]).optional(),
45
+ embeddingModel: z.string().optional(),
46
+ batchConfig: z.object({
47
+ pagesPerBatch: z.number().min(1).max(50).optional(),
48
+ maxConcurrency: z.number().min(1).max(10).optional(),
49
+ maxRetries: z.number().min(0).max(10).optional(),
50
+ retryDelayMs: z.number().min(100).max(6e4).optional(),
51
+ backoffMultiplier: z.number().min(1).max(5).optional()
52
+ }).optional(),
53
+ chunkConfig: z.object({
54
+ maxTokens: z.number().min(100).max(2e3).optional(),
55
+ overlapTokens: z.number().min(0).max(500).optional()
56
+ }).optional(),
57
+ rateLimitConfig: z.object({
58
+ requestsPerMinute: z.number().min(1).max(1e3).optional(),
59
+ adaptive: z.boolean().optional()
60
+ }).optional(),
61
+ logging: z.object({
62
+ level: z.enum(["debug", "info", "warn", "error"]).optional(),
63
+ structured: z.boolean().optional()
64
+ }).optional()
65
+ });
66
+
67
+ // src/errors/index.ts
68
+ var ContextRAGError = class extends Error {
69
+ code;
70
+ details;
71
+ constructor(message, code, details) {
72
+ super(message);
73
+ this.name = "ContextRAGError";
74
+ this.code = code;
75
+ this.details = details;
76
+ Error.captureStackTrace(this, this.constructor);
77
+ }
78
+ toJSON() {
79
+ return {
80
+ name: this.name,
81
+ code: this.code,
82
+ message: this.message,
83
+ details: this.details
84
+ };
85
+ }
86
+ };
87
+ var ConfigurationError = class extends ContextRAGError {
88
+ constructor(message, details) {
89
+ super(message, "CONFIGURATION_ERROR", details);
90
+ this.name = "ConfigurationError";
91
+ }
92
+ };
93
+ var IngestionError = class extends ContextRAGError {
94
+ batchIndex;
95
+ retryable;
96
+ constructor(message, options = {}) {
97
+ super(message, "INGESTION_ERROR", options.details);
98
+ this.name = "IngestionError";
99
+ this.batchIndex = options.batchIndex;
100
+ this.retryable = options.retryable ?? false;
101
+ }
102
+ };
103
+ var SearchError = class extends ContextRAGError {
104
+ constructor(message, details) {
105
+ super(message, "SEARCH_ERROR", details);
106
+ this.name = "SearchError";
107
+ }
108
+ };
109
+ var DiscoveryError = class extends ContextRAGError {
110
+ constructor(message, details) {
111
+ super(message, "DISCOVERY_ERROR", details);
112
+ this.name = "DiscoveryError";
113
+ }
114
+ };
115
+ var DatabaseError = class extends ContextRAGError {
116
+ constructor(message, details) {
117
+ super(message, "DATABASE_ERROR", details);
118
+ this.name = "DatabaseError";
119
+ }
120
+ };
121
+ var RateLimitError = class extends ContextRAGError {
122
+ retryAfterMs;
123
+ constructor(message, retryAfterMs) {
124
+ super(message, "RATE_LIMIT_ERROR", { retryAfterMs });
125
+ this.name = "RateLimitError";
126
+ this.retryAfterMs = retryAfterMs;
127
+ }
128
+ };
129
+ var NotFoundError = class extends ContextRAGError {
130
+ resourceType;
131
+ resourceId;
132
+ constructor(resourceType, resourceId) {
133
+ super(`${resourceType} not found: ${resourceId}`, "NOT_FOUND", {
134
+ resourceType,
135
+ resourceId
136
+ });
137
+ this.name = "NotFoundError";
138
+ this.resourceType = resourceType;
139
+ this.resourceId = resourceId;
140
+ }
141
+ };
142
+
143
+ // src/utils/logger.ts
144
+ var LOG_LEVELS = {
145
+ debug: 0,
146
+ info: 1,
147
+ warn: 2,
148
+ error: 3
149
+ };
150
+ function createLogger(config) {
151
+ const currentLevel = LOG_LEVELS[config.level];
152
+ const shouldLog = (level) => {
153
+ return LOG_LEVELS[level] >= currentLevel;
154
+ };
155
+ const formatMessage = (level, message, meta) => {
156
+ if (config.structured) {
157
+ return JSON.stringify({
158
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
159
+ level,
160
+ message,
161
+ ...meta
162
+ });
163
+ }
164
+ const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
165
+ return `[${(/* @__PURE__ */ new Date()).toISOString()}] [${level.toUpperCase()}] ${message}${metaStr}`;
166
+ };
167
+ const log = (level, message, meta) => {
168
+ if (!shouldLog(level)) return;
169
+ if (config.customLogger) {
170
+ config.customLogger(level, message, meta);
171
+ return;
172
+ }
173
+ const formattedMessage = formatMessage(level, message, meta);
174
+ switch (level) {
175
+ case "debug":
176
+ case "info":
177
+ console.log(formattedMessage);
178
+ break;
179
+ case "warn":
180
+ console.warn(formattedMessage);
181
+ break;
182
+ case "error":
183
+ console.error(formattedMessage);
184
+ break;
185
+ }
186
+ };
187
+ return {
188
+ debug: (message, meta) => log("debug", message, meta),
189
+ info: (message, meta) => log("info", message, meta),
190
+ warn: (message, meta) => log("warn", message, meta),
191
+ error: (message, meta) => log("error", message, meta)
192
+ };
193
+ }
194
+ function generateCorrelationId() {
195
+ return `crag_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
196
+ }
197
+ function hashBuffer(buffer) {
198
+ return createHash("sha256").update(buffer).digest("hex");
199
+ }
200
+
201
+ // src/utils/retry.ts
202
+ function getRetryOptions(batchConfig) {
203
+ return {
204
+ maxRetries: batchConfig.maxRetries,
205
+ initialDelayMs: batchConfig.retryDelayMs,
206
+ maxDelayMs: 3e4,
207
+ backoffMultiplier: batchConfig.backoffMultiplier,
208
+ retryableErrors: ["429", "503", "TIMEOUT", "ECONNRESET", "ETIMEDOUT"]
209
+ };
210
+ }
211
+ function isRetryableError(error, retryableErrors = []) {
212
+ const errorString = error.message + (error.name || "");
213
+ if (error instanceof RateLimitError) {
214
+ return true;
215
+ }
216
+ return retryableErrors.some(
217
+ (pattern) => errorString.includes(pattern) || error.name.includes(pattern)
218
+ );
219
+ }
220
+ function calculateBackoffDelay(attempt, initialDelayMs, backoffMultiplier, maxDelayMs) {
221
+ const delay = initialDelayMs * Math.pow(backoffMultiplier, attempt - 1);
222
+ const jitter = delay * 0.1 * (Math.random() * 2 - 1);
223
+ return Math.min(delay + jitter, maxDelayMs);
224
+ }
225
+ function sleep(ms) {
226
+ return new Promise((resolve) => setTimeout(resolve, ms));
227
+ }
228
+ async function withRetry(fn, options) {
229
+ let lastError;
230
+ for (let attempt = 1; attempt <= options.maxRetries + 1; attempt++) {
231
+ try {
232
+ return await fn();
233
+ } catch (error) {
234
+ lastError = error;
235
+ if (attempt > options.maxRetries) {
236
+ break;
237
+ }
238
+ if (!isRetryableError(lastError, options.retryableErrors)) {
239
+ throw lastError;
240
+ }
241
+ let delayMs = calculateBackoffDelay(
242
+ attempt,
243
+ options.initialDelayMs,
244
+ options.backoffMultiplier,
245
+ options.maxDelayMs
246
+ );
247
+ if (lastError instanceof RateLimitError && lastError.retryAfterMs) {
248
+ delayMs = Math.max(delayMs, lastError.retryAfterMs);
249
+ }
250
+ options.onRetry?.(attempt, lastError, delayMs);
251
+ await sleep(delayMs);
252
+ }
253
+ }
254
+ throw lastError;
255
+ }
256
+
257
+ // src/utils/rate-limiter.ts
258
+ var RateLimiter = class {
259
+ config;
260
+ state;
261
+ minRpm;
262
+ maxRpm;
263
+ intervalMs = 6e4;
264
+ // 1 minute
265
+ constructor(config) {
266
+ this.config = config;
267
+ this.minRpm = Math.floor(config.requestsPerMinute * 0.2);
268
+ this.maxRpm = Math.floor(config.requestsPerMinute * 1.5);
269
+ this.state = {
270
+ tokens: config.requestsPerMinute,
271
+ lastRefill: Date.now(),
272
+ currentRpm: config.requestsPerMinute,
273
+ consecutiveSuccesses: 0,
274
+ consecutiveFailures: 0
275
+ };
276
+ }
277
+ /**
278
+ * Wait until a token is available and consume it
279
+ */
280
+ async acquire() {
281
+ this.refillTokens();
282
+ while (this.state.tokens < 1) {
283
+ const waitTime = this.calculateWaitTime();
284
+ await sleep(waitTime);
285
+ this.refillTokens();
286
+ }
287
+ this.state.tokens -= 1;
288
+ }
289
+ /**
290
+ * Report a successful request (for adaptive rate limiting)
291
+ */
292
+ reportSuccess() {
293
+ if (!this.config.adaptive) return;
294
+ this.state.consecutiveSuccesses += 1;
295
+ this.state.consecutiveFailures = 0;
296
+ if (this.state.consecutiveSuccesses >= 10) {
297
+ this.adjustRate(1.1);
298
+ this.state.consecutiveSuccesses = 0;
299
+ }
300
+ }
301
+ /**
302
+ * Report a rate limit error (for adaptive rate limiting)
303
+ */
304
+ reportRateLimitError() {
305
+ if (!this.config.adaptive) return;
306
+ this.state.consecutiveFailures += 1;
307
+ this.state.consecutiveSuccesses = 0;
308
+ this.adjustRate(0.7);
309
+ }
310
+ /**
311
+ * Get current rate limit status
312
+ */
313
+ getStatus() {
314
+ this.refillTokens();
315
+ return {
316
+ currentRpm: this.state.currentRpm,
317
+ availableTokens: Math.floor(this.state.tokens)
318
+ };
319
+ }
320
+ refillTokens() {
321
+ const now = Date.now();
322
+ const elapsed = now - this.state.lastRefill;
323
+ const tokensToAdd = elapsed / this.intervalMs * this.state.currentRpm;
324
+ this.state.tokens = Math.min(
325
+ this.state.tokens + tokensToAdd,
326
+ this.state.currentRpm
327
+ );
328
+ this.state.lastRefill = now;
329
+ }
330
+ calculateWaitTime() {
331
+ const tokensNeeded = 1 - this.state.tokens;
332
+ return Math.ceil(tokensNeeded / this.state.currentRpm * this.intervalMs);
333
+ }
334
+ adjustRate(multiplier) {
335
+ const newRpm = Math.floor(this.state.currentRpm * multiplier);
336
+ this.state.currentRpm = Math.max(this.minRpm, Math.min(newRpm, this.maxRpm));
337
+ }
338
+ };
339
+
340
+ // src/config/templates.ts
341
+ var DISCOVERY_TEMPLATE = `You are a document analysis AI. Analyze the provided document and determine the optimal processing strategy.
342
+
343
+ Analyze the document and return ONLY a JSON response with the following structure:
344
+
345
+ {
346
+ "documentType": "Medical|Legal|Financial|Technical|Academic|General",
347
+ "documentTypeName": "Human readable name for this document type",
348
+ "language": "tr|en|de|fr|...",
349
+ "complexity": "low|medium|high",
350
+
351
+ "detectedElements": [
352
+ { "type": "table", "count": 5, "description": "Brief description of tables" },
353
+ { "type": "list", "count": 10, "description": "Brief description of lists" },
354
+ { "type": "code", "count": 0, "description": "" },
355
+ { "type": "image", "count": 3, "description": "Brief description of images" }
356
+ ],
357
+
358
+ "specialInstructions": [
359
+ "Specific instruction 1 for this document type",
360
+ "Specific instruction 2 for this document type",
361
+ "Specific instruction 3 for this document type"
362
+ ],
363
+
364
+ "exampleFormats": {
365
+ "example1": "How a specific format should look",
366
+ "example2": "Another format example"
367
+ },
368
+
369
+ "chunkStrategy": {
370
+ "maxTokens": 800,
371
+ "overlapTokens": 100,
372
+ "splitBy": "section|page|paragraph|semantic",
373
+ "preserveTables": true,
374
+ "preserveLists": true
375
+ },
376
+
377
+ "confidence": 0.85,
378
+ "reasoning": "Brief explanation of why this strategy was chosen"
379
+ }
380
+
381
+ IMPORTANT RULES:
382
+ 1. DO NOT generate a full extraction prompt
383
+ 2. Only provide structured analysis and specific instructions
384
+ 3. Instructions should be actionable and specific to this document type
385
+ 4. Example formats help maintain consistency in extraction
386
+
387
+ {{DOCUMENT_TYPE_HINT}}
388
+ `;
389
+ var BASE_EXTRACTION_TEMPLATE = `You are a document processing AI. Extract content following the EXACT format below.
390
+
391
+ ## OUTPUT FORMAT (MANDATORY - DO NOT MODIFY)
392
+
393
+ Use this structure for EVERY content section:
394
+
395
+ <!-- SECTION type="[TYPE]" page="[PAGE]" confidence="[0.0-1.0]" -->
396
+ [Content here in Markdown format]
397
+ <!-- /SECTION -->
398
+
399
+ ### Valid Types:
400
+ - TEXT: Regular paragraphs and prose
401
+ - TABLE: Data tables in Markdown format
402
+ - LIST: Bullet (-) or numbered (1. 2. 3.) lists
403
+ - HEADING: Section headers with # ## ### levels
404
+ - CODE: Code blocks with language specification
405
+ - QUOTE: Quoted text or citations
406
+ - IMAGE_REF: Description of images, charts, figures
407
+ - QUESTION: Multiple choice questions with options (A, B, C, D, E)
408
+
409
+ ### Format Rules:
410
+ 1. **Tables**: Use Markdown table format
411
+ | Column1 | Column2 | Column3 |
412
+ |---------|---------|---------|
413
+ | data | data | data |
414
+
415
+ 2. **Lists**: Use consistent format
416
+ - Bullet item
417
+ - Another bullet
418
+
419
+ OR
420
+
421
+ 1. Numbered item
422
+ 2. Another numbered
423
+
424
+ 3. **Headings**: Maximum 3 levels, use hierarchy
425
+ # Main Section
426
+ ## Subsection
427
+ ### Sub-subsection
428
+
429
+ 4. **Code**: Specify language
430
+ \`\`\`python
431
+ code here
432
+ \`\`\`
433
+
434
+ 5. **Images**: Describe visual content
435
+ [IMAGE: Description of what the image shows]
436
+
437
+ 6. **Questions**: Multiple choice questions with options
438
+ **Question 1:** Question text here?
439
+ A) Option A text
440
+ B) Option B text
441
+ C) Option C text
442
+ D) Option D text
443
+ E) Option E text (if exists)
444
+ **Answer:** [Letter] (if answer is provided in document)
445
+
446
+ ## DOCUMENT-SPECIFIC INSTRUCTIONS
447
+ {{DOCUMENT_INSTRUCTIONS}}
448
+
449
+ ## CRITICAL EXTRACTION RULES (DO NOT VIOLATE)
450
+ \u26A0\uFE0F These rules are MANDATORY for legal, medical, and financial document accuracy:
451
+
452
+ 1. **NO SUMMARIZATION**: Extract content EXACTLY as written. Do not summarize, paraphrase, or condense.
453
+ 2. **NO INTERPRETATION**: Do not interpret, explain, or add commentary to the content.
454
+ 3. **PRESERVE ORIGINAL WORDING**: Keep exact terminology, especially for:
455
+ - Legal terms, clauses, and article references
456
+ - Medical terminology, diagnoses, and prescriptions
457
+ - Financial figures, percentages, and calculations
458
+ - Technical specifications and measurements
459
+ 4. **VERBATIM EXTRACTION**: Copy text word-for-word from the document.
460
+ 5. **NO OMISSIONS**: Include all content, even if it seems redundant or repetitive.
461
+ 6. **UNCLEAR CONTENT**: If text is unclear or illegible, extract as-is and mark: [UNCLEAR: partial text visible]
462
+ 7. **FOREIGN TERMS**: Keep foreign language terms, Latin phrases, and abbreviations exactly as written.
463
+
464
+ ## PROCESSING RULES
465
+ - Extract ALL content completely, do not summarize or skip
466
+ - Preserve original document structure and hierarchy
467
+ - Include page references for each section
468
+ - Maintain technical accuracy and terminology
469
+ - Use appropriate confidence scores based on extraction quality
470
+ - If content spans multiple pages, use the starting page number
471
+
472
+ ## PAGE RANGE
473
+ {{PAGE_RANGE}}
474
+ `;
475
+ var DEFAULT_DOCUMENT_INSTRUCTIONS = `
476
+ - Extract all text content preserving structure
477
+ - Convert tables to Markdown table format
478
+ - Convert lists to Markdown list format
479
+ - Preserve headings with appropriate # levels
480
+ - Note any images with descriptive text
481
+ - Maintain the logical flow of content
482
+ `;
483
+ function buildExtractionPrompt(documentInstructions, exampleFormats, pageStart, pageEnd) {
484
+ let instructionsBlock = documentInstructions.map((instruction) => `- ${instruction}`).join("\n");
485
+ if (exampleFormats && Object.keys(exampleFormats).length > 0) {
486
+ instructionsBlock += "\n\n### Example Formats:\n";
487
+ for (const [key, value] of Object.entries(exampleFormats)) {
488
+ instructionsBlock += `- **${key}**: \`${value}\`
489
+ `;
490
+ }
491
+ }
492
+ let pageRange = "";
493
+ if (pageStart !== void 0 && pageEnd !== void 0) {
494
+ if (pageStart === pageEnd) {
495
+ pageRange = `Process page ${pageStart} of this document.`;
496
+ } else {
497
+ pageRange = `Process pages ${pageStart}-${pageEnd} of this document.`;
498
+ }
499
+ }
500
+ return BASE_EXTRACTION_TEMPLATE.replace("{{DOCUMENT_INSTRUCTIONS}}", instructionsBlock || DEFAULT_DOCUMENT_INSTRUCTIONS).replace("{{PAGE_RANGE}}", pageRange);
501
+ }
502
+ function buildDiscoveryPrompt(documentTypeHint) {
503
+ let hint = "";
504
+ if (documentTypeHint) {
505
+ hint = `
506
+ Hint: The user expects this to be a "${documentTypeHint}" document. Consider this when analyzing.`;
507
+ }
508
+ return DISCOVERY_TEMPLATE.replace("{{DOCUMENT_TYPE_HINT}}", hint);
509
+ }
510
+ var SECTION_PATTERN = /<!-- SECTION type="(\w+)" page="(\d+)" confidence="([\d.]+)" -->\n?([\s\S]*?)\n?<!-- \/SECTION -->/g;
511
+
512
+ // src/types/enums.ts
513
+ var ChunkTypeEnum = {
514
+ TEXT: "TEXT",
515
+ TABLE: "TABLE",
516
+ LIST: "LIST",
517
+ CODE: "CODE",
518
+ HEADING: "HEADING",
519
+ IMAGE_REF: "IMAGE_REF",
520
+ QUOTE: "QUOTE",
521
+ QUESTION: "QUESTION",
522
+ MIXED: "MIXED"
523
+ };
524
+ var BatchStatusEnum = {
525
+ PENDING: "PENDING",
526
+ PROCESSING: "PROCESSING",
527
+ RETRYING: "RETRYING",
528
+ COMPLETED: "COMPLETED",
529
+ FAILED: "FAILED"
530
+ };
531
+ var DocumentStatusEnum = {
532
+ PENDING: "PENDING",
533
+ DISCOVERING: "DISCOVERING",
534
+ AWAITING_APPROVAL: "AWAITING_APPROVAL",
535
+ PROCESSING: "PROCESSING",
536
+ COMPLETED: "COMPLETED",
537
+ FAILED: "FAILED",
538
+ PARTIAL: "PARTIAL"
539
+ };
540
+ var SearchModeEnum = {
541
+ SEMANTIC: "semantic",
542
+ KEYWORD: "keyword",
543
+ HYBRID: "hybrid"
544
+ };
545
+
546
+ // src/utils/chunk-parser.ts
547
+ function parseSections(aiOutput) {
548
+ const sections = [];
549
+ const regex = new RegExp(SECTION_PATTERN.source, "g");
550
+ let match;
551
+ let index = 0;
552
+ while ((match = regex.exec(aiOutput)) !== null) {
553
+ const typeStr = (match[1] ?? "TEXT").toUpperCase();
554
+ const page = parseInt(match[2] ?? "1", 10);
555
+ const confidence = parseFloat(match[3] ?? "0.5");
556
+ const content = (match[4] ?? "").trim();
557
+ const type = mapToChunkType(typeStr);
558
+ sections.push({
559
+ type,
560
+ page,
561
+ confidence: isNaN(confidence) ? 0.5 : Math.min(1, Math.max(0, confidence)),
562
+ content,
563
+ index: index++
564
+ });
565
+ }
566
+ return sections;
567
+ }
568
+ function mapToChunkType(typeStr) {
569
+ const typeMap = {
570
+ "TEXT": ChunkTypeEnum.TEXT,
571
+ "TABLE": ChunkTypeEnum.TABLE,
572
+ "LIST": ChunkTypeEnum.LIST,
573
+ "CODE": ChunkTypeEnum.CODE,
574
+ "HEADING": ChunkTypeEnum.HEADING,
575
+ "QUOTE": ChunkTypeEnum.QUOTE,
576
+ "IMAGE_REF": ChunkTypeEnum.IMAGE_REF,
577
+ "QUESTION": ChunkTypeEnum.QUESTION,
578
+ "MIXED": ChunkTypeEnum.MIXED
579
+ };
580
+ return typeMap[typeStr] ?? ChunkTypeEnum.TEXT;
581
+ }
582
+ function hasValidSections(aiOutput) {
583
+ const regex = new RegExp(SECTION_PATTERN.source);
584
+ return regex.test(aiOutput);
585
+ }
586
+ function parseFallbackContent(content, pageStart, _pageEnd) {
587
+ const sections = [];
588
+ const parts = content.split(/\n(?=#{1,6}\s)|(?:\n\n)/);
589
+ let index = 0;
590
+ for (const part of parts) {
591
+ const trimmed = part.trim();
592
+ if (!trimmed || trimmed.length < 10) continue;
593
+ sections.push({
594
+ type: detectContentType(trimmed),
595
+ page: pageStart,
596
+ confidence: 0.6,
597
+ // Lower confidence for fallback
598
+ content: trimmed,
599
+ index: index++
600
+ });
601
+ }
602
+ return sections;
603
+ }
604
+ function detectContentType(content) {
605
+ if (content.includes("|") && content.includes("---")) {
606
+ return ChunkTypeEnum.TABLE;
607
+ }
608
+ if (/^[-*]\s/m.test(content) || /^\d+\.\s/m.test(content)) {
609
+ return ChunkTypeEnum.LIST;
610
+ }
611
+ if (content.includes("```")) {
612
+ return ChunkTypeEnum.CODE;
613
+ }
614
+ if (/^#{1,6}\s/.test(content)) {
615
+ return ChunkTypeEnum.HEADING;
616
+ }
617
+ if (content.startsWith(">")) {
618
+ return ChunkTypeEnum.QUOTE;
619
+ }
620
+ if (content.includes("[IMAGE:")) {
621
+ return ChunkTypeEnum.IMAGE_REF;
622
+ }
623
+ if (/[A-E][).]\s/m.test(content) && /[B-E][).]\s/m.test(content)) {
624
+ return ChunkTypeEnum.QUESTION;
625
+ }
626
+ return ChunkTypeEnum.TEXT;
627
+ }
628
+ function cleanForSearch(content) {
629
+ return content.replace(/#{1,6}\s/g, "").replace(/\*\*/g, "").replace(/\*/g, "").replace(/`/g, "").replace(/\|/g, " ").replace(/---+/g, "").replace(/\[IMAGE:[^\]]*\]/g, "").replace(/<!--.*?-->/gs, "").replace(/\s+/g, " ").trim();
630
+ }
631
+
632
+ // src/types/prompt.types.ts
633
+ var DEFAULT_CHUNK_STRATEGY = {
634
+ maxTokens: 500,
635
+ overlapTokens: 50,
636
+ splitBy: "semantic",
637
+ preserveTables: true,
638
+ preserveLists: true,
639
+ extractHeadings: true
640
+ };
641
+
642
+ // src/database/repositories/prompt-config.repository.ts
643
+ var PromptConfigRepository = class {
644
+ constructor(prisma) {
645
+ this.prisma = prisma;
646
+ }
647
+ /**
648
+ * Create a new prompt configuration
649
+ */
650
+ async create(input) {
651
+ try {
652
+ const latestVersion = await this.prisma.contextRagPromptConfig.findFirst({
653
+ where: { documentType: input.documentType },
654
+ orderBy: { version: "desc" },
655
+ select: { version: true }
656
+ });
657
+ const version = (latestVersion?.version ?? 0) + 1;
658
+ if (input.setAsDefault) {
659
+ await this.prisma.contextRagPromptConfig.updateMany({
660
+ where: { documentType: input.documentType, isDefault: true },
661
+ data: { isDefault: false }
662
+ });
663
+ }
664
+ const chunkStrategy = {
665
+ ...DEFAULT_CHUNK_STRATEGY,
666
+ ...input.chunkStrategy
667
+ };
668
+ const created = await this.prisma.contextRagPromptConfig.create({
669
+ data: {
670
+ documentType: input.documentType,
671
+ name: input.name,
672
+ systemPrompt: input.systemPrompt,
673
+ chunkStrategy,
674
+ version,
675
+ isActive: true,
676
+ isDefault: input.setAsDefault ?? false,
677
+ createdBy: "manual",
678
+ changeLog: input.changeLog
679
+ }
680
+ });
681
+ return this.mapToPromptConfig(created);
682
+ } catch (error) {
683
+ throw new DatabaseError("Failed to create prompt config", {
684
+ error: error.message,
685
+ documentType: input.documentType
686
+ });
687
+ }
688
+ }
689
+ /**
690
+ * Get a prompt configuration by ID
691
+ */
692
+ async getById(id) {
693
+ const config = await this.prisma.contextRagPromptConfig.findUnique({
694
+ where: { id }
695
+ });
696
+ if (!config) {
697
+ throw new NotFoundError("PromptConfig", id);
698
+ }
699
+ return this.mapToPromptConfig(config);
700
+ }
701
+ /**
702
+ * Get prompt configurations with optional filters
703
+ */
704
+ async getMany(filters) {
705
+ const where = {};
706
+ if (filters?.documentType) {
707
+ where["documentType"] = filters.documentType;
708
+ }
709
+ if (filters?.activeOnly) {
710
+ where["isActive"] = true;
711
+ }
712
+ if (filters?.defaultOnly) {
713
+ where["isDefault"] = true;
714
+ }
715
+ if (filters?.createdBy) {
716
+ where["createdBy"] = filters.createdBy;
717
+ }
718
+ const configs = await this.prisma.contextRagPromptConfig.findMany({
719
+ where,
720
+ orderBy: [{ documentType: "asc" }, { version: "desc" }]
721
+ });
722
+ return configs.map((c) => this.mapToPromptConfig(c));
723
+ }
724
+ /**
725
+ * Get the active default config for a document type
726
+ */
727
+ async getDefault(documentType) {
728
+ const config = await this.prisma.contextRagPromptConfig.findFirst({
729
+ where: {
730
+ documentType,
731
+ isActive: true,
732
+ isDefault: true
733
+ }
734
+ });
735
+ return config ? this.mapToPromptConfig(config) : null;
736
+ }
737
+ /**
738
+ * Get the latest active config for a document type
739
+ */
740
+ async getLatest(documentType) {
741
+ const config = await this.prisma.contextRagPromptConfig.findFirst({
742
+ where: {
743
+ documentType,
744
+ isActive: true
745
+ },
746
+ orderBy: { version: "desc" }
747
+ });
748
+ return config ? this.mapToPromptConfig(config) : null;
749
+ }
750
+ /**
751
+ * Activate a specific config version
752
+ */
753
+ async activate(id) {
754
+ const config = await this.prisma.contextRagPromptConfig.findUnique({
755
+ where: { id }
756
+ });
757
+ if (!config) {
758
+ throw new NotFoundError("PromptConfig", id);
759
+ }
760
+ await this.prisma.contextRagPromptConfig.updateMany({
761
+ where: {
762
+ documentType: config.documentType,
763
+ id: { not: id }
764
+ },
765
+ data: { isActive: false, isDefault: false }
766
+ });
767
+ await this.prisma.contextRagPromptConfig.update({
768
+ where: { id },
769
+ data: { isActive: true, isDefault: true }
770
+ });
771
+ }
772
+ /**
773
+ * Deactivate a config
774
+ */
775
+ async deactivate(id) {
776
+ await this.prisma.contextRagPromptConfig.update({
777
+ where: { id },
778
+ data: { isActive: false, isDefault: false }
779
+ });
780
+ }
781
+ /**
782
+ * Delete a config (only if no chunks reference it)
783
+ */
784
+ async delete(id) {
785
+ const chunkCount = await this.prisma.contextRagChunk.count({
786
+ where: { promptConfigId: id }
787
+ });
788
+ if (chunkCount > 0) {
789
+ throw new DatabaseError("Cannot delete prompt config with existing chunks", {
790
+ id,
791
+ chunkCount
792
+ });
793
+ }
794
+ await this.prisma.contextRagPromptConfig.delete({
795
+ where: { id }
796
+ });
797
+ }
798
+ /**
799
+ * Map database record to PromptConfig type
800
+ */
801
+ mapToPromptConfig(record) {
802
+ return {
803
+ id: record["id"],
804
+ documentType: record["documentType"],
805
+ name: record["name"],
806
+ systemPrompt: record["systemPrompt"],
807
+ chunkStrategy: record["chunkStrategy"],
808
+ version: record["version"],
809
+ isActive: record["isActive"],
810
+ isDefault: record["isDefault"],
811
+ createdBy: record["createdBy"],
812
+ changeLog: record["changeLog"],
813
+ createdAt: record["createdAt"],
814
+ updatedAt: record["updatedAt"]
815
+ };
816
+ }
817
+ };
818
+
819
+ // src/database/repositories/document.repository.ts
820
+ var DocumentRepository = class {
821
+ constructor(prisma) {
822
+ this.prisma = prisma;
823
+ }
824
+ /**
825
+ * Create a new document record
826
+ */
827
+ async create(input) {
828
+ try {
829
+ const doc = await this.prisma.contextRagDocument.create({
830
+ data: {
831
+ filename: input.filename,
832
+ fileHash: input.fileHash,
833
+ fileSize: input.fileSize,
834
+ pageCount: input.pageCount,
835
+ documentType: input.documentType,
836
+ promptConfigId: input.promptConfigId,
837
+ status: DocumentStatusEnum.PENDING,
838
+ totalBatches: input.totalBatches,
839
+ experimentId: input.experimentId,
840
+ modelName: input.modelName,
841
+ modelConfig: input.modelConfig
842
+ }
843
+ });
844
+ return doc.id;
845
+ } catch (error) {
846
+ if (error.code === "P2002") {
847
+ throw new DatabaseError("Document with this hash and experimentId already exists", {
848
+ fileHash: input.fileHash,
849
+ experimentId: input.experimentId
850
+ });
851
+ }
852
+ throw new DatabaseError("Failed to create document", {
853
+ error: error.message
854
+ });
855
+ }
856
+ }
857
+ /**
858
+ * Get document by ID
859
+ */
860
+ async getById(id) {
861
+ const doc = await this.prisma.contextRagDocument.findUnique({
862
+ where: { id }
863
+ });
864
+ if (!doc) {
865
+ throw new NotFoundError("Document", id);
866
+ }
867
+ return this.mapToDocumentStatus(doc);
868
+ }
869
+ /**
870
+ * Get document by file hash (legacy - returns first match)
871
+ */
872
+ async getByHash(fileHash) {
873
+ const doc = await this.prisma.contextRagDocument.findFirst({
874
+ where: { fileHash }
875
+ });
876
+ return doc ? this.mapToDocumentStatus(doc) : null;
877
+ }
878
+ /**
879
+ * Get document by file hash and experiment ID
880
+ */
881
+ async getByHashAndExperiment(fileHash, experimentId) {
882
+ const doc = await this.prisma.contextRagDocument.findFirst({
883
+ where: {
884
+ fileHash,
885
+ experimentId: experimentId ?? null
886
+ }
887
+ });
888
+ return doc ? this.mapToDocumentStatus(doc) : null;
889
+ }
890
+ /**
891
+ * Update document
892
+ */
893
+ async update(id, input) {
894
+ await this.prisma.contextRagDocument.update({
895
+ where: { id },
896
+ data: input
897
+ });
898
+ }
899
+ /**
900
+ * Increment completed batches count
901
+ */
902
+ async incrementCompleted(id) {
903
+ await this.prisma.contextRagDocument.update({
904
+ where: { id },
905
+ data: {
906
+ completedBatches: { increment: 1 }
907
+ }
908
+ });
909
+ }
910
+ /**
911
+ * Increment failed batches count
912
+ */
913
+ async incrementFailed(id) {
914
+ await this.prisma.contextRagDocument.update({
915
+ where: { id },
916
+ data: {
917
+ failedBatches: { increment: 1 }
918
+ }
919
+ });
920
+ }
921
+ /**
922
+ * Mark document as completed
923
+ */
924
+ async markCompleted(id, tokenUsage, processingMs) {
925
+ const doc = await this.prisma.contextRagDocument.findUnique({
926
+ where: { id },
927
+ select: { failedBatches: true }
928
+ });
929
+ const status = doc?.failedBatches > 0 ? DocumentStatusEnum.PARTIAL : DocumentStatusEnum.COMPLETED;
930
+ await this.prisma.contextRagDocument.update({
931
+ where: { id },
932
+ data: {
933
+ status,
934
+ tokenUsage,
935
+ processingMs,
936
+ completedAt: /* @__PURE__ */ new Date()
937
+ }
938
+ });
939
+ }
940
+ /**
941
+ * Mark document as failed
942
+ */
943
+ async markFailed(id, errorMessage) {
944
+ await this.prisma.contextRagDocument.update({
945
+ where: { id },
946
+ data: {
947
+ status: DocumentStatusEnum.FAILED,
948
+ errorMessage,
949
+ completedAt: /* @__PURE__ */ new Date()
950
+ }
951
+ });
952
+ }
953
+ /**
954
+ * Delete document and all related data
955
+ */
956
+ async delete(id) {
957
+ await this.prisma.contextRagDocument.delete({
958
+ where: { id }
959
+ });
960
+ }
961
+ /**
962
+ * Get documents by status
963
+ */
964
+ async getByStatus(status) {
965
+ const docs = await this.prisma.contextRagDocument.findMany({
966
+ where: { status },
967
+ orderBy: { createdAt: "desc" }
968
+ });
969
+ return docs.map((d) => this.mapToDocumentStatus(d));
970
+ }
971
+ /**
972
+ * Map database record to DocumentStatus type
973
+ */
974
+ mapToDocumentStatus(record) {
975
+ const totalBatches = record["totalBatches"];
976
+ const completedBatches = record["completedBatches"];
977
+ return {
978
+ id: record["id"],
979
+ filename: record["filename"],
980
+ status: record["status"],
981
+ documentType: record["documentType"],
982
+ pageCount: record["pageCount"],
983
+ progress: {
984
+ totalBatches,
985
+ completedBatches,
986
+ failedBatches: record["failedBatches"],
987
+ percentage: totalBatches > 0 ? Math.round(completedBatches / totalBatches * 100) : 0
988
+ },
989
+ tokenUsage: record["tokenUsage"],
990
+ processingMs: record["processingMs"],
991
+ error: record["errorMessage"],
992
+ createdAt: record["createdAt"],
993
+ completedAt: record["completedAt"]
994
+ };
995
+ }
996
+ };
997
+
998
+ // src/database/repositories/batch.repository.ts
999
+ var BatchRepository = class {
1000
+ constructor(prisma) {
1001
+ this.prisma = prisma;
1002
+ }
1003
+ /**
1004
+ * Create multiple batches for a document
1005
+ */
1006
+ async createMany(inputs) {
1007
+ try {
1008
+ await this.prisma.contextRagBatch.createMany({
1009
+ data: inputs.map((input) => ({
1010
+ documentId: input.documentId,
1011
+ batchIndex: input.batchIndex,
1012
+ pageStart: input.pageStart,
1013
+ pageEnd: input.pageEnd,
1014
+ status: BatchStatusEnum.PENDING
1015
+ }))
1016
+ });
1017
+ } catch (error) {
1018
+ throw new DatabaseError("Failed to create batches", {
1019
+ error: error.message
1020
+ });
1021
+ }
1022
+ }
1023
+ /**
1024
+ * Get batch by ID
1025
+ */
1026
+ async getById(id) {
1027
+ const batch = await this.prisma.contextRagBatch.findUnique({
1028
+ where: { id }
1029
+ });
1030
+ if (!batch) {
1031
+ throw new NotFoundError("Batch", id);
1032
+ }
1033
+ return this.mapToBatchRecord(batch);
1034
+ }
1035
+ /**
1036
+ * Get all batches for a document
1037
+ */
1038
+ async getByDocumentId(documentId) {
1039
+ const batches = await this.prisma.contextRagBatch.findMany({
1040
+ where: { documentId },
1041
+ orderBy: { batchIndex: "asc" }
1042
+ });
1043
+ return batches.map((b) => this.mapToBatchRecord(b));
1044
+ }
1045
+ /**
1046
+ * Get pending batches for a document
1047
+ */
1048
+ async getPending(documentId) {
1049
+ const batches = await this.prisma.contextRagBatch.findMany({
1050
+ where: {
1051
+ documentId,
1052
+ status: BatchStatusEnum.PENDING
1053
+ },
1054
+ orderBy: { batchIndex: "asc" }
1055
+ });
1056
+ return batches.map((b) => this.mapToBatchRecord(b));
1057
+ }
1058
+ /**
1059
+ * Get failed batches for retry
1060
+ */
1061
+ async getFailed(documentId, maxRetries) {
1062
+ const batches = await this.prisma.contextRagBatch.findMany({
1063
+ where: {
1064
+ documentId,
1065
+ status: BatchStatusEnum.FAILED,
1066
+ retryCount: { lt: maxRetries }
1067
+ },
1068
+ orderBy: { batchIndex: "asc" }
1069
+ });
1070
+ return batches.map((b) => this.mapToBatchRecord(b));
1071
+ }
1072
+ /**
1073
+ * Mark batch as processing
1074
+ */
1075
+ async markProcessing(id) {
1076
+ await this.prisma.contextRagBatch.update({
1077
+ where: { id },
1078
+ data: {
1079
+ status: BatchStatusEnum.PROCESSING,
1080
+ startedAt: /* @__PURE__ */ new Date()
1081
+ }
1082
+ });
1083
+ }
1084
+ /**
1085
+ * Mark batch as retrying
1086
+ */
1087
+ async markRetrying(id, error) {
1088
+ await this.prisma.contextRagBatch.update({
1089
+ where: { id },
1090
+ data: {
1091
+ status: BatchStatusEnum.RETRYING,
1092
+ lastError: error,
1093
+ retryCount: { increment: 1 }
1094
+ }
1095
+ });
1096
+ }
1097
+ /**
1098
+ * Mark batch as completed
1099
+ */
1100
+ async markCompleted(id, tokenUsage, processingMs) {
1101
+ await this.prisma.contextRagBatch.update({
1102
+ where: { id },
1103
+ data: {
1104
+ status: BatchStatusEnum.COMPLETED,
1105
+ tokenUsage,
1106
+ processingMs,
1107
+ completedAt: /* @__PURE__ */ new Date()
1108
+ }
1109
+ });
1110
+ }
1111
+ /**
1112
+ * Mark batch as failed
1113
+ */
1114
+ async markFailed(id, error) {
1115
+ await this.prisma.contextRagBatch.update({
1116
+ where: { id },
1117
+ data: {
1118
+ status: BatchStatusEnum.FAILED,
1119
+ lastError: error,
1120
+ completedAt: /* @__PURE__ */ new Date()
1121
+ }
1122
+ });
1123
+ }
1124
+ /**
1125
+ * Reset batch for retry (set back to pending)
1126
+ */
1127
+ async resetForRetry(id) {
1128
+ await this.prisma.contextRagBatch.update({
1129
+ where: { id },
1130
+ data: {
1131
+ status: BatchStatusEnum.PENDING,
1132
+ startedAt: null,
1133
+ completedAt: null
1134
+ }
1135
+ });
1136
+ }
1137
+ /**
1138
+ * Map database record to BatchRecord type
1139
+ */
1140
+ mapToBatchRecord(record) {
1141
+ return {
1142
+ id: record["id"],
1143
+ documentId: record["documentId"],
1144
+ batchIndex: record["batchIndex"],
1145
+ pageStart: record["pageStart"],
1146
+ pageEnd: record["pageEnd"],
1147
+ status: record["status"],
1148
+ retryCount: record["retryCount"],
1149
+ lastError: record["lastError"],
1150
+ tokenUsage: record["tokenUsage"],
1151
+ processingMs: record["processingMs"],
1152
+ startedAt: record["startedAt"],
1153
+ completedAt: record["completedAt"],
1154
+ createdAt: record["createdAt"]
1155
+ };
1156
+ }
1157
+ };
1158
+
1159
+ // src/database/repositories/chunk.repository.ts
1160
+ var ChunkRepository = class {
1161
+ constructor(prisma) {
1162
+ this.prisma = prisma;
1163
+ }
1164
+ /**
1165
+ * Create a single chunk with embedding
1166
+ */
1167
+ async create(input, embedding) {
1168
+ try {
1169
+ const result = await this.prisma.$queryRaw`
1170
+ INSERT INTO context_rag_chunks (
1171
+ id, prompt_config_id, document_id, chunk_index, chunk_type,
1172
+ search_content, search_vector, display_content,
1173
+ source_page_start, source_page_end, confidence_score, metadata, created_at
1174
+ ) VALUES (
1175
+ gen_random_uuid(),
1176
+ ${input.promptConfigId},
1177
+ ${input.documentId},
1178
+ ${input.chunkIndex},
1179
+ ${input.chunkType},
1180
+ ${input.searchContent},
1181
+ ${embedding}::vector,
1182
+ ${input.displayContent},
1183
+ ${input.sourcePageStart},
1184
+ ${input.sourcePageEnd},
1185
+ ${input.confidenceScore},
1186
+ ${JSON.stringify(input.metadata)}::jsonb,
1187
+ NOW()
1188
+ )
1189
+ RETURNING id
1190
+ `;
1191
+ return result[0]?.id ?? "";
1192
+ } catch (error) {
1193
+ throw new DatabaseError("Failed to create chunk", {
1194
+ error: error.message
1195
+ });
1196
+ }
1197
+ }
1198
+ /**
1199
+ * Create multiple chunks with embeddings
1200
+ */
1201
+ async createMany(inputs, embeddings) {
1202
+ const ids = [];
1203
+ await this.prisma.$transaction(async (tx) => {
1204
+ for (let i = 0; i < inputs.length; i++) {
1205
+ const input = inputs[i];
1206
+ const embedding = embeddings[i];
1207
+ if (!input || !embedding) continue;
1208
+ const result = await tx.$queryRaw`
1209
+ INSERT INTO context_rag_chunks (
1210
+ id, prompt_config_id, document_id, chunk_index, chunk_type,
1211
+ search_content, search_vector, display_content,
1212
+ source_page_start, source_page_end, confidence_score, metadata, created_at
1213
+ ) VALUES (
1214
+ gen_random_uuid(),
1215
+ ${input.promptConfigId},
1216
+ ${input.documentId},
1217
+ ${input.chunkIndex},
1218
+ ${input.chunkType},
1219
+ ${input.searchContent},
1220
+ ${embedding}::vector,
1221
+ ${input.displayContent},
1222
+ ${input.sourcePageStart},
1223
+ ${input.sourcePageEnd},
1224
+ ${input.confidenceScore},
1225
+ ${JSON.stringify(input.metadata)}::jsonb,
1226
+ NOW()
1227
+ )
1228
+ RETURNING id
1229
+ `;
1230
+ const id = result[0]?.id;
1231
+ if (id) ids.push(id);
1232
+ }
1233
+ });
1234
+ return ids;
1235
+ }
1236
+ /**
1237
+ * Vector similarity search
1238
+ */
1239
+ async searchSemantic(queryEmbedding, limit, filters, minScore) {
1240
+ const whereConditions = [];
1241
+ const params = [queryEmbedding, limit];
1242
+ let paramIndex = 3;
1243
+ if (filters?.documentTypes?.length) {
1244
+ whereConditions.push(`c.document_id IN (
1245
+ SELECT id FROM context_rag_documents WHERE document_type = ANY($${paramIndex})
1246
+ )`);
1247
+ params.push(filters.documentTypes);
1248
+ paramIndex++;
1249
+ }
1250
+ if (filters?.chunkTypes?.length) {
1251
+ whereConditions.push(`c.chunk_type = ANY($${paramIndex})`);
1252
+ params.push(filters.chunkTypes);
1253
+ paramIndex++;
1254
+ }
1255
+ if (filters?.minConfidence !== void 0) {
1256
+ whereConditions.push(`c.confidence_score >= $${paramIndex}`);
1257
+ params.push(filters.minConfidence);
1258
+ paramIndex++;
1259
+ }
1260
+ if (filters?.documentIds?.length) {
1261
+ whereConditions.push(`c.document_id = ANY($${paramIndex})`);
1262
+ params.push(filters.documentIds);
1263
+ paramIndex++;
1264
+ }
1265
+ if (filters?.promptConfigIds?.length) {
1266
+ whereConditions.push(`c.prompt_config_id = ANY($${paramIndex})`);
1267
+ params.push(filters.promptConfigIds);
1268
+ paramIndex++;
1269
+ }
1270
+ const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
1271
+ const scoreThreshold = minScore !== void 0 ? `HAVING 1 - (c.search_vector <=> $1::vector) >= ${minScore}` : "";
1272
+ const query = `
1273
+ SELECT
1274
+ c.id, c.prompt_config_id, c.document_id, c.chunk_index, c.chunk_type,
1275
+ c.search_content, c.display_content,
1276
+ c.source_page_start, c.source_page_end, c.confidence_score,
1277
+ c.metadata, c.created_at,
1278
+ 1 - (c.search_vector <=> $1::vector) as similarity
1279
+ FROM context_rag_chunks c
1280
+ ${whereClause}
1281
+ GROUP BY c.id
1282
+ ${scoreThreshold}
1283
+ ORDER BY c.search_vector <=> $1::vector
1284
+ LIMIT $2
1285
+ `;
1286
+ const results = await this.prisma.$queryRawUnsafe(query, ...params);
1287
+ return results.map((row) => ({
1288
+ chunk: this.mapToVectorChunk(row),
1289
+ similarity: row["similarity"]
1290
+ }));
1291
+ }
1292
+ /**
1293
+ * Full-text keyword search
1294
+ */
1295
+ async searchKeyword(query, limit, filters) {
1296
+ const whereConditions = [
1297
+ `to_tsvector('english', c.search_content) @@ plainto_tsquery('english', $1)`
1298
+ ];
1299
+ const params = [query, limit];
1300
+ let paramIndex = 3;
1301
+ if (filters?.chunkTypes?.length) {
1302
+ whereConditions.push(`c.chunk_type = ANY($${paramIndex})`);
1303
+ params.push(filters.chunkTypes);
1304
+ paramIndex++;
1305
+ }
1306
+ if (filters?.documentIds?.length) {
1307
+ whereConditions.push(`c.document_id = ANY($${paramIndex})`);
1308
+ params.push(filters.documentIds);
1309
+ paramIndex++;
1310
+ }
1311
+ const whereClause = `WHERE ${whereConditions.join(" AND ")}`;
1312
+ const queryStr = `
1313
+ SELECT
1314
+ c.id, c.prompt_config_id, c.document_id, c.chunk_index, c.chunk_type,
1315
+ c.search_content, c.display_content,
1316
+ c.source_page_start, c.source_page_end, c.confidence_score,
1317
+ c.metadata, c.created_at,
1318
+ ts_rank(to_tsvector('english', c.search_content), plainto_tsquery('english', $1)) as similarity
1319
+ FROM context_rag_chunks c
1320
+ ${whereClause}
1321
+ ORDER BY similarity DESC
1322
+ LIMIT $2
1323
+ `;
1324
+ const results = await this.prisma.$queryRawUnsafe(queryStr, ...params);
1325
+ return results.map((row) => ({
1326
+ chunk: this.mapToVectorChunk(row),
1327
+ similarity: row["similarity"]
1328
+ }));
1329
+ }
1330
+ /**
1331
+ * Get chunks by document ID
1332
+ */
1333
+ async getByDocumentId(documentId) {
1334
+ const chunks = await this.prisma.contextRagChunk.findMany({
1335
+ where: { documentId },
1336
+ orderBy: { chunkIndex: "asc" }
1337
+ });
1338
+ return chunks.map((c) => this.mapToVectorChunk(c));
1339
+ }
1340
+ /**
1341
+ * Delete chunks by document ID
1342
+ */
1343
+ async deleteByDocumentId(documentId) {
1344
+ const result = await this.prisma.contextRagChunk.deleteMany({
1345
+ where: { documentId }
1346
+ });
1347
+ return result.count;
1348
+ }
1349
+ /**
1350
+ * Count chunks by document ID
1351
+ */
1352
+ async countByDocumentId(documentId) {
1353
+ return await this.prisma.contextRagChunk.count({
1354
+ where: { documentId }
1355
+ });
1356
+ }
1357
+ /**
1358
+ * Map database record to VectorChunk type
1359
+ */
1360
+ mapToVectorChunk(record) {
1361
+ return {
1362
+ id: record["id"],
1363
+ promptConfigId: record["prompt_config_id"] ?? record["promptConfigId"],
1364
+ documentId: record["document_id"] ?? record["documentId"],
1365
+ chunkIndex: record["chunk_index"] ?? record["chunkIndex"],
1366
+ chunkType: record["chunk_type"] ?? record["chunkType"],
1367
+ searchContent: record["search_content"] ?? record["searchContent"],
1368
+ displayContent: record["display_content"] ?? record["displayContent"],
1369
+ sourcePageStart: record["source_page_start"] ?? record["sourcePageStart"],
1370
+ sourcePageEnd: record["source_page_end"] ?? record["sourcePageEnd"],
1371
+ confidenceScore: record["confidence_score"] ?? record["confidenceScore"],
1372
+ metadata: record["metadata"],
1373
+ createdAt: record["created_at"] ?? record["createdAt"]
1374
+ };
1375
+ }
1376
+ };
1377
+
1378
+ // src/database/utils.ts
1379
+ async function checkPgVectorExtension(prisma) {
1380
+ try {
1381
+ const result = await prisma.$queryRaw`
1382
+ SELECT EXISTS (
1383
+ SELECT 1 FROM pg_extension WHERE extname = 'vector'
1384
+ ) as exists
1385
+ `;
1386
+ return result[0]?.exists ?? false;
1387
+ } catch (error) {
1388
+ throw new DatabaseError("Failed to check pgvector extension", {
1389
+ error: error.message
1390
+ });
1391
+ }
1392
+ }
1393
+ async function checkDatabaseConnection(prisma) {
1394
+ try {
1395
+ await prisma.$queryRaw`SELECT 1`;
1396
+ return true;
1397
+ } catch {
1398
+ return false;
1399
+ }
1400
+ }
1401
+ async function getDatabaseStats(prisma) {
1402
+ try {
1403
+ const [documents, chunks, promptConfigs, batches] = await Promise.all([
1404
+ prisma.contextRagDocument.count(),
1405
+ prisma.contextRagChunk.count(),
1406
+ prisma.contextRagPromptConfig.count(),
1407
+ prisma.contextRagBatch.count()
1408
+ ]);
1409
+ const storageResult = await prisma.$queryRaw`
1410
+ SELECT
1411
+ COALESCE(SUM(pg_total_relation_size(quote_ident(tablename)::regclass)), 0) as total_bytes
1412
+ FROM pg_tables
1413
+ WHERE tablename LIKE 'context_rag_%'
1414
+ `;
1415
+ const totalStorageBytes = Number(storageResult[0]?.total_bytes ?? 0);
1416
+ return {
1417
+ documents,
1418
+ chunks,
1419
+ promptConfigs,
1420
+ batches,
1421
+ totalStorageBytes
1422
+ };
1423
+ } catch (error) {
1424
+ throw new DatabaseError("Failed to get database stats", {
1425
+ error: error.message
1426
+ });
1427
+ }
1428
+ }
1429
+ var GeminiService = class {
1430
+ genAI;
1431
+ fileManager;
1432
+ model;
1433
+ embeddingModel;
1434
+ config;
1435
+ rateLimiter;
1436
+ logger;
1437
+ constructor(config, rateLimiter, logger) {
1438
+ this.genAI = new GoogleGenerativeAI(config.geminiApiKey);
1439
+ this.fileManager = new GoogleAIFileManager(config.geminiApiKey);
1440
+ this.model = this.genAI.getGenerativeModel({ model: config.model });
1441
+ this.embeddingModel = this.genAI.getGenerativeModel({ model: config.embeddingModel });
1442
+ this.config = config;
1443
+ this.rateLimiter = rateLimiter;
1444
+ this.logger = logger;
1445
+ }
1446
+ /**
1447
+ * Generate text content
1448
+ */
1449
+ async generate(systemPrompt, userContent, options) {
1450
+ await this.rateLimiter.acquire();
1451
+ try {
1452
+ const result = await this.model.generateContent({
1453
+ contents: [
1454
+ {
1455
+ role: "user",
1456
+ parts: [{ text: `${systemPrompt}
1457
+
1458
+ ${userContent}` }]
1459
+ }
1460
+ ],
1461
+ generationConfig: {
1462
+ temperature: options?.temperature ?? this.config.generationConfig.temperature,
1463
+ maxOutputTokens: options?.maxOutputTokens ?? this.config.generationConfig.maxOutputTokens
1464
+ }
1465
+ });
1466
+ const response = result.response;
1467
+ const text = response.text();
1468
+ const usage = response.usageMetadata;
1469
+ this.rateLimiter.reportSuccess();
1470
+ return {
1471
+ text,
1472
+ tokenUsage: {
1473
+ input: usage?.promptTokenCount ?? 0,
1474
+ output: usage?.candidatesTokenCount ?? 0,
1475
+ total: usage?.totalTokenCount ?? 0
1476
+ }
1477
+ };
1478
+ } catch (error) {
1479
+ this.handleError(error);
1480
+ throw error;
1481
+ }
1482
+ }
1483
+ /**
1484
+ * Generate content with vision (PDF pages as images)
1485
+ */
1486
+ async generateWithVision(systemPrompt, parts, options) {
1487
+ await this.rateLimiter.acquire();
1488
+ try {
1489
+ const result = await this.model.generateContent({
1490
+ contents: [
1491
+ {
1492
+ role: "user",
1493
+ parts: [{ text: systemPrompt }, ...parts]
1494
+ }
1495
+ ],
1496
+ generationConfig: {
1497
+ temperature: options?.temperature ?? this.config.generationConfig.temperature,
1498
+ maxOutputTokens: options?.maxOutputTokens ?? this.config.generationConfig.maxOutputTokens
1499
+ }
1500
+ });
1501
+ const response = result.response;
1502
+ const text = response.text();
1503
+ const usage = response.usageMetadata;
1504
+ this.rateLimiter.reportSuccess();
1505
+ return {
1506
+ text,
1507
+ tokenUsage: {
1508
+ input: usage?.promptTokenCount ?? 0,
1509
+ output: usage?.candidatesTokenCount ?? 0,
1510
+ total: usage?.totalTokenCount ?? 0
1511
+ }
1512
+ };
1513
+ } catch (error) {
1514
+ this.handleError(error);
1515
+ throw error;
1516
+ }
1517
+ }
1518
+ /**
1519
+ * Generate embeddings for text with task type
1520
+ *
1521
+ * Best practices:
1522
+ * - Use RETRIEVAL_DOCUMENT for documents being indexed
1523
+ * - Use RETRIEVAL_QUERY for search queries
1524
+ *
1525
+ * @see https://ai.google.dev/gemini-api/docs/embeddings
1526
+ */
1527
+ async embed(text, taskType = "RETRIEVAL_DOCUMENT") {
1528
+ await this.rateLimiter.acquire();
1529
+ try {
1530
+ const result = await this.embeddingModel.embedContent({
1531
+ content: { parts: [{ text }], role: "user" },
1532
+ taskType: this.mapTaskType(taskType)
1533
+ });
1534
+ this.rateLimiter.reportSuccess();
1535
+ return {
1536
+ embedding: result.embedding.values,
1537
+ tokenCount: text.split(/\s+/).length
1538
+ // Approximate
1539
+ };
1540
+ } catch (error) {
1541
+ this.handleError(error);
1542
+ throw error;
1543
+ }
1544
+ }
1545
+ /**
1546
+ * Generate embeddings for documents (for indexing)
1547
+ * Uses RETRIEVAL_DOCUMENT task type
1548
+ */
1549
+ async embedDocument(text) {
1550
+ return this.embed(text, "RETRIEVAL_DOCUMENT");
1551
+ }
1552
+ /**
1553
+ * Generate embeddings for search query
1554
+ * Uses RETRIEVAL_QUERY task type
1555
+ */
1556
+ async embedQuery(text) {
1557
+ return this.embed(text, "RETRIEVAL_QUERY");
1558
+ }
1559
+ /**
1560
+ * Generate embeddings for multiple documents (batch)
1561
+ * Uses RETRIEVAL_DOCUMENT task type
1562
+ */
1563
+ async embedBatch(texts) {
1564
+ const results = [];
1565
+ for (const text of texts) {
1566
+ const result = await this.embedDocument(text);
1567
+ results.push(result);
1568
+ }
1569
+ return results;
1570
+ }
1571
+ /**
1572
+ * Map our task type enum to Gemini's TaskType
1573
+ */
1574
+ mapTaskType(taskType) {
1575
+ const mapping = {
1576
+ "RETRIEVAL_DOCUMENT": TaskType.RETRIEVAL_DOCUMENT,
1577
+ "RETRIEVAL_QUERY": TaskType.RETRIEVAL_QUERY,
1578
+ "SEMANTIC_SIMILARITY": TaskType.SEMANTIC_SIMILARITY,
1579
+ "CLASSIFICATION": TaskType.CLASSIFICATION,
1580
+ "CLUSTERING": TaskType.CLUSTERING
1581
+ };
1582
+ return mapping[taskType];
1583
+ }
1584
+ /**
1585
+ * Simple text generation (single prompt)
1586
+ * Used for context generation in RAG enhancement
1587
+ */
1588
+ async generateSimple(prompt) {
1589
+ await this.rateLimiter.acquire();
1590
+ try {
1591
+ const result = await this.model.generateContent({
1592
+ contents: [
1593
+ {
1594
+ role: "user",
1595
+ parts: [{ text: prompt }]
1596
+ }
1597
+ ],
1598
+ generationConfig: {
1599
+ temperature: 0.3,
1600
+ maxOutputTokens: 200
1601
+ // Short context
1602
+ }
1603
+ });
1604
+ this.rateLimiter.reportSuccess();
1605
+ return result.response.text().trim();
1606
+ } catch (error) {
1607
+ this.handleError(error);
1608
+ throw error;
1609
+ }
1610
+ }
1611
+ /**
1612
+ * Generate content with file reference (for contextual retrieval)
1613
+ * Uses Gemini's file caching for efficiency
1614
+ */
1615
+ async generateWithFileRef(fileUri, prompt) {
1616
+ await this.rateLimiter.acquire();
1617
+ try {
1618
+ const result = await this.model.generateContent({
1619
+ contents: [
1620
+ {
1621
+ role: "user",
1622
+ parts: [
1623
+ { fileData: { mimeType: "application/pdf", fileUri } },
1624
+ { text: prompt }
1625
+ ]
1626
+ }
1627
+ ],
1628
+ generationConfig: {
1629
+ temperature: 0.3,
1630
+ maxOutputTokens: 200
1631
+ }
1632
+ });
1633
+ this.rateLimiter.reportSuccess();
1634
+ return result.response.text().trim();
1635
+ } catch (error) {
1636
+ this.handleError(error);
1637
+ throw error;
1638
+ }
1639
+ }
1640
+ /**
1641
+ * Upload PDF buffer to Gemini Files API
1642
+ * Returns file URI for use in subsequent requests
1643
+ * File is cached by Gemini for efficient reuse
1644
+ */
1645
+ async uploadPdfBuffer(buffer, filename) {
1646
+ try {
1647
+ const fs2 = await import('fs');
1648
+ const path2 = await import('path');
1649
+ const os = await import('os');
1650
+ const tempPath = path2.join(os.tmpdir(), `context-rag-${Date.now()}-${filename}`);
1651
+ fs2.writeFileSync(tempPath, buffer);
1652
+ this.logger.info("Uploading PDF to Gemini Files API", { filename });
1653
+ const uploadResult = await this.fileManager.uploadFile(tempPath, {
1654
+ mimeType: "application/pdf",
1655
+ displayName: filename
1656
+ });
1657
+ fs2.unlinkSync(tempPath);
1658
+ this.logger.info("PDF uploaded successfully", {
1659
+ fileUri: uploadResult.file.uri,
1660
+ displayName: uploadResult.file.displayName
1661
+ });
1662
+ return uploadResult.file.uri;
1663
+ } catch (error) {
1664
+ this.logger.error("Failed to upload PDF", { error: error.message });
1665
+ throw error;
1666
+ }
1667
+ }
1668
+ /**
1669
+ * Generate content using uploaded PDF URI
1670
+ * Uses Gemini's file caching for efficient context generation
1671
+ */
1672
+ async generateWithPdfUri(pdfUri, prompt, options) {
1673
+ await this.rateLimiter.acquire();
1674
+ try {
1675
+ const result = await this.model.generateContent({
1676
+ contents: [
1677
+ {
1678
+ role: "user",
1679
+ parts: [
1680
+ { fileData: { mimeType: "application/pdf", fileUri: pdfUri } },
1681
+ { text: prompt }
1682
+ ]
1683
+ }
1684
+ ],
1685
+ generationConfig: {
1686
+ temperature: options?.temperature ?? 0.3,
1687
+ maxOutputTokens: options?.maxOutputTokens ?? 200
1688
+ }
1689
+ });
1690
+ const response = result.response;
1691
+ const text = response.text().trim();
1692
+ const usage = response.usageMetadata;
1693
+ this.rateLimiter.reportSuccess();
1694
+ return {
1695
+ text,
1696
+ tokenUsage: {
1697
+ input: usage?.promptTokenCount ?? 0,
1698
+ output: usage?.candidatesTokenCount ?? 0,
1699
+ total: usage?.totalTokenCount ?? 0
1700
+ }
1701
+ };
1702
+ } catch (error) {
1703
+ this.handleError(error);
1704
+ throw error;
1705
+ }
1706
+ }
1707
+ /**
1708
+ * Handle API errors
1709
+ */
1710
+ handleError(error) {
1711
+ const message = error.message.toLowerCase();
1712
+ if (message.includes("429") || message.includes("rate limit")) {
1713
+ this.rateLimiter.reportRateLimitError();
1714
+ throw new RateLimitError("Gemini API rate limit exceeded");
1715
+ }
1716
+ this.logger.error("Gemini API error", {
1717
+ error: error.message
1718
+ });
1719
+ }
1720
+ };
1721
+ var PDFProcessor = class {
1722
+ logger;
1723
+ constructor(logger) {
1724
+ this.logger = logger;
1725
+ }
1726
+ /**
1727
+ * Load PDF from file path or buffer
1728
+ */
1729
+ async load(input) {
1730
+ let buffer;
1731
+ let filename;
1732
+ if (typeof input === "string") {
1733
+ buffer = await fs.readFile(input);
1734
+ filename = path.basename(input);
1735
+ } else {
1736
+ buffer = input;
1737
+ filename = "document.pdf";
1738
+ }
1739
+ const fileHash = hashBuffer(buffer);
1740
+ const fileSize = buffer.length;
1741
+ const pdfData = await pdf(buffer);
1742
+ const pageCount = pdfData.numpages;
1743
+ this.logger.debug("PDF loaded", {
1744
+ filename,
1745
+ fileSize,
1746
+ pageCount
1747
+ });
1748
+ return {
1749
+ buffer,
1750
+ metadata: {
1751
+ filename,
1752
+ fileHash,
1753
+ fileSize,
1754
+ pageCount,
1755
+ title: pdfData.info?.Title,
1756
+ author: pdfData.info?.Author
1757
+ }
1758
+ };
1759
+ }
1760
+ /**
1761
+ * Extract text from all pages
1762
+ */
1763
+ async extractText(buffer) {
1764
+ const pdfData = await pdf(buffer);
1765
+ return [{
1766
+ pageNumber: 1,
1767
+ text: pdfData.text
1768
+ }];
1769
+ }
1770
+ /**
1771
+ * Convert PDF buffer to base64 for Gemini Vision API
1772
+ */
1773
+ toBase64(buffer) {
1774
+ return buffer.toString("base64");
1775
+ }
1776
+ /**
1777
+ * Create Gemini vision part from PDF
1778
+ */
1779
+ createVisionPart(buffer) {
1780
+ return {
1781
+ inlineData: {
1782
+ mimeType: "application/pdf",
1783
+ data: this.toBase64(buffer)
1784
+ }
1785
+ };
1786
+ }
1787
+ /**
1788
+ * Split document into batches
1789
+ */
1790
+ createBatches(pageCount, pagesPerBatch) {
1791
+ const batches = [];
1792
+ for (let i = 0; i < pageCount; i += pagesPerBatch) {
1793
+ batches.push({
1794
+ batchIndex: batches.length,
1795
+ pageStart: i + 1,
1796
+ // 1-indexed
1797
+ pageEnd: Math.min(i + pagesPerBatch, pageCount)
1798
+ });
1799
+ }
1800
+ this.logger.debug("Created batches", {
1801
+ pageCount,
1802
+ pagesPerBatch,
1803
+ batchCount: batches.length
1804
+ });
1805
+ return batches;
1806
+ }
1807
+ /**
1808
+ * Get page range description for prompts
1809
+ */
1810
+ getPageRangeDescription(pageStart, pageEnd) {
1811
+ if (pageStart === pageEnd) {
1812
+ return `page ${pageStart}`;
1813
+ }
1814
+ return `pages ${pageStart}-${pageEnd}`;
1815
+ }
1816
+ };
1817
+
1818
+ // src/enhancements/no-op.handler.ts
1819
+ var NoOpHandler = class {
1820
+ shouldSkip() {
1821
+ return true;
1822
+ }
1823
+ async generateContext() {
1824
+ return "";
1825
+ }
1826
+ };
1827
+
1828
+ // src/types/rag-enhancement.types.ts
1829
+ var DEFAULT_ANTHROPIC_CONFIG = {
1830
+ skipChunkTypes: ["HEADING", "IMAGE_REF"],
1831
+ concurrencyLimit: 5,
1832
+ template: "[{documentType}] [{chunkType}] Page {page}",
1833
+ contextPrompt: "Bu par\xE7ay\u0131 belgede konumland\u0131r. Par\xE7an\u0131n ne hakk\u0131nda oldu\u011Funu ve belgede nerede bulundu\u011Funu 1-2 c\xFCmle ile T\xFCrk\xE7e a\xE7\u0131kla:"
1834
+ };
1835
+ var AnthropicHandler = class {
1836
+ config;
1837
+ gemini;
1838
+ limit;
1839
+ skipTypes;
1840
+ constructor(config, gemini) {
1841
+ this.config = config;
1842
+ this.gemini = gemini;
1843
+ this.limit = pLimit(config.concurrencyLimit ?? DEFAULT_ANTHROPIC_CONFIG.concurrencyLimit);
1844
+ this.skipTypes = new Set(config.skipChunkTypes ?? DEFAULT_ANTHROPIC_CONFIG.skipChunkTypes);
1845
+ }
1846
+ shouldSkip(chunkType) {
1847
+ return this.skipTypes.has(chunkType);
1848
+ }
1849
+ async generateContext(chunk, doc) {
1850
+ if (this.shouldSkip(chunk.chunkType)) {
1851
+ return "";
1852
+ }
1853
+ switch (this.config.strategy) {
1854
+ case "none":
1855
+ return "";
1856
+ case "simple":
1857
+ return this.generateSimpleContext(chunk, doc);
1858
+ case "llm":
1859
+ return this.limit(() => this.generateLLMContext(chunk, doc));
1860
+ default:
1861
+ return "";
1862
+ }
1863
+ }
1864
+ /**
1865
+ * Simple template-based context generation (free)
1866
+ */
1867
+ generateSimpleContext(chunk, doc) {
1868
+ const template = this.config.template ?? DEFAULT_ANTHROPIC_CONFIG.template;
1869
+ return template.replace("{documentType}", doc.documentType ?? "Document").replace("{chunkType}", chunk.chunkType).replace("{page}", String(chunk.page)).replace("{parentHeading}", chunk.parentHeading ?? "");
1870
+ }
1871
+ /**
1872
+ * LLM-based context generation (best quality, ~$0.005/chunk)
1873
+ */
1874
+ async generateLLMContext(chunk, doc) {
1875
+ const prompt = this.config.contextPrompt ?? DEFAULT_ANTHROPIC_CONFIG.contextPrompt;
1876
+ const fullPrompt = `${prompt}
1877
+
1878
+ <document_info>
1879
+ Dosya: ${doc.filename}
1880
+ Tip: ${doc.documentType ?? "Bilinmiyor"}
1881
+ Toplam Sayfa: ${doc.pageCount}
1882
+ </document_info>
1883
+
1884
+ ${doc.fullDocumentText ? `<full_document>
1885
+ ${doc.fullDocumentText.slice(0, 15e3)}
1886
+ </full_document>
1887
+
1888
+ ` : ""}<chunk_to_contextualize>
1889
+ ${chunk.content}
1890
+ </chunk_to_contextualize>
1891
+
1892
+ Bu chunk'\u0131n belgede nerede oldu\u011Funu ve ne hakk\u0131nda oldu\u011Funu 1-2 c\xFCmle ile T\xFCrk\xE7e a\xE7\u0131kla:`;
1893
+ try {
1894
+ if (doc.fileUri) {
1895
+ const chunkPrompt = `Bu chunk'\u0131n belgede nerede oldu\u011Funu ve ne hakk\u0131nda oldu\u011Funu 1-2 c\xFCmle ile T\xFCrk\xE7e a\xE7\u0131kla:
1896
+
1897
+ <chunk>
1898
+ ${chunk.content}
1899
+ </chunk>`;
1900
+ const result2 = await this.gemini.generateWithPdfUri(doc.fileUri, chunkPrompt);
1901
+ return result2.text;
1902
+ }
1903
+ const result = await this.gemini.generateSimple(fullPrompt);
1904
+ return result;
1905
+ } catch (error) {
1906
+ console.warn("LLM context generation failed, using simple context:", error);
1907
+ return this.generateSimpleContext(chunk, doc);
1908
+ }
1909
+ }
1910
+ };
1911
+
1912
+ // src/enhancements/enhancement-registry.ts
1913
+ function createEnhancementHandler(config, resolvedConfig, gemini) {
1914
+ if (!config || config.approach === "none") {
1915
+ return new NoOpHandler();
1916
+ }
1917
+ switch (config.approach) {
1918
+ case "anthropic_contextual":
1919
+ return new AnthropicHandler(config, gemini);
1920
+ case "google_grounding":
1921
+ throw new Error("Google Grounding is not yet implemented");
1922
+ case "custom":
1923
+ return new CustomHandler(config.handler, config.skipChunkTypes);
1924
+ default:
1925
+ throw new Error(`Unknown RAG enhancement approach: ${config.approach}`);
1926
+ }
1927
+ }
1928
+ var CustomHandler = class {
1929
+ constructor(handler, skipChunkTypes) {
1930
+ this.handler = handler;
1931
+ this.skipChunkTypes = skipChunkTypes;
1932
+ }
1933
+ shouldSkip(chunkType) {
1934
+ return this.skipChunkTypes?.includes(chunkType) ?? false;
1935
+ }
1936
+ async generateContext(chunk, doc) {
1937
+ if (this.shouldSkip(chunk.chunkType)) {
1938
+ return "";
1939
+ }
1940
+ return this.handler({ chunk, doc });
1941
+ }
1942
+ };
1943
+
1944
+ // src/engines/ingestion.engine.ts
1945
+ var IngestionEngine = class {
1946
+ config;
1947
+ prisma;
1948
+ gemini;
1949
+ pdfProcessor;
1950
+ documentRepo;
1951
+ batchRepo;
1952
+ chunkRepo;
1953
+ promptConfigRepo;
1954
+ logger;
1955
+ enhancementHandler;
1956
+ constructor(config, rateLimiter, logger) {
1957
+ this.config = config;
1958
+ this.prisma = config.prisma;
1959
+ this.gemini = new GeminiService(config, rateLimiter, logger);
1960
+ this.pdfProcessor = new PDFProcessor(logger);
1961
+ this.documentRepo = new DocumentRepository(this.prisma);
1962
+ this.batchRepo = new BatchRepository(this.prisma);
1963
+ this.chunkRepo = new ChunkRepository(this.prisma);
1964
+ this.promptConfigRepo = new PromptConfigRepository(this.prisma);
1965
+ this.logger = logger;
1966
+ this.enhancementHandler = createEnhancementHandler(
1967
+ config.ragEnhancement,
1968
+ config,
1969
+ this.gemini
1970
+ );
1971
+ }
1972
+ /**
1973
+ * Ingest a document
1974
+ */
1975
+ async ingest(options) {
1976
+ const startTime = Date.now();
1977
+ this.logger.info("Starting ingestion", {
1978
+ documentType: options.documentType
1979
+ });
1980
+ const { buffer, metadata } = await this.pdfProcessor.load(options.file);
1981
+ const fileUri = await this.gemini.uploadPdfBuffer(buffer, metadata.filename);
1982
+ if (options.skipExisting) {
1983
+ const existing = await this.documentRepo.getByHashAndExperiment(
1984
+ metadata.fileHash,
1985
+ options.experimentId
1986
+ );
1987
+ if (existing) {
1988
+ this.logger.info("Document already exists for this experiment, skipping", {
1989
+ documentId: existing.id,
1990
+ experimentId: options.experimentId
1991
+ });
1992
+ return {
1993
+ documentId: existing.id,
1994
+ status: existing.status,
1995
+ chunkCount: 0,
1996
+ batchCount: existing.progress.totalBatches,
1997
+ failedBatchCount: existing.progress.failedBatches,
1998
+ tokenUsage: existing.tokenUsage ?? { input: 0, output: 0, total: 0 },
1999
+ processingMs: 0,
2000
+ batches: [],
2001
+ warnings: ["Document already exists for this experiment, skipped processing"]
2002
+ };
2003
+ }
2004
+ }
2005
+ let documentInstructions = [];
2006
+ let exampleFormats;
2007
+ let promptConfigId = options.promptConfigId;
2008
+ if (!promptConfigId && options.documentType) {
2009
+ const promptConfig = await this.promptConfigRepo.getDefault(options.documentType);
2010
+ if (promptConfig) {
2011
+ promptConfigId = promptConfig.id;
2012
+ documentInstructions = promptConfig.systemPrompt.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
2013
+ }
2014
+ }
2015
+ if (options.customPrompt) {
2016
+ documentInstructions = options.customPrompt.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
2017
+ } else if (documentInstructions.length === 0) {
2018
+ documentInstructions = DEFAULT_DOCUMENT_INSTRUCTIONS.split("\n").map((line) => line.replace(/^-\s*/, "").trim()).filter((line) => line.length > 0);
2019
+ }
2020
+ const batchSpecs = this.pdfProcessor.createBatches(
2021
+ metadata.pageCount,
2022
+ this.config.batchConfig.pagesPerBatch
2023
+ );
2024
+ const documentId = await this.documentRepo.create({
2025
+ filename: options.filename ?? metadata.filename,
2026
+ fileHash: metadata.fileHash,
2027
+ fileSize: metadata.fileSize,
2028
+ pageCount: metadata.pageCount,
2029
+ documentType: options.documentType,
2030
+ promptConfigId,
2031
+ totalBatches: batchSpecs.length,
2032
+ experimentId: options.experimentId,
2033
+ modelName: this.config.model,
2034
+ modelConfig: {
2035
+ temperature: this.config.generationConfig?.temperature,
2036
+ maxOutputTokens: this.config.generationConfig?.maxOutputTokens
2037
+ }
2038
+ });
2039
+ await this.batchRepo.createMany(
2040
+ batchSpecs.map((spec) => ({
2041
+ documentId,
2042
+ batchIndex: spec.batchIndex,
2043
+ pageStart: spec.pageStart,
2044
+ pageEnd: spec.pageEnd
2045
+ }))
2046
+ );
2047
+ await this.documentRepo.update(documentId, {
2048
+ status: DocumentStatusEnum.PROCESSING
2049
+ });
2050
+ const batchResults = await this.processBatchesConcurrently(
2051
+ documentId,
2052
+ // buffer removed
2053
+ documentInstructions,
2054
+ exampleFormats,
2055
+ promptConfigId ?? "default",
2056
+ fileUri,
2057
+ metadata.filename,
2058
+ options.onProgress
2059
+ );
2060
+ const totalTokenUsage = {
2061
+ input: 0,
2062
+ output: 0,
2063
+ total: 0
2064
+ };
2065
+ let totalChunks = 0;
2066
+ let failedCount = 0;
2067
+ for (const result of batchResults) {
2068
+ totalTokenUsage.input += result.tokenUsage.input;
2069
+ totalTokenUsage.output += result.tokenUsage.output;
2070
+ totalTokenUsage.total += result.tokenUsage.total;
2071
+ totalChunks += result.chunksCreated;
2072
+ if (result.status === BatchStatusEnum.FAILED) {
2073
+ failedCount++;
2074
+ }
2075
+ }
2076
+ const processingMs = Date.now() - startTime;
2077
+ await this.documentRepo.markCompleted(documentId, totalTokenUsage, processingMs);
2078
+ const status = failedCount > 0 ? DocumentStatusEnum.PARTIAL : DocumentStatusEnum.COMPLETED;
2079
+ this.logger.info("Ingestion completed", {
2080
+ documentId,
2081
+ status,
2082
+ chunkCount: totalChunks,
2083
+ batchCount: batchSpecs.length,
2084
+ failedBatchCount: failedCount,
2085
+ processingMs
2086
+ });
2087
+ return {
2088
+ documentId,
2089
+ status,
2090
+ chunkCount: totalChunks,
2091
+ batchCount: batchSpecs.length,
2092
+ failedBatchCount: failedCount,
2093
+ tokenUsage: totalTokenUsage,
2094
+ processingMs,
2095
+ batches: batchResults,
2096
+ warnings: failedCount > 0 ? [`${failedCount} batch(es) failed to process`] : void 0
2097
+ };
2098
+ }
2099
+ /**
2100
+ * Process batches with concurrency control
2101
+ */
2102
+ async processBatchesConcurrently(documentId, documentInstructions, exampleFormats, promptConfigId, fileUri, filename, onProgress) {
2103
+ const batches = await this.batchRepo.getByDocumentId(documentId);
2104
+ const results = [];
2105
+ const { maxConcurrency } = this.config.batchConfig;
2106
+ for (let i = 0; i < batches.length; i += maxConcurrency) {
2107
+ const currentBatch = batches.slice(i, i + maxConcurrency);
2108
+ const batchPromises = currentBatch.map(
2109
+ (batch) => this.processSingleBatch(
2110
+ batch,
2111
+ // pdfBuffer removed
2112
+ documentInstructions,
2113
+ exampleFormats,
2114
+ promptConfigId,
2115
+ fileUri,
2116
+ filename,
2117
+ documentId,
2118
+ batches.length,
2119
+ onProgress
2120
+ )
2121
+ );
2122
+ const batchResults = await Promise.all(batchPromises);
2123
+ results.push(...batchResults);
2124
+ }
2125
+ return results;
2126
+ }
2127
+ /**
2128
+ * Process a single batch with retry logic
2129
+ */
2130
+ async processSingleBatch(batch, documentInstructions, exampleFormats, promptConfigId, fileUri, filename, documentId, totalBatches, onProgress) {
2131
+ const startTime = Date.now();
2132
+ onProgress?.({
2133
+ current: batch.batchIndex + 1,
2134
+ total: totalBatches,
2135
+ status: BatchStatusEnum.PROCESSING,
2136
+ pageRange: { start: batch.pageStart, end: batch.pageEnd }
2137
+ });
2138
+ await this.batchRepo.markProcessing(batch.id);
2139
+ const retryOptions = getRetryOptions(this.config.batchConfig);
2140
+ let retryCount = 0;
2141
+ try {
2142
+ const result = await withRetry(
2143
+ async () => {
2144
+ const prompt = buildExtractionPrompt(
2145
+ documentInstructions,
2146
+ exampleFormats,
2147
+ batch.pageStart,
2148
+ batch.pageEnd
2149
+ );
2150
+ const fullPrompt = `${prompt}
2151
+
2152
+ IMPORTANT: You have the FULL document. Restrict your extraction STRICTLY to pages ${batch.pageStart} to ${batch.pageEnd}. Do not extract content from other pages.`;
2153
+ const response = await this.gemini.generateWithPdfUri(
2154
+ fileUri,
2155
+ fullPrompt,
2156
+ {
2157
+ temperature: this.config.generationConfig?.temperature,
2158
+ maxOutputTokens: this.config.generationConfig?.maxOutputTokens
2159
+ }
2160
+ );
2161
+ return response;
2162
+ },
2163
+ {
2164
+ ...retryOptions,
2165
+ onRetry: (attempt, error) => {
2166
+ retryCount = attempt;
2167
+ this.logger.warn("Batch retry", {
2168
+ batchId: batch.id,
2169
+ attempt,
2170
+ error: error.message
2171
+ });
2172
+ onProgress?.({
2173
+ current: batch.batchIndex + 1,
2174
+ total: totalBatches,
2175
+ status: BatchStatusEnum.RETRYING,
2176
+ pageRange: { start: batch.pageStart, end: batch.pageEnd },
2177
+ retryCount: attempt
2178
+ });
2179
+ }
2180
+ }
2181
+ );
2182
+ const chunks = this.parseContentToChunks(
2183
+ result.text,
2184
+ promptConfigId,
2185
+ documentId,
2186
+ batch.pageStart,
2187
+ batch.pageEnd
2188
+ );
2189
+ const docContext = {
2190
+ documentType: void 0,
2191
+ // Inferred from processing
2192
+ filename,
2193
+ pageCount: batch.pageEnd,
2194
+ // Approximate from batch
2195
+ fileUri
2196
+ // Pass the Files API URI for context generation
2197
+ };
2198
+ for (const chunk of chunks) {
2199
+ const chunkData = {
2200
+ content: chunk.displayContent,
2201
+ searchContent: chunk.searchContent,
2202
+ displayContent: chunk.displayContent,
2203
+ chunkType: chunk.chunkType,
2204
+ page: chunk.sourcePageStart,
2205
+ parentHeading: void 0
2206
+ // Could be extracted from metadata
2207
+ };
2208
+ const context = await this.enhancementHandler.generateContext(chunkData, docContext);
2209
+ if (context) {
2210
+ chunk.contextText = context;
2211
+ const enriched = `${context} ${chunk.searchContent}`;
2212
+ chunk.enrichedContent = enriched;
2213
+ chunk.searchContent = enriched;
2214
+ }
2215
+ }
2216
+ const textsToEmbed = chunks.map(
2217
+ (c) => c.enrichedContent ?? c.searchContent
2218
+ );
2219
+ const embeddings = await this.gemini.embedBatch(textsToEmbed);
2220
+ await this.chunkRepo.createMany(
2221
+ chunks,
2222
+ embeddings.map((e) => e.embedding)
2223
+ );
2224
+ const processingMs = Date.now() - startTime;
2225
+ await this.batchRepo.markCompleted(batch.id, result.tokenUsage, processingMs);
2226
+ await this.documentRepo.incrementCompleted(documentId);
2227
+ onProgress?.({
2228
+ current: batch.batchIndex + 1,
2229
+ total: totalBatches,
2230
+ status: BatchStatusEnum.COMPLETED,
2231
+ pageRange: { start: batch.pageStart, end: batch.pageEnd }
2232
+ });
2233
+ return {
2234
+ batchIndex: batch.batchIndex,
2235
+ status: BatchStatusEnum.COMPLETED,
2236
+ chunksCreated: chunks.length,
2237
+ tokenUsage: result.tokenUsage,
2238
+ processingMs,
2239
+ retryCount
2240
+ };
2241
+ } catch (error) {
2242
+ const errorMessage = error.message;
2243
+ await this.batchRepo.markFailed(batch.id, errorMessage);
2244
+ await this.documentRepo.incrementFailed(documentId);
2245
+ onProgress?.({
2246
+ current: batch.batchIndex + 1,
2247
+ total: totalBatches,
2248
+ status: BatchStatusEnum.FAILED,
2249
+ pageRange: { start: batch.pageStart, end: batch.pageEnd },
2250
+ error: errorMessage
2251
+ });
2252
+ this.logger.error("Batch failed", {
2253
+ batchId: batch.id,
2254
+ error: errorMessage
2255
+ });
2256
+ return {
2257
+ batchIndex: batch.batchIndex,
2258
+ status: BatchStatusEnum.FAILED,
2259
+ chunksCreated: 0,
2260
+ tokenUsage: { input: 0, output: 0, total: 0 },
2261
+ processingMs: Date.now() - startTime,
2262
+ retryCount,
2263
+ error: errorMessage
2264
+ };
2265
+ }
2266
+ }
2267
+ /**
2268
+ * Parse extracted content into chunks
2269
+ * Uses structured <!-- SECTION --> markers when available,
2270
+ * falls back to legacy parsing for compatibility.
2271
+ */
2272
+ parseContentToChunks(content, promptConfigId, documentId, pageStart, pageEnd) {
2273
+ const chunks = [];
2274
+ if (hasValidSections(content)) {
2275
+ const sections2 = parseSections(content);
2276
+ this.logger.debug("Using structured section parser", {
2277
+ sectionCount: sections2.length
2278
+ });
2279
+ for (const section of sections2) {
2280
+ if (section.content.length < 10) continue;
2281
+ chunks.push({
2282
+ promptConfigId,
2283
+ documentId,
2284
+ chunkIndex: section.index,
2285
+ chunkType: section.type,
2286
+ searchContent: cleanForSearch(section.content),
2287
+ displayContent: section.content,
2288
+ sourcePageStart: section.page,
2289
+ sourcePageEnd: section.page,
2290
+ confidenceScore: section.confidence,
2291
+ metadata: {
2292
+ type: section.type,
2293
+ pageRange: { start: section.page, end: section.page },
2294
+ confidence: {
2295
+ score: section.confidence,
2296
+ category: section.confidence >= 0.8 ? "HIGH" : section.confidence >= 0.5 ? "MEDIUM" : "LOW"
2297
+ },
2298
+ parsedWithStructuredMarkers: true
2299
+ }
2300
+ });
2301
+ }
2302
+ return chunks;
2303
+ }
2304
+ this.logger.debug("Using fallback parser (no structured markers found)");
2305
+ const sections = parseFallbackContent(content, pageStart);
2306
+ for (const section of sections) {
2307
+ if (section.content.length < 10) continue;
2308
+ chunks.push({
2309
+ promptConfigId,
2310
+ documentId,
2311
+ chunkIndex: section.index,
2312
+ chunkType: section.type,
2313
+ searchContent: cleanForSearch(section.content),
2314
+ displayContent: section.content,
2315
+ sourcePageStart: pageStart,
2316
+ sourcePageEnd: pageEnd,
2317
+ confidenceScore: section.confidence,
2318
+ metadata: {
2319
+ type: section.type,
2320
+ pageRange: { start: pageStart, end: pageEnd },
2321
+ confidence: { score: section.confidence, category: "MEDIUM" },
2322
+ parsedWithStructuredMarkers: false
2323
+ }
2324
+ });
2325
+ }
2326
+ return chunks;
2327
+ }
2328
+ };
2329
+
2330
+ // src/engines/retrieval.engine.ts
2331
+ var RetrievalEngine = class {
2332
+ chunkRepo;
2333
+ gemini;
2334
+ logger;
2335
+ constructor(config, rateLimiter, logger) {
2336
+ this.chunkRepo = new ChunkRepository(config.prisma);
2337
+ this.gemini = new GeminiService(config, rateLimiter, logger);
2338
+ this.logger = logger;
2339
+ }
2340
+ /**
2341
+ * Search for relevant content
2342
+ * Note: HEADING chunks are excluded by default. Use filters.chunkTypes to include them.
2343
+ */
2344
+ async search(options) {
2345
+ const startTime = Date.now();
2346
+ const mode = options.mode ?? SearchModeEnum.HYBRID;
2347
+ const limit = options.limit ?? 10;
2348
+ const filters = {
2349
+ ...options.filters
2350
+ };
2351
+ if (!filters.chunkTypes) {
2352
+ filters.chunkTypes = ["TEXT", "TABLE", "LIST", "CODE", "QUOTE", "IMAGE_REF", "QUESTION", "MIXED"];
2353
+ }
2354
+ this.logger.debug("Starting search", {
2355
+ query: options.query.substring(0, 50),
2356
+ mode,
2357
+ limit
2358
+ });
2359
+ let results;
2360
+ switch (mode) {
2361
+ case SearchModeEnum.SEMANTIC:
2362
+ results = await this.semanticSearch(options.query, limit, filters, options.minScore);
2363
+ break;
2364
+ case SearchModeEnum.KEYWORD:
2365
+ results = await this.keywordSearch(options.query, limit, filters);
2366
+ break;
2367
+ case SearchModeEnum.HYBRID:
2368
+ default:
2369
+ results = await this.hybridSearch(options.query, limit, filters, options.minScore);
2370
+ break;
2371
+ }
2372
+ if (options.typeBoost) {
2373
+ results = this.applyTypeBoost(results, options.typeBoost);
2374
+ }
2375
+ if (options.includeExplanation) {
2376
+ results = results.map((r) => ({
2377
+ ...r,
2378
+ explanation: {
2379
+ matchType: mode === SearchModeEnum.HYBRID ? "both" : mode === SearchModeEnum.SEMANTIC ? "semantic" : "keyword",
2380
+ rawScores: {
2381
+ semantic: r.score
2382
+ }
2383
+ }
2384
+ }));
2385
+ }
2386
+ this.logger.debug("Search completed", {
2387
+ resultCount: results.length,
2388
+ processingTimeMs: Date.now() - startTime
2389
+ });
2390
+ return results;
2391
+ }
2392
+ /**
2393
+ * Search with full metadata response
2394
+ */
2395
+ async searchWithMetadata(options) {
2396
+ const startTime = Date.now();
2397
+ const results = await this.search(options);
2398
+ return {
2399
+ results,
2400
+ metadata: {
2401
+ totalFound: results.length,
2402
+ processingTimeMs: Date.now() - startTime,
2403
+ searchMode: options.mode ?? SearchModeEnum.HYBRID
2404
+ }
2405
+ };
2406
+ }
2407
+ /**
2408
+ * Semantic search using vector similarity
2409
+ */
2410
+ async semanticSearch(query, limit, filters, minScore) {
2411
+ const { embedding } = await this.gemini.embedQuery(query);
2412
+ const results = await this.chunkRepo.searchSemantic(
2413
+ embedding,
2414
+ limit,
2415
+ filters,
2416
+ minScore
2417
+ );
2418
+ return results.map((r) => ({
2419
+ chunk: r.chunk,
2420
+ score: r.similarity
2421
+ }));
2422
+ }
2423
+ /**
2424
+ * Keyword-based search using full-text search
2425
+ */
2426
+ async keywordSearch(query, limit, filters) {
2427
+ const results = await this.chunkRepo.searchKeyword(query, limit, filters);
2428
+ return results.map((r) => ({
2429
+ chunk: r.chunk,
2430
+ score: r.similarity
2431
+ }));
2432
+ }
2433
+ /**
2434
+ * Hybrid search combining semantic and keyword
2435
+ */
2436
+ async hybridSearch(query, limit, filters, minScore) {
2437
+ const [semanticResults, keywordResults] = await Promise.all([
2438
+ this.semanticSearch(query, limit * 2, filters, minScore),
2439
+ this.keywordSearch(query, limit * 2, filters)
2440
+ ]);
2441
+ const combinedMap = /* @__PURE__ */ new Map();
2442
+ for (const result of semanticResults) {
2443
+ combinedMap.set(result.chunk.id, {
2444
+ ...result,
2445
+ score: result.score * 0.7
2446
+ });
2447
+ }
2448
+ for (const result of keywordResults) {
2449
+ const existing = combinedMap.get(result.chunk.id);
2450
+ if (existing) {
2451
+ existing.score += result.score * 0.3;
2452
+ } else {
2453
+ combinedMap.set(result.chunk.id, {
2454
+ ...result,
2455
+ score: result.score * 0.3
2456
+ });
2457
+ }
2458
+ }
2459
+ const combined = Array.from(combinedMap.values()).sort((a, b) => b.score - a.score).slice(0, limit);
2460
+ return combined;
2461
+ }
2462
+ /**
2463
+ * Apply type-based boosting to results
2464
+ */
2465
+ applyTypeBoost(results, typeBoost) {
2466
+ return results.map((result) => {
2467
+ const boost = typeBoost[result.chunk.chunkType] ?? 1;
2468
+ return {
2469
+ ...result,
2470
+ score: result.score * boost,
2471
+ explanation: result.explanation ? {
2472
+ ...result.explanation,
2473
+ intentBoost: boost !== 1,
2474
+ boostReason: boost !== 1 ? `Type boost for ${result.chunk.chunkType}: ${boost}x` : void 0
2475
+ } : void 0
2476
+ };
2477
+ }).sort((a, b) => b.score - a.score);
2478
+ }
2479
+ };
2480
+
2481
+ // src/engines/discovery.engine.ts
2482
+ var DiscoveryEngine = class {
2483
+ gemini;
2484
+ pdfProcessor;
2485
+ logger;
2486
+ sessions = /* @__PURE__ */ new Map();
2487
+ constructor(config, rateLimiter, logger) {
2488
+ this.gemini = new GeminiService(config, rateLimiter, logger);
2489
+ this.pdfProcessor = new PDFProcessor(logger);
2490
+ this.logger = logger;
2491
+ }
2492
+ /**
2493
+ * Analyze a document and generate processing strategy
2494
+ */
2495
+ async discover(options) {
2496
+ const correlationId = generateCorrelationId();
2497
+ this.logger.info("Starting document discovery", { correlationId });
2498
+ const { buffer, metadata } = await this.pdfProcessor.load(options.file);
2499
+ const fileUri = await this.gemini.uploadPdfBuffer(buffer, metadata.filename);
2500
+ const prompt = buildDiscoveryPrompt(options.documentTypeHint);
2501
+ const response = await this.gemini.generateWithPdfUri(fileUri, prompt);
2502
+ let analysisResult;
2503
+ try {
2504
+ let jsonStr = response.text;
2505
+ const jsonMatch = jsonStr.match(/```json\s*([\s\S]*?)\s*```/) || jsonStr.match(/```\s*([\s\S]*?)\s*```/);
2506
+ if (jsonMatch?.[1]) {
2507
+ jsonStr = jsonMatch[1];
2508
+ }
2509
+ analysisResult = JSON.parse(jsonStr);
2510
+ if (!analysisResult.documentType) {
2511
+ throw new Error("Missing documentType in response");
2512
+ }
2513
+ if (!Array.isArray(analysisResult.specialInstructions)) {
2514
+ analysisResult.specialInstructions = this.getDefaultInstructions();
2515
+ }
2516
+ } catch (parseError) {
2517
+ this.logger.warn("Failed to parse discovery response as JSON, using defaults", {
2518
+ error: parseError.message
2519
+ });
2520
+ analysisResult = {
2521
+ documentType: options.documentTypeHint ?? "General",
2522
+ documentTypeName: options.documentTypeHint ?? "General Document",
2523
+ detectedElements: [],
2524
+ specialInstructions: this.getDefaultInstructions(),
2525
+ chunkStrategy: DEFAULT_CHUNK_STRATEGY,
2526
+ confidence: 0.5,
2527
+ reasoning: "Failed to parse AI response, using default configuration"
2528
+ };
2529
+ }
2530
+ const discoveryResult = {
2531
+ id: correlationId,
2532
+ documentType: analysisResult.documentType,
2533
+ documentTypeName: analysisResult.documentTypeName,
2534
+ detectedElements: analysisResult.detectedElements ?? [],
2535
+ specialInstructions: analysisResult.specialInstructions,
2536
+ exampleFormats: analysisResult.exampleFormats,
2537
+ suggestedChunkStrategy: {
2538
+ ...DEFAULT_CHUNK_STRATEGY,
2539
+ ...analysisResult.chunkStrategy
2540
+ },
2541
+ confidence: analysisResult.confidence ?? 0.5,
2542
+ reasoning: analysisResult.reasoning ?? "",
2543
+ pageCount: metadata.pageCount,
2544
+ fileHash: metadata.fileHash,
2545
+ createdAt: /* @__PURE__ */ new Date(),
2546
+ expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1e3)
2547
+ // 24 hours
2548
+ };
2549
+ this.sessions.set(correlationId, {
2550
+ id: correlationId,
2551
+ result: discoveryResult,
2552
+ fileBuffer: buffer,
2553
+ createdAt: /* @__PURE__ */ new Date(),
2554
+ expiresAt: discoveryResult.expiresAt
2555
+ });
2556
+ this.cleanupSessions();
2557
+ this.logger.info("Discovery completed", {
2558
+ correlationId,
2559
+ documentType: discoveryResult.documentType,
2560
+ confidence: discoveryResult.confidence,
2561
+ instructionCount: discoveryResult.specialInstructions.length
2562
+ });
2563
+ return discoveryResult;
2564
+ }
2565
+ /**
2566
+ * Get stored discovery session
2567
+ */
2568
+ getSession(id) {
2569
+ const session = this.sessions.get(id);
2570
+ if (session && session.expiresAt > /* @__PURE__ */ new Date()) {
2571
+ return session;
2572
+ }
2573
+ return void 0;
2574
+ }
2575
+ /**
2576
+ * Remove a session after approval
2577
+ */
2578
+ removeSession(id) {
2579
+ this.sessions.delete(id);
2580
+ }
2581
+ /**
2582
+ * Clean up expired sessions
2583
+ */
2584
+ cleanupSessions() {
2585
+ const now = /* @__PURE__ */ new Date();
2586
+ for (const [id, session] of this.sessions) {
2587
+ if (session.expiresAt <= now) {
2588
+ this.sessions.delete(id);
2589
+ }
2590
+ }
2591
+ }
2592
+ /**
2593
+ * Get default extraction instructions
2594
+ */
2595
+ getDefaultInstructions() {
2596
+ return [
2597
+ "Extract all text content preserving structure",
2598
+ "Convert tables to Markdown table format",
2599
+ "Convert lists to Markdown list format",
2600
+ "Preserve headings with appropriate # levels",
2601
+ "Note any images with [IMAGE: description]",
2602
+ "Maintain the logical flow of content"
2603
+ ];
2604
+ }
2605
+ };
2606
+
2607
+ // src/context-rag.ts
2608
+ var ContextRAG = class {
2609
+ config;
2610
+ logger;
2611
+ rateLimiter;
2612
+ // Engines
2613
+ ingestionEngine;
2614
+ retrievalEngine;
2615
+ discoveryEngine;
2616
+ // Repositories
2617
+ promptConfigRepo;
2618
+ documentRepo;
2619
+ chunkRepo;
2620
+ constructor(userConfig) {
2621
+ const validation = configSchema.safeParse(userConfig);
2622
+ if (!validation.success) {
2623
+ throw new ConfigurationError("Invalid configuration", {
2624
+ errors: validation.error.errors
2625
+ });
2626
+ }
2627
+ this.config = this.resolveConfig(userConfig);
2628
+ this.logger = createLogger(this.config.logging);
2629
+ this.rateLimiter = new RateLimiter(this.config.rateLimitConfig);
2630
+ this.promptConfigRepo = new PromptConfigRepository(this.config.prisma);
2631
+ this.documentRepo = new DocumentRepository(this.config.prisma);
2632
+ this.chunkRepo = new ChunkRepository(this.config.prisma);
2633
+ this.ingestionEngine = new IngestionEngine(this.config, this.rateLimiter, this.logger);
2634
+ this.retrievalEngine = new RetrievalEngine(this.config, this.rateLimiter, this.logger);
2635
+ this.discoveryEngine = new DiscoveryEngine(this.config, this.rateLimiter, this.logger);
2636
+ this.logger.info("Context-RAG initialized", {
2637
+ model: this.config.model,
2638
+ batchConfig: this.config.batchConfig
2639
+ });
2640
+ }
2641
+ /**
2642
+ * Resolve user config with defaults
2643
+ */
2644
+ resolveConfig(userConfig) {
2645
+ return {
2646
+ prisma: userConfig.prisma,
2647
+ geminiApiKey: userConfig.geminiApiKey,
2648
+ model: userConfig.model ?? "gemini-1.5-pro",
2649
+ embeddingModel: userConfig.embeddingModel ?? "text-embedding-004",
2650
+ generationConfig: {
2651
+ ...DEFAULT_GENERATION_CONFIG,
2652
+ ...userConfig.generationConfig
2653
+ },
2654
+ batchConfig: {
2655
+ ...DEFAULT_BATCH_CONFIG,
2656
+ ...userConfig.batchConfig
2657
+ },
2658
+ chunkConfig: {
2659
+ ...DEFAULT_CHUNK_CONFIG,
2660
+ ...userConfig.chunkConfig
2661
+ },
2662
+ rateLimitConfig: {
2663
+ ...DEFAULT_RATE_LIMIT_CONFIG,
2664
+ ...userConfig.rateLimitConfig
2665
+ },
2666
+ logging: {
2667
+ ...DEFAULT_LOG_CONFIG,
2668
+ ...userConfig.logging
2669
+ }
2670
+ };
2671
+ }
2672
+ /**
2673
+ * Get the resolved configuration
2674
+ */
2675
+ getConfig() {
2676
+ return this.config;
2677
+ }
2678
+ // ============================================
2679
+ // DISCOVERY METHODS
2680
+ // ============================================
2681
+ /**
2682
+ * Analyze a document and get AI-suggested processing strategy
2683
+ */
2684
+ async discover(options) {
2685
+ return this.discoveryEngine.discover(options);
2686
+ }
2687
+ /**
2688
+ * Approve a discovery strategy and create a prompt config
2689
+ */
2690
+ async approveStrategy(strategyId, overrides) {
2691
+ const session = this.discoveryEngine.getSession(strategyId);
2692
+ if (!session) {
2693
+ throw new NotFoundError("Discovery session", strategyId);
2694
+ }
2695
+ const result = session.result;
2696
+ const systemPrompt = overrides?.systemPrompt ?? result.suggestedPrompt ?? result.specialInstructions.join("\n");
2697
+ const promptConfig = await this.promptConfigRepo.create({
2698
+ documentType: overrides?.documentType ?? result.documentType,
2699
+ name: overrides?.name ?? result.documentTypeName,
2700
+ systemPrompt,
2701
+ chunkStrategy: {
2702
+ ...result.suggestedChunkStrategy,
2703
+ ...overrides?.chunkStrategy
2704
+ },
2705
+ setAsDefault: true,
2706
+ changeLog: overrides?.changeLog ?? `Auto-generated from discovery (confidence: ${result.confidence})`
2707
+ });
2708
+ this.discoveryEngine.removeSession(strategyId);
2709
+ this.logger.info("Strategy approved", {
2710
+ strategyId,
2711
+ promptConfigId: promptConfig.id
2712
+ });
2713
+ return promptConfig;
2714
+ }
2715
+ // ============================================
2716
+ // PROMPT CONFIG METHODS
2717
+ // ============================================
2718
+ /**
2719
+ * Create a custom prompt configuration
2720
+ */
2721
+ async createPromptConfig(config) {
2722
+ return this.promptConfigRepo.create(config);
2723
+ }
2724
+ /**
2725
+ * Get prompt configurations
2726
+ */
2727
+ async getPromptConfigs(filters) {
2728
+ return this.promptConfigRepo.getMany(filters);
2729
+ }
2730
+ /**
2731
+ * Update a prompt configuration (creates new version)
2732
+ */
2733
+ async updatePromptConfig(id, updates) {
2734
+ const existing = await this.promptConfigRepo.getById(id);
2735
+ return this.promptConfigRepo.create({
2736
+ documentType: existing.documentType,
2737
+ name: updates.name ?? existing.name,
2738
+ systemPrompt: updates.systemPrompt ?? existing.systemPrompt,
2739
+ chunkStrategy: {
2740
+ ...existing.chunkStrategy,
2741
+ ...updates.chunkStrategy
2742
+ },
2743
+ setAsDefault: true,
2744
+ changeLog: updates.changeLog ?? `Updated from version ${existing.version}`
2745
+ });
2746
+ }
2747
+ /**
2748
+ * Activate a specific prompt config version
2749
+ */
2750
+ async activatePromptConfig(id) {
2751
+ return this.promptConfigRepo.activate(id);
2752
+ }
2753
+ // ============================================
2754
+ // INGESTION METHODS
2755
+ // ============================================
2756
+ /**
2757
+ * Ingest a document into the RAG system
2758
+ */
2759
+ async ingest(options) {
2760
+ return this.ingestionEngine.ingest(options);
2761
+ }
2762
+ /**
2763
+ * Get the status of a document processing job
2764
+ */
2765
+ async getDocumentStatus(documentId) {
2766
+ return this.documentRepo.getById(documentId);
2767
+ }
2768
+ /**
2769
+ * Retry failed batches for a document
2770
+ */
2771
+ async retryFailedBatches(documentId, _options) {
2772
+ const doc = await this.documentRepo.getById(documentId);
2773
+ throw new Error(
2774
+ `Retry not yet fully implemented. Document ${doc.id} has ${doc.progress.failedBatches} failed batches.`
2775
+ );
2776
+ }
2777
+ // ============================================
2778
+ // SEARCH METHODS
2779
+ // ============================================
2780
+ /**
2781
+ * Search for relevant content
2782
+ */
2783
+ async search(options) {
2784
+ return this.retrievalEngine.search(options);
2785
+ }
2786
+ /**
2787
+ * Search with full metadata response
2788
+ */
2789
+ async searchWithMetadata(options) {
2790
+ return this.retrievalEngine.searchWithMetadata(options);
2791
+ }
2792
+ // ============================================
2793
+ // ADMIN METHODS
2794
+ // ============================================
2795
+ /**
2796
+ * Delete a document and all its chunks
2797
+ */
2798
+ async deleteDocument(documentId) {
2799
+ this.logger.info("Deleting document", { documentId });
2800
+ await this.chunkRepo.deleteByDocumentId(documentId);
2801
+ await this.documentRepo.delete(documentId);
2802
+ }
2803
+ /**
2804
+ * Get system statistics
2805
+ */
2806
+ async getStats() {
2807
+ const stats = await getDatabaseStats(this.config.prisma);
2808
+ return {
2809
+ totalDocuments: stats.documents,
2810
+ totalChunks: stats.chunks,
2811
+ promptConfigs: stats.promptConfigs,
2812
+ storageBytes: stats.totalStorageBytes
2813
+ };
2814
+ }
2815
+ /**
2816
+ * Health check
2817
+ */
2818
+ async healthCheck() {
2819
+ const database = await checkDatabaseConnection(this.config.prisma);
2820
+ let pgvector = false;
2821
+ if (database) {
2822
+ try {
2823
+ pgvector = await checkPgVectorExtension(this.config.prisma);
2824
+ } catch {
2825
+ pgvector = false;
2826
+ }
2827
+ }
2828
+ let status;
2829
+ if (database && pgvector) {
2830
+ status = "healthy";
2831
+ } else if (database) {
2832
+ status = "degraded";
2833
+ } else {
2834
+ status = "unhealthy";
2835
+ }
2836
+ return { status, database, pgvector };
2837
+ }
2838
+ };
2839
+
2840
+ export { BatchStatusEnum, ChunkTypeEnum, ConfigurationError, ContextRAG, ContextRAGError, DiscoveryError, DocumentStatusEnum, IngestionError, SearchError };
2841
+ //# sourceMappingURL=index.js.map
2842
+ //# sourceMappingURL=index.js.map