@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/LICENSE +21 -0
- package/README.md +464 -0
- package/dist/bin/cli.cjs +210 -0
- package/dist/bin/cli.cjs.map +1 -0
- package/dist/bin/cli.d.cts +1 -0
- package/dist/bin/cli.d.ts +1 -0
- package/dist/bin/cli.js +187 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/index.cjs +2877 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +812 -0
- package/dist/index.d.ts +812 -0
- package/dist/index.js +2842 -0
- package/dist/index.js.map +1 -0
- package/package.json +91 -0
- package/prisma/schema.prisma +225 -0
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
|