plugin-docpixie 1.0.0
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/client.d.ts +1 -0
- package/client.js +1 -0
- package/dist/client/index.js +10 -0
- package/dist/externalVersion.js +17 -0
- package/dist/index.js +48 -0
- package/dist/locale/en-US.json +21 -0
- package/dist/locale/vi-VN.json +21 -0
- package/dist/server/collections/docpixie-config.js +61 -0
- package/dist/server/collections/docpixie-documents.js +71 -0
- package/dist/server/collections/docpixie-pages.js +59 -0
- package/dist/server/exceptions.js +127 -0
- package/dist/server/index.js +49 -0
- package/dist/server/plugin.js +178 -0
- package/dist/server/prompts.js +388 -0
- package/dist/server/providers/index.js +36 -0
- package/dist/server/providers/llm-adapter.js +253 -0
- package/dist/server/services/DocPixieService.js +1300 -0
- package/dist/server/types.js +24 -0
- package/package.json +40 -0
- package/server.d.ts +1 -0
- package/server.js +1 -0
|
@@ -0,0 +1,1300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var __create = Object.create;
|
|
11
|
+
var __defProp = Object.defineProperty;
|
|
12
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
13
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
14
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
15
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
16
|
+
var __export = (target, all) => {
|
|
17
|
+
for (var name in all)
|
|
18
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
19
|
+
};
|
|
20
|
+
var __copyProps = (to, from, except, desc) => {
|
|
21
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
22
|
+
for (let key of __getOwnPropNames(from))
|
|
23
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
24
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
25
|
+
}
|
|
26
|
+
return to;
|
|
27
|
+
};
|
|
28
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
29
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
30
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
31
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
32
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
33
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
34
|
+
mod
|
|
35
|
+
));
|
|
36
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
|
+
var DocPixieService_exports = {};
|
|
38
|
+
__export(DocPixieService_exports, {
|
|
39
|
+
DocPixieService: () => DocPixieService
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(DocPixieService_exports);
|
|
42
|
+
var fs = __toESM(require("fs"));
|
|
43
|
+
var path = __toESM(require("path"));
|
|
44
|
+
var import_prompts = require("../prompts");
|
|
45
|
+
var import_llm_adapter = require("../providers/llm-adapter");
|
|
46
|
+
var import_exceptions = require("../exceptions");
|
|
47
|
+
const SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
48
|
+
".pdf",
|
|
49
|
+
".jpg",
|
|
50
|
+
".jpeg",
|
|
51
|
+
".png",
|
|
52
|
+
".gif",
|
|
53
|
+
".webp",
|
|
54
|
+
".bmp",
|
|
55
|
+
".tiff",
|
|
56
|
+
".tif"
|
|
57
|
+
]);
|
|
58
|
+
const MIME_TYPES = {
|
|
59
|
+
".jpg": "image/jpeg",
|
|
60
|
+
".jpeg": "image/jpeg",
|
|
61
|
+
".png": "image/png",
|
|
62
|
+
".gif": "image/gif",
|
|
63
|
+
".webp": "image/webp",
|
|
64
|
+
".bmp": "image/bmp",
|
|
65
|
+
".tiff": "image/tiff",
|
|
66
|
+
".tif": "image/tiff",
|
|
67
|
+
".pdf": "application/pdf"
|
|
68
|
+
};
|
|
69
|
+
function createTaskPlan(query, tasks, maxIterations) {
|
|
70
|
+
const plan = {
|
|
71
|
+
initialQuery: query,
|
|
72
|
+
tasks: tasks.map((t) => ({
|
|
73
|
+
...t,
|
|
74
|
+
id: `task_${Math.random().toString(36).substring(2, 9)}`,
|
|
75
|
+
status: "pending"
|
|
76
|
+
})),
|
|
77
|
+
currentIteration: 0,
|
|
78
|
+
maxIterations,
|
|
79
|
+
hasPendingTasks() {
|
|
80
|
+
return this.tasks.some((t) => t.status === "pending");
|
|
81
|
+
},
|
|
82
|
+
getNextPendingTask() {
|
|
83
|
+
return this.tasks.find((t) => t.status === "pending") || null;
|
|
84
|
+
},
|
|
85
|
+
getCompletedTasks() {
|
|
86
|
+
return this.tasks.filter((t) => t.status === "completed");
|
|
87
|
+
},
|
|
88
|
+
addTask(task) {
|
|
89
|
+
const newTask = {
|
|
90
|
+
...task,
|
|
91
|
+
id: `task_${Math.random().toString(36).substring(2, 9)}`,
|
|
92
|
+
status: "pending"
|
|
93
|
+
};
|
|
94
|
+
this.tasks.push(newTask);
|
|
95
|
+
return newTask;
|
|
96
|
+
},
|
|
97
|
+
removeTask(taskId) {
|
|
98
|
+
const idx = this.tasks.findIndex((t) => t.id === taskId && t.status === "pending");
|
|
99
|
+
if (idx === -1) return false;
|
|
100
|
+
this.tasks.splice(idx, 1);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
return plan;
|
|
105
|
+
}
|
|
106
|
+
class DocPixieService {
|
|
107
|
+
app;
|
|
108
|
+
db;
|
|
109
|
+
logger;
|
|
110
|
+
config = null;
|
|
111
|
+
llmProvider = null;
|
|
112
|
+
ocrProvider = null;
|
|
113
|
+
constructor(app, db, logger) {
|
|
114
|
+
this.app = app;
|
|
115
|
+
this.db = db;
|
|
116
|
+
this.logger = logger;
|
|
117
|
+
}
|
|
118
|
+
// ═══════════════════════════════════════════
|
|
119
|
+
// Initialization
|
|
120
|
+
// ═══════════════════════════════════════════
|
|
121
|
+
/**
|
|
122
|
+
* Initialize the service with plugin configuration.
|
|
123
|
+
*
|
|
124
|
+
* Loads config from `docpixie_config` collection, resolves LLM providers
|
|
125
|
+
* from NocoBase's plugin-ai infrastructure, and validates connectivity.
|
|
126
|
+
*
|
|
127
|
+
* Called during plugin `load()` and after config changes.
|
|
128
|
+
*
|
|
129
|
+
* @throws Error if config is missing or providers fail validation
|
|
130
|
+
*/
|
|
131
|
+
async initialize(config) {
|
|
132
|
+
if (config) {
|
|
133
|
+
this.config = config;
|
|
134
|
+
} else {
|
|
135
|
+
this.config = await this.loadConfig();
|
|
136
|
+
}
|
|
137
|
+
if (!this.config) {
|
|
138
|
+
this.logger.warn("DocPixie: No configuration found. Plugin will be inactive.");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.llmProvider = await this.resolveNocoBaseLLMProvider(this.config);
|
|
142
|
+
this.ocrProvider = this.createOCRProvider(this.config);
|
|
143
|
+
this.logger.info("DocPixie service initialized", {
|
|
144
|
+
strategy: this.config.analysisStrategy,
|
|
145
|
+
ocrProvider: this.config.ocrProvider,
|
|
146
|
+
llmService: this.config.llmServiceName,
|
|
147
|
+
visionLlmService: this.config.visionLlmServiceName
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Check whether the service is ready to process documents and queries.
|
|
152
|
+
*/
|
|
153
|
+
isReady() {
|
|
154
|
+
return !!(this.config && this.llmProvider);
|
|
155
|
+
}
|
|
156
|
+
// ═══════════════════════════════════════════
|
|
157
|
+
// Document Processing
|
|
158
|
+
// ═══════════════════════════════════════════
|
|
159
|
+
/**
|
|
160
|
+
* Process a document file through the full ingestion pipeline:
|
|
161
|
+
*
|
|
162
|
+
* 1. Validate file exists and is a supported type (.pdf, .jpg, .png, ...)
|
|
163
|
+
* 2. Copy file to storage
|
|
164
|
+
* 3. Detect if page has text layer (digital PDF) or needs OCR (scanned)
|
|
165
|
+
* 4. Extract structured text per page (OCR or text layer)
|
|
166
|
+
* 5. Store document + pages in NocoBase collections
|
|
167
|
+
* 6. Generate document summary via LLM
|
|
168
|
+
* 7. Update document status to 'ready'
|
|
169
|
+
*
|
|
170
|
+
* @param filePath - Absolute path to the source document
|
|
171
|
+
* @param options - Optional overrides
|
|
172
|
+
* @returns Created document record ID
|
|
173
|
+
*/
|
|
174
|
+
async processDocument(filePath, options) {
|
|
175
|
+
this.ensureReady();
|
|
176
|
+
const docRepo = this.db.getRepository("docpixie_documents");
|
|
177
|
+
const pageRepo = this.db.getRepository("docpixie_pages");
|
|
178
|
+
const doc = await docRepo.create({
|
|
179
|
+
values: {
|
|
180
|
+
name: (options == null ? void 0 : options.name) || this.extractFileName(filePath),
|
|
181
|
+
originalPath: filePath,
|
|
182
|
+
status: "pending",
|
|
183
|
+
extractionMethod: this.config.ocrProvider,
|
|
184
|
+
createdById: options == null ? void 0 : options.userId,
|
|
185
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
186
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
const documentId = doc.get("id");
|
|
190
|
+
try {
|
|
191
|
+
await docRepo.update({
|
|
192
|
+
filterByTk: documentId,
|
|
193
|
+
values: { status: "extracting" }
|
|
194
|
+
});
|
|
195
|
+
const pages = await this.extractPages(filePath, documentId);
|
|
196
|
+
for (const page of pages) {
|
|
197
|
+
await pageRepo.create({
|
|
198
|
+
values: {
|
|
199
|
+
documentId,
|
|
200
|
+
pageNumber: page.pageNumber,
|
|
201
|
+
imagePath: page.imagePath,
|
|
202
|
+
structuredText: page.structuredText,
|
|
203
|
+
regions: page.regions,
|
|
204
|
+
hasTables: page.hasTables,
|
|
205
|
+
hasFigures: page.hasFigures,
|
|
206
|
+
headings: page.headings,
|
|
207
|
+
extractionMethod: page.extractionMethod
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
await docRepo.update({
|
|
212
|
+
filterByTk: documentId,
|
|
213
|
+
values: { status: "summarizing" }
|
|
214
|
+
});
|
|
215
|
+
const summary = await this.generateSummary(documentId, pages);
|
|
216
|
+
await docRepo.update({
|
|
217
|
+
filterByTk: documentId,
|
|
218
|
+
values: {
|
|
219
|
+
status: "ready",
|
|
220
|
+
pageCount: pages.length,
|
|
221
|
+
summary,
|
|
222
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
this.logger.info(`DocPixie: Document processed \u2014 id=${documentId}, pages=${pages.length}`);
|
|
226
|
+
return documentId;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
await docRepo.update({
|
|
229
|
+
filterByTk: documentId,
|
|
230
|
+
values: { status: "failed", metadata: { error: String(error) } }
|
|
231
|
+
});
|
|
232
|
+
this.logger.error(`DocPixie: Document processing failed \u2014 id=${documentId}`, error);
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Delete a document and all its pages from the database and filesystem.
|
|
238
|
+
*/
|
|
239
|
+
async deleteDocument(documentId) {
|
|
240
|
+
const docRepo = this.db.getRepository("docpixie_documents");
|
|
241
|
+
const pageRepo = this.db.getRepository("docpixie_pages");
|
|
242
|
+
const doc = await docRepo.findOne({ filterByTk: documentId });
|
|
243
|
+
if (!doc) return false;
|
|
244
|
+
await pageRepo.destroy({ filter: { documentId } });
|
|
245
|
+
await docRepo.destroy({ filterByTk: documentId });
|
|
246
|
+
const storageDir = path.join(process.cwd(), "storage", "docpixie", String(documentId));
|
|
247
|
+
if (fs.existsSync(storageDir)) {
|
|
248
|
+
fs.rmSync(storageDir, { recursive: true, force: true });
|
|
249
|
+
}
|
|
250
|
+
this.logger.info(`DocPixie: Document deleted \u2014 id=${documentId}`);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* List all documents with their status and page counts.
|
|
255
|
+
*/
|
|
256
|
+
async listDocuments(options) {
|
|
257
|
+
const repo = this.db.getRepository("docpixie_documents");
|
|
258
|
+
const filter = {};
|
|
259
|
+
if (options == null ? void 0 : options.status) filter.status = options.status;
|
|
260
|
+
return repo.find({
|
|
261
|
+
filter,
|
|
262
|
+
sort: ["-createdAt"],
|
|
263
|
+
limit: (options == null ? void 0 : options.limit) || 50,
|
|
264
|
+
offset: (options == null ? void 0 : options.offset) || 0
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Get a single document with all its pages loaded.
|
|
269
|
+
*/
|
|
270
|
+
async getDocument(documentId) {
|
|
271
|
+
const repo = this.db.getRepository("docpixie_documents");
|
|
272
|
+
return repo.findOne({
|
|
273
|
+
filterByTk: documentId,
|
|
274
|
+
appends: ["pages"]
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
// ═══════════════════════════════════════════
|
|
278
|
+
// Query Pipeline
|
|
279
|
+
// ═══════════════════════════════════════════
|
|
280
|
+
/**
|
|
281
|
+
* Execute a document query through the adaptive RAG pipeline.
|
|
282
|
+
*
|
|
283
|
+
* Pipeline steps (mirrors DocPixie's PixieRAGAgent):
|
|
284
|
+
*
|
|
285
|
+
* 1. **Context Processing** — Summarize long conversation history (>8 turns)
|
|
286
|
+
* 2. **Query Reformulation** — Resolve pronoun references ("it" → actual subject)
|
|
287
|
+
* 3. **Query Classification** — Determine if documents are needed
|
|
288
|
+
* 4. **Task Planning** — Create 1-4 tasks, each assigned to one document
|
|
289
|
+
* 5. **Page Selection** — For each task, select relevant pages
|
|
290
|
+
* 6. **Page Analysis** — Extract answer from selected pages
|
|
291
|
+
* 7. **Adaptive Update** — Agent may add/remove/modify tasks
|
|
292
|
+
* 8. **Response Synthesis** — Combine all task results into final answer
|
|
293
|
+
*/
|
|
294
|
+
async query(input) {
|
|
295
|
+
this.ensureReady();
|
|
296
|
+
const startTime = Date.now();
|
|
297
|
+
const strategy = input.strategy || this.config.analysisStrategy;
|
|
298
|
+
this.logger.info("DocPixie: Query started", {
|
|
299
|
+
query: input.query.substring(0, 100),
|
|
300
|
+
strategy,
|
|
301
|
+
documentIds: input.documentIds
|
|
302
|
+
});
|
|
303
|
+
this.llmProvider.resetCost();
|
|
304
|
+
const documents = await this.loadQueryDocuments(input.documentIds);
|
|
305
|
+
if (documents.length === 0) {
|
|
306
|
+
return this.createEmptyResult(input.query, startTime, "No documents found");
|
|
307
|
+
}
|
|
308
|
+
let processedQuery = input.query;
|
|
309
|
+
if (input.conversationHistory && input.conversationHistory.length > 0) {
|
|
310
|
+
processedQuery = await this.reformulateQuery(
|
|
311
|
+
input.query,
|
|
312
|
+
input.conversationHistory
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
const needsDocuments = await this.classifyQuery(processedQuery);
|
|
316
|
+
if (!needsDocuments) {
|
|
317
|
+
const directAnswer = await this.getDirectAnswer(processedQuery);
|
|
318
|
+
return {
|
|
319
|
+
answer: directAnswer,
|
|
320
|
+
sourcePages: [],
|
|
321
|
+
confidence: 0.5,
|
|
322
|
+
totalCost: this.llmProvider.getTotalCost(),
|
|
323
|
+
processingTime: (Date.now() - startTime) / 1e3,
|
|
324
|
+
tasksSummary: []
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const taskPlan = await this.createInitialPlan(processedQuery, documents);
|
|
328
|
+
const { taskResults, allSourcePages, analysisResults } = await this.executeAdaptivePlan(
|
|
329
|
+
taskPlan,
|
|
330
|
+
processedQuery,
|
|
331
|
+
documents,
|
|
332
|
+
strategy,
|
|
333
|
+
input.conversationHistory
|
|
334
|
+
);
|
|
335
|
+
const answer = await this.synthesizeResponse(processedQuery, analysisResults);
|
|
336
|
+
const result = {
|
|
337
|
+
answer,
|
|
338
|
+
sourcePages: allSourcePages,
|
|
339
|
+
confidence: this.calculateConfidence(taskResults),
|
|
340
|
+
totalCost: this.llmProvider.getTotalCost(),
|
|
341
|
+
processingTime: (Date.now() - startTime) / 1e3,
|
|
342
|
+
tasksSummary: taskResults
|
|
343
|
+
};
|
|
344
|
+
this.logger.info("DocPixie: Query completed", {
|
|
345
|
+
processingTime: result.processingTime,
|
|
346
|
+
totalCost: result.totalCost,
|
|
347
|
+
tasksCompleted: taskResults.filter((t) => t.status === "completed").length,
|
|
348
|
+
totalIterations: taskPlan.currentIteration
|
|
349
|
+
});
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
// ═══════════════════════════════════════════
|
|
353
|
+
// Page Selection Strategies
|
|
354
|
+
// ═══════════════════════════════════════════
|
|
355
|
+
/**
|
|
356
|
+
* Select relevant pages for a task using the configured strategy.
|
|
357
|
+
*/
|
|
358
|
+
async selectPages(documentId, taskDescription, strategy, maxPages = 5) {
|
|
359
|
+
const pageRepo = this.db.getRepository("docpixie_pages");
|
|
360
|
+
const pages = await pageRepo.find({
|
|
361
|
+
filter: { documentId },
|
|
362
|
+
sort: ["pageNumber"]
|
|
363
|
+
});
|
|
364
|
+
if (pages.length === 0) return [];
|
|
365
|
+
if (pages.length <= maxPages) return pages.map((p) => p.get("pageNumber"));
|
|
366
|
+
switch (strategy) {
|
|
367
|
+
case "ocr_only":
|
|
368
|
+
return this.selectPagesByText(pages, taskDescription, maxPages);
|
|
369
|
+
case "vision":
|
|
370
|
+
return this.selectPagesByVision(pages, taskDescription, maxPages);
|
|
371
|
+
case "hybrid":
|
|
372
|
+
return this.selectPagesByText(pages, taskDescription, maxPages);
|
|
373
|
+
default:
|
|
374
|
+
return this.selectPagesByText(pages, taskDescription, maxPages);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Select pages by analyzing their structuredText content with text LLM.
|
|
379
|
+
*/
|
|
380
|
+
async selectPagesByText(pages, taskDescription, maxPages) {
|
|
381
|
+
const pageSummaries = pages.map((p) => {
|
|
382
|
+
const text = p.get("structuredText") || "";
|
|
383
|
+
const headings = p.get("headings") || [];
|
|
384
|
+
const preview = text.substring(0, 300);
|
|
385
|
+
return `Page ${p.get("pageNumber")}: [Headings: ${headings.join(", ")}] ${preview}`;
|
|
386
|
+
});
|
|
387
|
+
const prompt = (0, import_prompts.fillPrompt)(import_prompts.TEXT_PAGE_SELECTION_PROMPT, {
|
|
388
|
+
task_description: taskDescription,
|
|
389
|
+
page_summaries: pageSummaries.join("\n\n"),
|
|
390
|
+
max_pages: String(maxPages)
|
|
391
|
+
});
|
|
392
|
+
const response = await this.llmProvider.processTextMessages(
|
|
393
|
+
[
|
|
394
|
+
{ role: "system", content: import_prompts.SYSTEM_PAGE_SELECTOR },
|
|
395
|
+
{ role: "user", content: prompt }
|
|
396
|
+
],
|
|
397
|
+
200,
|
|
398
|
+
0.1
|
|
399
|
+
);
|
|
400
|
+
return this.parsePageSelection(response, maxPages);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Select pages by sending their images to the vision LLM.
|
|
404
|
+
*/
|
|
405
|
+
async selectPagesByVision(pages, taskDescription, maxPages) {
|
|
406
|
+
const prompt = (0, import_prompts.fillPrompt)(import_prompts.VISION_PAGE_SELECTION_PROMPT, {
|
|
407
|
+
query: taskDescription,
|
|
408
|
+
query_description: taskDescription
|
|
409
|
+
});
|
|
410
|
+
const messageContent = [
|
|
411
|
+
{ type: "text", text: prompt }
|
|
412
|
+
];
|
|
413
|
+
for (const page of pages) {
|
|
414
|
+
const imagePath = page.get("imagePath");
|
|
415
|
+
if (imagePath) {
|
|
416
|
+
messageContent.push({
|
|
417
|
+
type: "text",
|
|
418
|
+
text: `--- Page ${page.get("pageNumber")} ---`
|
|
419
|
+
});
|
|
420
|
+
messageContent.push({
|
|
421
|
+
type: "image_path",
|
|
422
|
+
image_path: imagePath,
|
|
423
|
+
detail: "low"
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const response = await this.llmProvider.processMultimodalMessages(
|
|
428
|
+
[
|
|
429
|
+
{ role: "system", content: import_prompts.SYSTEM_PAGE_SELECTOR },
|
|
430
|
+
{ role: "user", content: messageContent }
|
|
431
|
+
],
|
|
432
|
+
200,
|
|
433
|
+
0.1
|
|
434
|
+
);
|
|
435
|
+
return this.parsePageSelection(response, maxPages);
|
|
436
|
+
}
|
|
437
|
+
// ═══════════════════════════════════════════
|
|
438
|
+
// Page Analysis
|
|
439
|
+
// ═══════════════════════════════════════════
|
|
440
|
+
/**
|
|
441
|
+
* Analyze selected pages to extract information for a task.
|
|
442
|
+
*/
|
|
443
|
+
async analyzePages(pages, task, strategy, conversationHistory) {
|
|
444
|
+
const memorySummary = this.buildMemorySummary(conversationHistory);
|
|
445
|
+
try {
|
|
446
|
+
switch (strategy) {
|
|
447
|
+
case "ocr_only":
|
|
448
|
+
return await this.analyzePagesText(pages, task, memorySummary);
|
|
449
|
+
case "vision":
|
|
450
|
+
return await this.analyzePagesVision(pages, task, memorySummary);
|
|
451
|
+
case "hybrid":
|
|
452
|
+
default:
|
|
453
|
+
return await this.analyzePagesHybrid(pages, task, memorySummary);
|
|
454
|
+
}
|
|
455
|
+
} catch (err) {
|
|
456
|
+
throw new import_exceptions.TaskAnalysisError(`Page analysis failed: ${err.message}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/** Analyze pages using only their structured text (cheapest). */
|
|
460
|
+
async analyzePagesText(pages, task, memorySummary) {
|
|
461
|
+
const pagesContent = pages.map((p) => {
|
|
462
|
+
return `=== Page ${p.get("pageNumber")} ===
|
|
463
|
+
${p.get("structuredText") || "(no text)"}`;
|
|
464
|
+
}).join("\n\n");
|
|
465
|
+
const prompt = (0, import_prompts.fillPrompt)(import_prompts.TASK_PROCESSING_PROMPT, {
|
|
466
|
+
task_description: task,
|
|
467
|
+
search_queries: task,
|
|
468
|
+
memory_summary: memorySummary
|
|
469
|
+
});
|
|
470
|
+
return this.llmProvider.processTextMessages(
|
|
471
|
+
[
|
|
472
|
+
{ role: "system", content: import_prompts.SYSTEM_DOCPIXIE },
|
|
473
|
+
{ role: "user", content: `${prompt}
|
|
474
|
+
|
|
475
|
+
Document content (OCR text):
|
|
476
|
+
${pagesContent}` }
|
|
477
|
+
],
|
|
478
|
+
1e3,
|
|
479
|
+
0.3
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
/** Analyze pages using only their images (most expensive). */
|
|
483
|
+
async analyzePagesVision(pages, task, memorySummary) {
|
|
484
|
+
const prompt = (0, import_prompts.fillPrompt)(import_prompts.TASK_PROCESSING_PROMPT, {
|
|
485
|
+
task_description: task,
|
|
486
|
+
search_queries: task,
|
|
487
|
+
memory_summary: memorySummary
|
|
488
|
+
});
|
|
489
|
+
const content = [{ type: "text", text: prompt }];
|
|
490
|
+
for (let i = 0; i < pages.length; i++) {
|
|
491
|
+
const page = pages[i];
|
|
492
|
+
content.push({
|
|
493
|
+
type: "image_path",
|
|
494
|
+
image_path: page.get("imagePath"),
|
|
495
|
+
detail: "high"
|
|
496
|
+
});
|
|
497
|
+
content.push({
|
|
498
|
+
type: "text",
|
|
499
|
+
text: `[Page ${i + 1} from document]`
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return this.llmProvider.processMultimodalMessages(
|
|
503
|
+
[
|
|
504
|
+
{ role: "system", content: import_prompts.SYSTEM_DOCPIXIE },
|
|
505
|
+
{ role: "user", content }
|
|
506
|
+
],
|
|
507
|
+
600,
|
|
508
|
+
0.3
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
/** Analyze pages using structured text as ground truth + images for context. */
|
|
512
|
+
async analyzePagesHybrid(pages, task, memorySummary) {
|
|
513
|
+
const prompt = (0, import_prompts.fillPrompt)(import_prompts.TASK_PROCESSING_PROMPT, {
|
|
514
|
+
task_description: task,
|
|
515
|
+
search_queries: task,
|
|
516
|
+
memory_summary: memorySummary
|
|
517
|
+
});
|
|
518
|
+
const content = [];
|
|
519
|
+
const textReference = pages.map((p) => {
|
|
520
|
+
return `=== Page ${p.get("pageNumber")} (OCR Text) ===
|
|
521
|
+
${p.get("structuredText") || "(no text)"}`;
|
|
522
|
+
}).join("\n\n");
|
|
523
|
+
content.push({
|
|
524
|
+
type: "text",
|
|
525
|
+
text: `${prompt}
|
|
526
|
+
|
|
527
|
+
OCR text reference (use for exact numbers/data):
|
|
528
|
+
${textReference}
|
|
529
|
+
|
|
530
|
+
Page images below (use for charts, diagrams, visual context):`
|
|
531
|
+
});
|
|
532
|
+
for (let i = 0; i < pages.length; i++) {
|
|
533
|
+
const page = pages[i];
|
|
534
|
+
const imagePath = page.get("imagePath");
|
|
535
|
+
if (imagePath) {
|
|
536
|
+
content.push({ type: "image_path", image_path: imagePath, detail: "high" });
|
|
537
|
+
content.push({ type: "text", text: `[Page ${i + 1} from document]` });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return this.llmProvider.processMultimodalMessages(
|
|
541
|
+
[
|
|
542
|
+
{
|
|
543
|
+
role: "system",
|
|
544
|
+
content: `${import_prompts.SYSTEM_DOCPIXIE}
|
|
545
|
+
When citing numbers or data, PRIORITIZE the OCR text reference. Use page images for understanding charts, diagrams, and visual layout.`
|
|
546
|
+
},
|
|
547
|
+
{ role: "user", content }
|
|
548
|
+
],
|
|
549
|
+
1e3,
|
|
550
|
+
0.3
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
// ═══════════════════════════════════════════
|
|
554
|
+
// Configuration
|
|
555
|
+
// ═══════════════════════════════════════════
|
|
556
|
+
/** Get current plugin configuration from the database. */
|
|
557
|
+
async getConfig() {
|
|
558
|
+
return this.loadConfig();
|
|
559
|
+
}
|
|
560
|
+
/** Update plugin configuration and reinitialize providers. */
|
|
561
|
+
async updateConfig(config) {
|
|
562
|
+
const repo = this.db.getRepository("docpixie_config");
|
|
563
|
+
const existing = await repo.findOne({});
|
|
564
|
+
if (existing) {
|
|
565
|
+
await repo.update({ filterByTk: existing.get("id"), values: config });
|
|
566
|
+
} else {
|
|
567
|
+
await repo.create({ values: config });
|
|
568
|
+
}
|
|
569
|
+
await this.initialize();
|
|
570
|
+
this.logger.info("DocPixie: Configuration updated");
|
|
571
|
+
}
|
|
572
|
+
// ═══════════════════════════════════════════
|
|
573
|
+
// Private Helpers
|
|
574
|
+
// ═══════════════════════════════════════════
|
|
575
|
+
ensureReady() {
|
|
576
|
+
if (!this.isReady()) {
|
|
577
|
+
throw new import_exceptions.DocPixieError("DocPixie service is not initialized. Call initialize() first.");
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async loadConfig() {
|
|
581
|
+
const repo = this.db.getRepository("docpixie_config");
|
|
582
|
+
const record = await repo.findOne({});
|
|
583
|
+
if (!record) return null;
|
|
584
|
+
return {
|
|
585
|
+
llmServiceName: record.get("llmServiceName"),
|
|
586
|
+
visionLlmServiceName: record.get("visionLlmServiceName"),
|
|
587
|
+
analysisStrategy: record.get("analysisStrategy"),
|
|
588
|
+
ocrProvider: record.get("ocrProvider"),
|
|
589
|
+
ocrApiEndpoint: record.get("ocrApiEndpoint"),
|
|
590
|
+
ocrApiKey: record.get("ocrApiKey"),
|
|
591
|
+
maxPagesPerTask: record.get("maxPagesPerTask"),
|
|
592
|
+
maxTasksPerPlan: record.get("maxTasksPerPlan")
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Resolve LLM providers from NocoBase's plugin-ai infrastructure.
|
|
597
|
+
*
|
|
598
|
+
* This is the KEY integration point — replaces the old createLLMProvider()
|
|
599
|
+
* that used a standalone OpenAICompatibleProvider.
|
|
600
|
+
*
|
|
601
|
+
* Flow:
|
|
602
|
+
* 1. Read llmServiceName from docpixie_config
|
|
603
|
+
* 2. Look up the service in `llmServices` collection
|
|
604
|
+
* 3. Get the registered provider class from aiManager
|
|
605
|
+
* 4. Create provider instance → extract chatModel
|
|
606
|
+
* 5. Wrap in NocoBaseLLMAdapter
|
|
607
|
+
*/
|
|
608
|
+
async resolveNocoBaseLLMProvider(config) {
|
|
609
|
+
if (!config.llmServiceName) {
|
|
610
|
+
this.logger.warn("DocPixie: LLM service name not configured");
|
|
611
|
+
return this.createNoopProvider();
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
const aiPlugin = this.app.pm.get("ai");
|
|
615
|
+
if (!aiPlugin) {
|
|
616
|
+
throw new import_exceptions.ProviderError("plugin-ai is not installed or enabled", "nocobase-llm");
|
|
617
|
+
}
|
|
618
|
+
const aiManager = aiPlugin.aiManager;
|
|
619
|
+
if (!aiManager) {
|
|
620
|
+
throw new import_exceptions.ProviderError("AIManager not available from plugin-ai", "nocobase-llm");
|
|
621
|
+
}
|
|
622
|
+
const textModel = await this.resolveChatModel(
|
|
623
|
+
aiManager,
|
|
624
|
+
config.llmServiceName,
|
|
625
|
+
"text"
|
|
626
|
+
);
|
|
627
|
+
const visionServiceName = config.visionLlmServiceName || config.llmServiceName;
|
|
628
|
+
let visionModel = textModel;
|
|
629
|
+
if (visionServiceName !== config.llmServiceName) {
|
|
630
|
+
visionModel = await this.resolveChatModel(
|
|
631
|
+
aiManager,
|
|
632
|
+
visionServiceName,
|
|
633
|
+
"vision"
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
this.logger.info(`DocPixie: LLM providers resolved \u2014 text: ${config.llmServiceName}, vision: ${visionServiceName}`);
|
|
637
|
+
return new import_llm_adapter.NocoBaseLLMAdapter(textModel, visionModel);
|
|
638
|
+
} catch (err) {
|
|
639
|
+
this.logger.error(`DocPixie: Failed to resolve LLM provider: ${err.message}`);
|
|
640
|
+
return this.createNoopProvider();
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Resolve a LangChain chatModel from NocoBase's llmServices collection.
|
|
645
|
+
* Follows the same pattern as AIEmployee.getLLMService().
|
|
646
|
+
*/
|
|
647
|
+
async resolveChatModel(aiManager, serviceName, purpose) {
|
|
648
|
+
var _a, _b;
|
|
649
|
+
const service = await this.db.getRepository("llmServices").findOne({
|
|
650
|
+
filter: { name: serviceName }
|
|
651
|
+
});
|
|
652
|
+
if (!service) {
|
|
653
|
+
throw new import_exceptions.ProviderError(
|
|
654
|
+
`LLM service '${serviceName}' not found in llmServices. Configure it in the AI Settings.`,
|
|
655
|
+
"nocobase-llm"
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
const providerMeta = aiManager.llmProviders.get(service.provider);
|
|
659
|
+
if (!providerMeta) {
|
|
660
|
+
throw new import_exceptions.ProviderError(
|
|
661
|
+
`LLM provider '${service.provider}' is not registered. Is the plugin installed?`,
|
|
662
|
+
"nocobase-llm"
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
const Provider = providerMeta.provider;
|
|
666
|
+
const provider = new Provider({
|
|
667
|
+
app: this.app,
|
|
668
|
+
serviceOptions: service.options,
|
|
669
|
+
modelOptions: {
|
|
670
|
+
llmService: serviceName,
|
|
671
|
+
model: ((_a = service.options) == null ? void 0 : _a.defaultModel) || ((_b = service.options) == null ? void 0 : _b.model)
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
const chatModel = provider.chatModel || provider.createModel();
|
|
675
|
+
if (!chatModel) {
|
|
676
|
+
throw new import_exceptions.ProviderError(
|
|
677
|
+
`Failed to create chatModel for service '${serviceName}' (${purpose})`,
|
|
678
|
+
"nocobase-llm"
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
this.logger.info(`DocPixie: Resolved ${purpose} model from service '${serviceName}' (provider: ${service.provider})`);
|
|
682
|
+
return chatModel;
|
|
683
|
+
}
|
|
684
|
+
/** Create a no-op provider that throws clear errors */
|
|
685
|
+
createNoopProvider() {
|
|
686
|
+
return {
|
|
687
|
+
async processTextMessages() {
|
|
688
|
+
throw new import_exceptions.ProviderError("LLM provider not configured. Set llmServiceName in DocPixie settings.", "none");
|
|
689
|
+
},
|
|
690
|
+
async processMultimodalMessages() {
|
|
691
|
+
throw new import_exceptions.ProviderError("LLM provider not configured. Set llmServiceName in DocPixie settings.", "none");
|
|
692
|
+
},
|
|
693
|
+
getTotalCost() {
|
|
694
|
+
return 0;
|
|
695
|
+
},
|
|
696
|
+
resetCost() {
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Extract pages from a document file.
|
|
702
|
+
*/
|
|
703
|
+
async extractPages(filePath, documentId) {
|
|
704
|
+
if (!fs.existsSync(filePath)) {
|
|
705
|
+
throw new import_exceptions.ProcessingError(`File not found: ${filePath}`, filePath);
|
|
706
|
+
}
|
|
707
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
708
|
+
if (!SUPPORTED_EXTENSIONS.has(ext)) {
|
|
709
|
+
throw new import_exceptions.ProcessingError(
|
|
710
|
+
`Unsupported file type: ${ext}. Supported: ${[...SUPPORTED_EXTENSIONS].join(", ")}`,
|
|
711
|
+
filePath
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
this.logger.info(`DocPixie: Processing file ${filePath} (ext=${ext})`);
|
|
715
|
+
const storageDir = path.join(process.cwd(), "storage", "docpixie", String(documentId));
|
|
716
|
+
fs.mkdirSync(storageDir, { recursive: true });
|
|
717
|
+
const storedFileName = `original${ext}`;
|
|
718
|
+
const storedFilePath = path.join(storageDir, storedFileName);
|
|
719
|
+
fs.copyFileSync(filePath, storedFilePath);
|
|
720
|
+
if (ext === ".pdf") {
|
|
721
|
+
return this.extractPdfPages(storedFilePath, storageDir, documentId);
|
|
722
|
+
} else {
|
|
723
|
+
return [{
|
|
724
|
+
pageNumber: 1,
|
|
725
|
+
structuredText: "",
|
|
726
|
+
regions: [],
|
|
727
|
+
imagePath: storedFilePath,
|
|
728
|
+
hasTables: false,
|
|
729
|
+
hasFigures: false,
|
|
730
|
+
headings: [],
|
|
731
|
+
extractionMethod: "text_layer"
|
|
732
|
+
}];
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Extract pages from a PDF file.
|
|
737
|
+
*/
|
|
738
|
+
async extractPdfPages(pdfPath, storageDir, documentId) {
|
|
739
|
+
const pages = [];
|
|
740
|
+
let ocrTexts = [];
|
|
741
|
+
if (this.ocrProvider) {
|
|
742
|
+
try {
|
|
743
|
+
const available = await this.ocrProvider.isAvailable();
|
|
744
|
+
if (available) {
|
|
745
|
+
const text = await this.ocrProvider.extractText(pdfPath);
|
|
746
|
+
ocrTexts = text.split(/\f|\n{4,}/).filter((t) => t.trim().length > 0);
|
|
747
|
+
}
|
|
748
|
+
} catch (err) {
|
|
749
|
+
this.logger.warn("DocPixie: OCR extraction failed, continuing without text", err);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const pageCount = Math.max(ocrTexts.length, 1);
|
|
753
|
+
for (let i = 0; i < pageCount; i++) {
|
|
754
|
+
pages.push({
|
|
755
|
+
pageNumber: i + 1,
|
|
756
|
+
structuredText: ocrTexts[i] || "",
|
|
757
|
+
regions: [],
|
|
758
|
+
imagePath: pdfPath,
|
|
759
|
+
hasTables: false,
|
|
760
|
+
hasFigures: false,
|
|
761
|
+
headings: [],
|
|
762
|
+
extractionMethod: ocrTexts[i] ? this.config.ocrProvider : "text_layer"
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
this.logger.info(`DocPixie: PDF processed \u2014 ${pageCount} pages from ${pdfPath}`);
|
|
766
|
+
return pages;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Generate a summary for the entire document using LLM vision.
|
|
770
|
+
*/
|
|
771
|
+
async generateSummary(documentId, pages) {
|
|
772
|
+
if (!this.llmProvider) return "Summary not available (LLM not configured)";
|
|
773
|
+
const imagePaths = pages.map((p) => p.imagePath).filter((p) => p && fs.existsSync(p));
|
|
774
|
+
if (imagePaths.length > 0) {
|
|
775
|
+
try {
|
|
776
|
+
const content = [
|
|
777
|
+
{
|
|
778
|
+
type: "text",
|
|
779
|
+
text: `Please analyze this complete document and provide a comprehensive summary. Look at all pages together to understand the document's overall structure, main themes, key information, and purpose.`
|
|
780
|
+
}
|
|
781
|
+
];
|
|
782
|
+
for (const imgPath of imagePaths) {
|
|
783
|
+
content.push({
|
|
784
|
+
type: "image_path",
|
|
785
|
+
image_path: imgPath,
|
|
786
|
+
detail: "low"
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
return await this.llmProvider.processMultimodalMessages(
|
|
790
|
+
[
|
|
791
|
+
{
|
|
792
|
+
role: "system",
|
|
793
|
+
content: "You are a document analysis expert. Analyze all pages of this document and create a comprehensive summary."
|
|
794
|
+
},
|
|
795
|
+
{ role: "user", content }
|
|
796
|
+
],
|
|
797
|
+
400,
|
|
798
|
+
0.3
|
|
799
|
+
);
|
|
800
|
+
} catch (err) {
|
|
801
|
+
this.logger.warn("DocPixie: Vision summary failed, falling back to text", err);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const allText = pages.map((p) => p.structuredText).join("\n\n---\n\n");
|
|
805
|
+
if (!allText.trim()) return "Document processed (no text content extracted)";
|
|
806
|
+
const truncated = allText.substring(0, 8e3);
|
|
807
|
+
return this.llmProvider.processTextMessages(
|
|
808
|
+
[
|
|
809
|
+
{ role: "system", content: import_prompts.SYSTEM_SUMMARIZER },
|
|
810
|
+
{ role: "user", content: `Summarize this document concisely in 2-3 paragraphs:
|
|
811
|
+
|
|
812
|
+
${truncated}` }
|
|
813
|
+
],
|
|
814
|
+
500,
|
|
815
|
+
0.3
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
// ═══════════════════════════════════════════
|
|
819
|
+
// Context Processing (ported from context_processor.py)
|
|
820
|
+
// ═══════════════════════════════════════════
|
|
821
|
+
async processConversationContext(history, currentQuery) {
|
|
822
|
+
const MAX_TURNS_BEFORE_SUMMARY = 8;
|
|
823
|
+
const TURNS_TO_SUMMARIZE = 4;
|
|
824
|
+
const TURNS_TO_KEEP_FULL = 4;
|
|
825
|
+
const turns = history.filter((m) => m.role === "user").length;
|
|
826
|
+
if (turns <= MAX_TURNS_BEFORE_SUMMARY) {
|
|
827
|
+
return history.map((h) => `${h.role === "user" ? "User" : "Assistant"}: ${h.content}`).join("\n\n");
|
|
828
|
+
}
|
|
829
|
+
this.logger.info(`DocPixie: Conversation has ${turns} turns, applying context summarization`);
|
|
830
|
+
let turnCount = 0;
|
|
831
|
+
let splitIndex = 0;
|
|
832
|
+
for (let i = 0; i < history.length; i += 2) {
|
|
833
|
+
if (i + 1 < history.length && history[i].role === "user") {
|
|
834
|
+
turnCount++;
|
|
835
|
+
if (turnCount === TURNS_TO_SUMMARIZE) {
|
|
836
|
+
splitIndex = i + 2;
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const toSummarize = history.slice(0, splitIndex);
|
|
842
|
+
let toKeep = history.slice(splitIndex);
|
|
843
|
+
const maxKeep = TURNS_TO_KEEP_FULL * 2;
|
|
844
|
+
if (toKeep.length > maxKeep) {
|
|
845
|
+
toKeep = toKeep.slice(-maxKeep);
|
|
846
|
+
}
|
|
847
|
+
const conversationText = toSummarize.map((h) => `${h.role === "user" ? "User" : "Assistant"}: ${h.content}`).join("\n\n");
|
|
848
|
+
const summaryPrompt = (0, import_prompts.fillPrompt)(import_prompts.CONVERSATION_SUMMARIZATION_PROMPT, {
|
|
849
|
+
conversation_text: conversationText
|
|
850
|
+
});
|
|
851
|
+
const summary = await this.llmProvider.processTextMessages(
|
|
852
|
+
[
|
|
853
|
+
{ role: "system", content: "You are a helpful assistant that creates concise conversation summaries." },
|
|
854
|
+
{ role: "user", content: summaryPrompt }
|
|
855
|
+
],
|
|
856
|
+
500,
|
|
857
|
+
0.3
|
|
858
|
+
);
|
|
859
|
+
const parts = [];
|
|
860
|
+
parts.push(`Previous Conversation Summary:
|
|
861
|
+
${summary.trim()}
|
|
862
|
+
`);
|
|
863
|
+
if (toKeep.length > 0) {
|
|
864
|
+
parts.push("Recent Conversation:");
|
|
865
|
+
parts.push(
|
|
866
|
+
toKeep.map((h) => `${h.role === "user" ? "User" : "Assistant"}: ${h.content}`).join("\n\n")
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
parts.push(`
|
|
870
|
+
Current Query: ${currentQuery}`);
|
|
871
|
+
return parts.join("\n");
|
|
872
|
+
}
|
|
873
|
+
// ═══════════════════════════════════════════
|
|
874
|
+
// Query Reformulation
|
|
875
|
+
// ═══════════════════════════════════════════
|
|
876
|
+
async reformulateQuery(query, history) {
|
|
877
|
+
try {
|
|
878
|
+
const context = await this.processConversationContext(history, query);
|
|
879
|
+
const prompt = (0, import_prompts.fillPrompt)(import_prompts.QUERY_REFORMULATION_PROMPT, {
|
|
880
|
+
conversation_context: context,
|
|
881
|
+
recent_topics: "",
|
|
882
|
+
current_query: query
|
|
883
|
+
});
|
|
884
|
+
const response = await this.llmProvider.processTextMessages(
|
|
885
|
+
[
|
|
886
|
+
{ role: "system", content: import_prompts.SYSTEM_QUERY_REFORMULATOR },
|
|
887
|
+
{ role: "user", content: prompt }
|
|
888
|
+
],
|
|
889
|
+
8192,
|
|
890
|
+
0.2
|
|
891
|
+
);
|
|
892
|
+
const parsed = JSON.parse(this.sanitizeLlmJson(response));
|
|
893
|
+
const reformulated = parsed.reformulated_query || query;
|
|
894
|
+
this.logger.info(`DocPixie: Query reformulation: '${query}' \u2192 '${reformulated}'`);
|
|
895
|
+
return reformulated;
|
|
896
|
+
} catch (err) {
|
|
897
|
+
this.logger.warn(`DocPixie: Reformulation failed, using original: ${err.message}`);
|
|
898
|
+
return query;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
// ═══════════════════════════════════════════
|
|
902
|
+
// Query Classification
|
|
903
|
+
// ═══════════════════════════════════════════
|
|
904
|
+
async classifyQuery(query) {
|
|
905
|
+
try {
|
|
906
|
+
const prompt = (0, import_prompts.fillPrompt)(import_prompts.QUERY_CLASSIFICATION_PROMPT, { query });
|
|
907
|
+
const response = await this.llmProvider.processTextMessages(
|
|
908
|
+
[
|
|
909
|
+
{ role: "system", content: import_prompts.SYSTEM_QUERY_CLASSIFIER },
|
|
910
|
+
{ role: "user", content: prompt }
|
|
911
|
+
],
|
|
912
|
+
200,
|
|
913
|
+
0.1
|
|
914
|
+
);
|
|
915
|
+
const parsed = JSON.parse(this.sanitizeLlmJson(response));
|
|
916
|
+
this.logger.info(`DocPixie: Query classification: ${parsed.reasoning}`);
|
|
917
|
+
return parsed.needs_documents !== false;
|
|
918
|
+
} catch (err) {
|
|
919
|
+
this.logger.warn(`DocPixie: Classification failed, defaulting to needs_documents=true: ${err.message}`);
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
async getDirectAnswer(query) {
|
|
924
|
+
return this.llmProvider.processTextMessages(
|
|
925
|
+
[
|
|
926
|
+
{ role: "system", content: import_prompts.SYSTEM_DIRECT_ANSWER },
|
|
927
|
+
{ role: "user", content: query }
|
|
928
|
+
],
|
|
929
|
+
500,
|
|
930
|
+
0.3
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
// ═══════════════════════════════════════════
|
|
934
|
+
// Adaptive Task Planning
|
|
935
|
+
// ═══════════════════════════════════════════
|
|
936
|
+
async createInitialPlan(query, documents) {
|
|
937
|
+
const docTexts = documents.map((d) => {
|
|
938
|
+
const id = d.get("id");
|
|
939
|
+
const name = d.get("name");
|
|
940
|
+
const summary = d.get("summary") || `Document with ${d.get("pageCount")} pages`;
|
|
941
|
+
return `${id}: ${name}
|
|
942
|
+
Summary: ${summary}`;
|
|
943
|
+
}).join("\n\n");
|
|
944
|
+
const prompt = (0, import_prompts.fillPrompt)(import_prompts.ADAPTIVE_INITIAL_PLANNING_PROMPT, {
|
|
945
|
+
query,
|
|
946
|
+
documents: docTexts
|
|
947
|
+
});
|
|
948
|
+
const response = await this.llmProvider.processTextMessages(
|
|
949
|
+
[
|
|
950
|
+
{ role: "system", content: import_prompts.SYSTEM_TASK_PLANNER },
|
|
951
|
+
{ role: "user", content: prompt }
|
|
952
|
+
],
|
|
953
|
+
8192,
|
|
954
|
+
0.3
|
|
955
|
+
);
|
|
956
|
+
try {
|
|
957
|
+
const parsed = JSON.parse(this.sanitizeLlmJson(response));
|
|
958
|
+
const rawTasks = (parsed.tasks || []).slice(0, this.config.maxTasksPerPlan);
|
|
959
|
+
const tasks = rawTasks.map((t) => {
|
|
960
|
+
var _a, _b;
|
|
961
|
+
const docId = t.document || t.document_id;
|
|
962
|
+
const doc = documents.find((d) => String(d.get("id")) === String(docId));
|
|
963
|
+
return {
|
|
964
|
+
name: t.name || "Unnamed Task",
|
|
965
|
+
description: t.description || "",
|
|
966
|
+
documentId: doc ? doc.get("id") : (_a = documents[0]) == null ? void 0 : _a.get("id"),
|
|
967
|
+
documentName: doc ? doc.get("name") : ((_b = documents[0]) == null ? void 0 : _b.get("name")) || "Unknown"
|
|
968
|
+
};
|
|
969
|
+
});
|
|
970
|
+
this.logger.info(`DocPixie: Initial plan created with ${tasks.length} tasks`);
|
|
971
|
+
return createTaskPlan(query, tasks, this.config.maxTasksPerPlan * 2);
|
|
972
|
+
} catch {
|
|
973
|
+
this.logger.warn("DocPixie: Failed to parse initial plan, using fallback");
|
|
974
|
+
const fallbackTasks = documents.slice(0, 2).map((d) => ({
|
|
975
|
+
name: `Analyze ${d.get("name")}`,
|
|
976
|
+
description: query,
|
|
977
|
+
documentId: d.get("id"),
|
|
978
|
+
documentName: d.get("name")
|
|
979
|
+
}));
|
|
980
|
+
return createTaskPlan(query, fallbackTasks, this.config.maxTasksPerPlan * 2);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Execute tasks with adaptive replanning.
|
|
985
|
+
* After each completed task, asks the LLM if the plan should be updated.
|
|
986
|
+
*/
|
|
987
|
+
async executeAdaptivePlan(taskPlan, query, documents, strategy, conversationHistory) {
|
|
988
|
+
const taskResults = [];
|
|
989
|
+
const allSourcePages = [];
|
|
990
|
+
const analysisResults = [];
|
|
991
|
+
let iteration = 0;
|
|
992
|
+
while (taskPlan.hasPendingTasks() && iteration < taskPlan.maxIterations) {
|
|
993
|
+
iteration++;
|
|
994
|
+
this.logger.info(`DocPixie: Agent iteration ${iteration}`);
|
|
995
|
+
const currentTask = taskPlan.getNextPendingTask();
|
|
996
|
+
if (!currentTask) break;
|
|
997
|
+
currentTask.status = "in_progress";
|
|
998
|
+
this.logger.info(`DocPixie: Executing task: ${currentTask.name}`);
|
|
999
|
+
try {
|
|
1000
|
+
const { analysis, sourcePages } = await this.executeSingleTask(
|
|
1001
|
+
currentTask,
|
|
1002
|
+
query,
|
|
1003
|
+
strategy,
|
|
1004
|
+
conversationHistory
|
|
1005
|
+
);
|
|
1006
|
+
currentTask.status = "completed";
|
|
1007
|
+
analysisResults.push(analysis);
|
|
1008
|
+
allSourcePages.push(...sourcePages);
|
|
1009
|
+
taskResults.push({
|
|
1010
|
+
taskName: currentTask.name,
|
|
1011
|
+
documentName: currentTask.documentName,
|
|
1012
|
+
pagesAnalyzed: sourcePages.map((p) => p.pageNumber),
|
|
1013
|
+
status: "completed"
|
|
1014
|
+
});
|
|
1015
|
+
this.logger.info(
|
|
1016
|
+
`DocPixie: Task completed: ${currentTask.name} (analyzed ${sourcePages.length} pages)`
|
|
1017
|
+
);
|
|
1018
|
+
if (taskPlan.hasPendingTasks()) {
|
|
1019
|
+
this.logger.info("DocPixie: Checking if task plan needs updating...");
|
|
1020
|
+
const oldTaskCount = taskPlan.tasks.length;
|
|
1021
|
+
await this.updatePlanAdaptively(
|
|
1022
|
+
taskPlan,
|
|
1023
|
+
currentTask,
|
|
1024
|
+
analysis,
|
|
1025
|
+
query,
|
|
1026
|
+
documents
|
|
1027
|
+
);
|
|
1028
|
+
if (taskPlan.tasks.length !== oldTaskCount) {
|
|
1029
|
+
this.logger.info(
|
|
1030
|
+
`DocPixie: Plan updated \u2014 ${oldTaskCount} \u2192 ${taskPlan.tasks.length} tasks`
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
currentTask.status = "failed";
|
|
1036
|
+
this.logger.error(`DocPixie: Task failed \u2014 ${currentTask.name}`, error);
|
|
1037
|
+
taskResults.push({
|
|
1038
|
+
taskName: currentTask.name,
|
|
1039
|
+
documentName: currentTask.documentName,
|
|
1040
|
+
pagesAnalyzed: [],
|
|
1041
|
+
status: "failed"
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
taskPlan.currentIteration = iteration;
|
|
1046
|
+
this.logger.info(`DocPixie: Adaptive execution completed after ${iteration} iterations`);
|
|
1047
|
+
return { taskResults, allSourcePages, analysisResults };
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Ask the LLM whether to continue/add/remove/modify tasks.
|
|
1051
|
+
*/
|
|
1052
|
+
async updatePlanAdaptively(plan, completedTask, taskFindings, originalQuery, documents) {
|
|
1053
|
+
var _a;
|
|
1054
|
+
const planStatus = plan.tasks.map((t) => `- [${t.id}] ${t.name}: ${t.status}`).join("\n");
|
|
1055
|
+
const completedTasks = plan.getCompletedTasks();
|
|
1056
|
+
const progressSummary = completedTasks.length === 1 ? `Just completed first task: ${completedTask.name}` : "Completed tasks:\n" + completedTasks.map((t) => `\u2713 ${t.name}`).join("\n");
|
|
1057
|
+
const docTexts = documents.map((d) => {
|
|
1058
|
+
const summary = d.get("summary") || `Document with ${d.get("pageCount")} pages`;
|
|
1059
|
+
return `${d.get("id")}: ${d.get("name")}
|
|
1060
|
+
Summary: ${summary}`;
|
|
1061
|
+
}).join("\n\n");
|
|
1062
|
+
const prompt = (0, import_prompts.fillPrompt)(import_prompts.ADAPTIVE_PLAN_UPDATE_PROMPT, {
|
|
1063
|
+
original_query: originalQuery,
|
|
1064
|
+
available_documents: docTexts,
|
|
1065
|
+
current_plan_status: planStatus,
|
|
1066
|
+
completed_task_name: completedTask.name,
|
|
1067
|
+
task_findings: taskFindings.substring(0, 2e3),
|
|
1068
|
+
progress_summary: progressSummary
|
|
1069
|
+
});
|
|
1070
|
+
const response = await this.llmProvider.processTextMessages(
|
|
1071
|
+
[
|
|
1072
|
+
{ role: "system", content: import_prompts.SYSTEM_ADAPTIVE_PLANNER },
|
|
1073
|
+
{ role: "user", content: prompt }
|
|
1074
|
+
],
|
|
1075
|
+
8192,
|
|
1076
|
+
0.3
|
|
1077
|
+
);
|
|
1078
|
+
try {
|
|
1079
|
+
const updateData = JSON.parse(this.sanitizeLlmJson(response));
|
|
1080
|
+
const action = updateData.action || "continue";
|
|
1081
|
+
const reason = updateData.reason || "";
|
|
1082
|
+
this.logger.info(`DocPixie: Plan update action: ${action} \u2014 ${reason}`);
|
|
1083
|
+
switch (action) {
|
|
1084
|
+
case "continue":
|
|
1085
|
+
break;
|
|
1086
|
+
case "add_tasks": {
|
|
1087
|
+
const newTasks = updateData.new_tasks || [];
|
|
1088
|
+
for (const t of newTasks) {
|
|
1089
|
+
const docId = t.document || t.document_id;
|
|
1090
|
+
const doc = documents.find((d) => String(d.get("id")) === String(docId));
|
|
1091
|
+
plan.addTask({
|
|
1092
|
+
name: t.name || "New Task",
|
|
1093
|
+
description: t.description || "",
|
|
1094
|
+
documentId: doc ? doc.get("id") : (_a = documents[0]) == null ? void 0 : _a.get("id"),
|
|
1095
|
+
documentName: doc ? doc.get("name") : "Unknown"
|
|
1096
|
+
});
|
|
1097
|
+
this.logger.info(`DocPixie: Added new task: ${t.name}`);
|
|
1098
|
+
}
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
case "remove_tasks": {
|
|
1102
|
+
const toRemove = updateData.tasks_to_remove || [];
|
|
1103
|
+
for (const taskId of toRemove) {
|
|
1104
|
+
if (plan.removeTask(taskId)) {
|
|
1105
|
+
this.logger.info(`DocPixie: Removed task: ${taskId}`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
case "modify_tasks": {
|
|
1111
|
+
const modifications = updateData.modified_tasks || [];
|
|
1112
|
+
for (const mod of modifications) {
|
|
1113
|
+
const task = plan.tasks.find((t) => t.id === mod.task_id && t.status === "pending");
|
|
1114
|
+
if (task) {
|
|
1115
|
+
const oldName = task.name;
|
|
1116
|
+
task.name = mod.new_name || task.name;
|
|
1117
|
+
task.description = mod.new_description || task.description;
|
|
1118
|
+
if (mod.new_document) {
|
|
1119
|
+
const doc = documents.find((d) => String(d.get("id")) === String(mod.new_document));
|
|
1120
|
+
if (doc) {
|
|
1121
|
+
task.documentId = doc.get("id");
|
|
1122
|
+
task.documentName = doc.get("name");
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
this.logger.info(`DocPixie: Modified task '${oldName}' \u2192 '${task.name}'`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
break;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
this.logger.warn("DocPixie: Failed to parse plan update response, continuing unchanged");
|
|
1133
|
+
}
|
|
1134
|
+
plan.currentIteration++;
|
|
1135
|
+
}
|
|
1136
|
+
/** Execute a single task: select pages → analyze. */
|
|
1137
|
+
async executeSingleTask(task, query, strategy, conversationHistory) {
|
|
1138
|
+
const selectedPageNumbers = await this.selectPages(
|
|
1139
|
+
task.documentId,
|
|
1140
|
+
task.description,
|
|
1141
|
+
strategy,
|
|
1142
|
+
this.config.maxPagesPerTask
|
|
1143
|
+
);
|
|
1144
|
+
const pageRepo = this.db.getRepository("docpixie_pages");
|
|
1145
|
+
const pages = await pageRepo.find({
|
|
1146
|
+
filter: {
|
|
1147
|
+
documentId: task.documentId,
|
|
1148
|
+
pageNumber: { $in: selectedPageNumbers }
|
|
1149
|
+
},
|
|
1150
|
+
sort: ["pageNumber"]
|
|
1151
|
+
});
|
|
1152
|
+
const analysis = await this.analyzePages(pages, task.description, strategy, conversationHistory);
|
|
1153
|
+
const sourcePages = selectedPageNumbers.map((pn) => ({
|
|
1154
|
+
documentId: task.documentId,
|
|
1155
|
+
documentName: task.documentName,
|
|
1156
|
+
pageNumber: pn
|
|
1157
|
+
}));
|
|
1158
|
+
return { analysis, sourcePages };
|
|
1159
|
+
}
|
|
1160
|
+
// ═══════════════════════════════════════════
|
|
1161
|
+
// Response Synthesis
|
|
1162
|
+
// ═══════════════════════════════════════════
|
|
1163
|
+
async synthesizeResponse(query, analyses) {
|
|
1164
|
+
if (analyses.length === 0) return "No relevant information found in the documents.";
|
|
1165
|
+
if (analyses.length === 1) return analyses[0];
|
|
1166
|
+
const resultsText = analyses.map((a, i) => `--- Analysis ${i + 1} ---
|
|
1167
|
+
${a}`).join("\n\n");
|
|
1168
|
+
const prompt = (0, import_prompts.fillPrompt)(import_prompts.SYNTHESIS_PROMPT, {
|
|
1169
|
+
original_query: query,
|
|
1170
|
+
results_text: resultsText
|
|
1171
|
+
});
|
|
1172
|
+
return this.llmProvider.processTextMessages(
|
|
1173
|
+
[
|
|
1174
|
+
{ role: "system", content: import_prompts.SYSTEM_SYNTHESIS },
|
|
1175
|
+
{ role: "user", content: prompt }
|
|
1176
|
+
],
|
|
1177
|
+
2e3,
|
|
1178
|
+
0.3
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
/** Build memory summary from conversation history (last 4 messages). */
|
|
1182
|
+
buildMemorySummary(history) {
|
|
1183
|
+
if (!history || history.length === 0) {
|
|
1184
|
+
return "CONVERSATION CONTEXT: This is the first query in the conversation.";
|
|
1185
|
+
}
|
|
1186
|
+
const recent = history.length > 4 ? history.slice(-4) : history;
|
|
1187
|
+
const parts = ["CONVERSATION CONTEXT:"];
|
|
1188
|
+
for (const msg of recent) {
|
|
1189
|
+
const role = msg.role === "user" ? "User" : "Assistant";
|
|
1190
|
+
const content = msg.content.length > 100 ? msg.content.substring(0, 100) + "..." : msg.content;
|
|
1191
|
+
parts.push(`- ${role}: ${content}`);
|
|
1192
|
+
}
|
|
1193
|
+
return parts.join("\n");
|
|
1194
|
+
}
|
|
1195
|
+
async loadQueryDocuments(documentIds) {
|
|
1196
|
+
const repo = this.db.getRepository("docpixie_documents");
|
|
1197
|
+
const filter = { status: "ready" };
|
|
1198
|
+
if (documentIds && documentIds.length > 0) {
|
|
1199
|
+
filter.id = { $in: documentIds };
|
|
1200
|
+
}
|
|
1201
|
+
return repo.find({ filter });
|
|
1202
|
+
}
|
|
1203
|
+
calculateConfidence(tasks) {
|
|
1204
|
+
if (tasks.length === 0) return 0;
|
|
1205
|
+
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
1206
|
+
return completed / tasks.length;
|
|
1207
|
+
}
|
|
1208
|
+
parsePageSelection(response, maxPages) {
|
|
1209
|
+
try {
|
|
1210
|
+
const cleaned = this.sanitizeLlmJson(response);
|
|
1211
|
+
const parsed = JSON.parse(cleaned);
|
|
1212
|
+
const pages = parsed.selected_pages || [];
|
|
1213
|
+
return pages.slice(0, maxPages).sort((a, b) => a - b);
|
|
1214
|
+
} catch {
|
|
1215
|
+
this.logger.warn("DocPixie: Failed to parse page selection response");
|
|
1216
|
+
return [1];
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
/** Sanitize LLM JSON response: strip markdown fences, trailing commas. */
|
|
1220
|
+
sanitizeLlmJson(text) {
|
|
1221
|
+
let cleaned = text.trim();
|
|
1222
|
+
cleaned = cleaned.replace(/^```json?\s*/i, "").replace(/```\s*$/i, "").trim();
|
|
1223
|
+
cleaned = cleaned.replace(/,\s*([}\]])/g, "$1");
|
|
1224
|
+
return cleaned;
|
|
1225
|
+
}
|
|
1226
|
+
extractFileName(filePath) {
|
|
1227
|
+
const parts = filePath.replace(/\\/g, "/").split("/");
|
|
1228
|
+
const filename = parts[parts.length - 1];
|
|
1229
|
+
return filename.replace(/\.[^.]+$/, "");
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Create OCR provider adapter.
|
|
1233
|
+
* OCR is optional — when all processing is delegated to LLM vision,
|
|
1234
|
+
* this returns null (no OCR needed).
|
|
1235
|
+
*/
|
|
1236
|
+
createOCRProvider(config) {
|
|
1237
|
+
if (config.ocrProvider === "external_api" && config.ocrApiEndpoint) {
|
|
1238
|
+
return {
|
|
1239
|
+
name: "external_api",
|
|
1240
|
+
async extractText(imagePath) {
|
|
1241
|
+
const buffer = fs.readFileSync(imagePath);
|
|
1242
|
+
const base64 = buffer.toString("base64");
|
|
1243
|
+
const ext = path.extname(imagePath).toLowerCase();
|
|
1244
|
+
const mime = MIME_TYPES[ext] || "image/jpeg";
|
|
1245
|
+
const response = await fetch(config.ocrApiEndpoint, {
|
|
1246
|
+
method: "POST",
|
|
1247
|
+
headers: {
|
|
1248
|
+
"Content-Type": "application/json",
|
|
1249
|
+
...config.ocrApiKey ? { Authorization: `Bearer ${config.ocrApiKey}` } : {}
|
|
1250
|
+
},
|
|
1251
|
+
body: JSON.stringify({
|
|
1252
|
+
image: `data:${mime};base64,${base64}`
|
|
1253
|
+
})
|
|
1254
|
+
});
|
|
1255
|
+
if (!response.ok) {
|
|
1256
|
+
throw new import_exceptions.ProviderError(`OCR API returned ${response.status}`, "external_api");
|
|
1257
|
+
}
|
|
1258
|
+
const data = await response.json();
|
|
1259
|
+
return data.text || data.result || "";
|
|
1260
|
+
},
|
|
1261
|
+
async extractStructured(imagePath) {
|
|
1262
|
+
const text = await this.extractText(imagePath);
|
|
1263
|
+
return {
|
|
1264
|
+
pageNumber: 1,
|
|
1265
|
+
structuredText: text,
|
|
1266
|
+
regions: [],
|
|
1267
|
+
imagePath,
|
|
1268
|
+
hasTables: false,
|
|
1269
|
+
hasFigures: false,
|
|
1270
|
+
headings: [],
|
|
1271
|
+
extractionMethod: "external_api"
|
|
1272
|
+
};
|
|
1273
|
+
},
|
|
1274
|
+
async isAvailable() {
|
|
1275
|
+
try {
|
|
1276
|
+
const r = await fetch(config.ocrApiEndpoint, { method: "HEAD" });
|
|
1277
|
+
return r.ok || r.status === 405;
|
|
1278
|
+
} catch {
|
|
1279
|
+
return false;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
return null;
|
|
1285
|
+
}
|
|
1286
|
+
createEmptyResult(query, startTime, reason) {
|
|
1287
|
+
return {
|
|
1288
|
+
answer: reason,
|
|
1289
|
+
sourcePages: [],
|
|
1290
|
+
confidence: 0,
|
|
1291
|
+
totalCost: 0,
|
|
1292
|
+
processingTime: (Date.now() - startTime) / 1e3,
|
|
1293
|
+
tasksSummary: []
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1298
|
+
0 && (module.exports = {
|
|
1299
|
+
DocPixieService
|
|
1300
|
+
});
|