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