doc2vec 1.1.0 → 1.2.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/README.md +69 -12
- package/dist/content-processor.js +725 -0
- package/dist/database.js +507 -0
- package/dist/doc2vec.js +48 -964
- package/dist/types.js +2 -0
- package/dist/utils.js +118 -0
- package/package.json +10 -4
- package/Dockerfile +0 -29
- package/config.yaml +0 -164
- package/dist/doc2vec copy.js +0 -1437
- package/dist/logger copy.js +0 -246
- package/doc2vec.ts +0 -1677
- package/tsconfig.json +0 -18
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.ContentProcessor = void 0;
|
|
40
|
+
const readability_1 = require("@mozilla/readability");
|
|
41
|
+
const axios_1 = __importDefault(require("axios"));
|
|
42
|
+
const cheerio_1 = require("cheerio");
|
|
43
|
+
const jsdom_1 = require("jsdom");
|
|
44
|
+
const puppeteer_1 = __importDefault(require("puppeteer"));
|
|
45
|
+
const sanitize_html_1 = __importDefault(require("sanitize-html"));
|
|
46
|
+
const turndown_1 = __importDefault(require("turndown"));
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const utils_1 = require("./utils");
|
|
50
|
+
class ContentProcessor {
|
|
51
|
+
constructor(logger) {
|
|
52
|
+
this.logger = logger;
|
|
53
|
+
this.turndownService = new turndown_1.default({
|
|
54
|
+
codeBlockStyle: 'fenced',
|
|
55
|
+
headingStyle: 'atx'
|
|
56
|
+
});
|
|
57
|
+
this.setupTurndownRules();
|
|
58
|
+
}
|
|
59
|
+
setupTurndownRules() {
|
|
60
|
+
const logger = this.logger.child('markdown');
|
|
61
|
+
logger.debug('Setting up Turndown rules for markdown conversion');
|
|
62
|
+
this.turndownService.addRule('codeBlocks', {
|
|
63
|
+
filter: (node) => node.nodeName === 'PRE',
|
|
64
|
+
replacement: (content, node) => {
|
|
65
|
+
const htmlNode = node;
|
|
66
|
+
const code = htmlNode.querySelector('code');
|
|
67
|
+
let codeContent;
|
|
68
|
+
if (code) {
|
|
69
|
+
codeContent = code.textContent || '';
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
codeContent = htmlNode.textContent || '';
|
|
73
|
+
}
|
|
74
|
+
const lines = codeContent.split('\n');
|
|
75
|
+
let minIndent = Infinity;
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
if (line.trim() === '')
|
|
78
|
+
continue;
|
|
79
|
+
const leadingWhitespace = line.match(/^\s*/)?.[0] || '';
|
|
80
|
+
minIndent = Math.min(minIndent, leadingWhitespace.length);
|
|
81
|
+
}
|
|
82
|
+
const cleanedLines = lines.map(line => {
|
|
83
|
+
return line.substring(minIndent);
|
|
84
|
+
});
|
|
85
|
+
let cleanContent = cleanedLines.join('\n');
|
|
86
|
+
cleanContent = cleanContent.replace(/^\s+|\s+$/g, '');
|
|
87
|
+
cleanContent = cleanContent.replace(/\n{2,}/g, '\n');
|
|
88
|
+
return `\n\`\`\`\n${cleanContent}\n\`\`\`\n`;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
this.turndownService.addRule('tableCell', {
|
|
92
|
+
filter: ['th', 'td'],
|
|
93
|
+
replacement: (content, node) => {
|
|
94
|
+
const htmlNode = node;
|
|
95
|
+
let cellContent = '';
|
|
96
|
+
if (htmlNode.querySelector('p')) {
|
|
97
|
+
cellContent = Array.from(htmlNode.querySelectorAll('p'))
|
|
98
|
+
.map(p => p.textContent || '')
|
|
99
|
+
.join(' ')
|
|
100
|
+
.trim();
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
cellContent = content.trim();
|
|
104
|
+
}
|
|
105
|
+
return ` ${cellContent.replace(/\|/g, '\\|')} |`;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
this.turndownService.addRule('tableRow', {
|
|
109
|
+
filter: 'tr',
|
|
110
|
+
replacement: (content, node) => {
|
|
111
|
+
const htmlNode = node;
|
|
112
|
+
const cells = Array.from(htmlNode.cells);
|
|
113
|
+
const isHeader = htmlNode.parentNode?.nodeName === 'THEAD';
|
|
114
|
+
let output = '|' + content.trimEnd();
|
|
115
|
+
if (isHeader) {
|
|
116
|
+
const separator = cells.map(() => '---').join(' | ');
|
|
117
|
+
output += '\n|' + separator + '|';
|
|
118
|
+
}
|
|
119
|
+
if (!isHeader || !htmlNode.nextElementSibling) {
|
|
120
|
+
output += '\n';
|
|
121
|
+
}
|
|
122
|
+
return output;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
this.turndownService.addRule('table', {
|
|
126
|
+
filter: 'table',
|
|
127
|
+
replacement: (content) => {
|
|
128
|
+
return '\n' + content.replace(/\n+/g, '\n').trim() + '\n';
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
this.turndownService.addRule('preserveTableWhitespace', {
|
|
132
|
+
filter: (node) => {
|
|
133
|
+
return ((node.nodeName === 'TD' || node.nodeName === 'TH') &&
|
|
134
|
+
(node.textContent?.trim().length === 0));
|
|
135
|
+
},
|
|
136
|
+
replacement: () => {
|
|
137
|
+
return ' |';
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
logger.debug('Turndown rules setup complete');
|
|
141
|
+
}
|
|
142
|
+
async parseSitemap(sitemapUrl, logger) {
|
|
143
|
+
logger.info(`Parsing sitemap from ${sitemapUrl}`);
|
|
144
|
+
try {
|
|
145
|
+
const response = await axios_1.default.get(sitemapUrl);
|
|
146
|
+
const $ = (0, cheerio_1.load)(response.data, { xmlMode: true });
|
|
147
|
+
const urls = [];
|
|
148
|
+
// Handle standard sitemaps
|
|
149
|
+
$('url > loc').each((_, element) => {
|
|
150
|
+
const url = $(element).text().trim();
|
|
151
|
+
if (url) {
|
|
152
|
+
urls.push(url);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
// Handle sitemap indexes (sitemaps that link to other sitemaps)
|
|
156
|
+
const sitemapLinks = [];
|
|
157
|
+
$('sitemap > loc').each((_, element) => {
|
|
158
|
+
const nestedSitemapUrl = $(element).text().trim();
|
|
159
|
+
if (nestedSitemapUrl) {
|
|
160
|
+
sitemapLinks.push(nestedSitemapUrl);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
// Recursively process nested sitemaps
|
|
164
|
+
for (const nestedSitemapUrl of sitemapLinks) {
|
|
165
|
+
logger.debug(`Found nested sitemap: ${nestedSitemapUrl}`);
|
|
166
|
+
const nestedUrls = await this.parseSitemap(nestedSitemapUrl, logger);
|
|
167
|
+
urls.push(...nestedUrls);
|
|
168
|
+
}
|
|
169
|
+
logger.info(`Found ${urls.length} URLs in sitemap ${sitemapUrl}`);
|
|
170
|
+
return urls;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
logger.error(`Error parsing sitemap at ${sitemapUrl}:`, error);
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async crawlWebsite(baseUrl, sourceConfig, processPageContent, parentLogger, visitedUrls) {
|
|
178
|
+
const logger = parentLogger.child('crawler');
|
|
179
|
+
const queue = [baseUrl];
|
|
180
|
+
// Process sitemap if provided
|
|
181
|
+
if (sourceConfig.sitemap_url) {
|
|
182
|
+
logger.section('SITEMAP PROCESSING');
|
|
183
|
+
const sitemapUrls = await this.parseSitemap(sourceConfig.sitemap_url, logger);
|
|
184
|
+
// Add sitemap URLs to the queue if they're within the website scope
|
|
185
|
+
for (const url of sitemapUrls) {
|
|
186
|
+
if (url.startsWith(sourceConfig.url) && !queue.includes(url)) {
|
|
187
|
+
logger.debug(`Adding URL from sitemap to queue: ${url}`);
|
|
188
|
+
queue.push(url);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
logger.info(`Added ${queue.length - 1} URLs from sitemap to the crawl queue`);
|
|
192
|
+
}
|
|
193
|
+
logger.info(`Starting crawl from ${baseUrl} with ${queue.length} URLs in initial queue`);
|
|
194
|
+
let processedCount = 0;
|
|
195
|
+
let skippedCount = 0;
|
|
196
|
+
let skippedSizeCount = 0;
|
|
197
|
+
let pdfProcessedCount = 0;
|
|
198
|
+
let errorCount = 0;
|
|
199
|
+
let hasNetworkErrors = false;
|
|
200
|
+
while (queue.length > 0) {
|
|
201
|
+
const url = queue.shift();
|
|
202
|
+
if (!url)
|
|
203
|
+
continue;
|
|
204
|
+
const normalizedUrl = utils_1.Utils.normalizeUrl(url);
|
|
205
|
+
if (visitedUrls.has(normalizedUrl))
|
|
206
|
+
continue;
|
|
207
|
+
visitedUrls.add(normalizedUrl);
|
|
208
|
+
if (!utils_1.Utils.shouldProcessUrl(url)) {
|
|
209
|
+
logger.debug(`Skipping URL with unsupported extension: ${url}`);
|
|
210
|
+
skippedCount++;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
logger.info(`Crawling: ${url}`);
|
|
215
|
+
const content = await this.processPage(url, sourceConfig);
|
|
216
|
+
if (content !== null) {
|
|
217
|
+
await processPageContent(url, content);
|
|
218
|
+
if (utils_1.Utils.isPdfUrl(url)) {
|
|
219
|
+
pdfProcessedCount++;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
processedCount++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
skippedSizeCount++;
|
|
227
|
+
}
|
|
228
|
+
// Only try to extract links from HTML pages, not PDFs
|
|
229
|
+
if (!utils_1.Utils.isPdfUrl(url)) {
|
|
230
|
+
const response = await axios_1.default.get(url);
|
|
231
|
+
const $ = (0, cheerio_1.load)(response.data);
|
|
232
|
+
logger.debug(`Finding links on page ${url}`);
|
|
233
|
+
let newLinksFound = 0;
|
|
234
|
+
$('a[href]').each((_, element) => {
|
|
235
|
+
const href = $(element).attr('href');
|
|
236
|
+
if (!href || href.startsWith('#') || href.startsWith('mailto:'))
|
|
237
|
+
return;
|
|
238
|
+
const fullUrl = utils_1.Utils.buildUrl(href, url);
|
|
239
|
+
if (fullUrl.startsWith(sourceConfig.url) && !visitedUrls.has(utils_1.Utils.normalizeUrl(fullUrl))) {
|
|
240
|
+
if (!queue.includes(fullUrl)) {
|
|
241
|
+
queue.push(fullUrl);
|
|
242
|
+
newLinksFound++;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
logger.debug(`Found ${newLinksFound} new links on ${url}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
logger.error(`Failed during processing or link discovery for ${url}:`, error);
|
|
251
|
+
errorCount++;
|
|
252
|
+
// Check if this is a network error (DNS resolution, connection issues, etc.)
|
|
253
|
+
if (this.isNetworkError(error)) {
|
|
254
|
+
hasNetworkErrors = true;
|
|
255
|
+
logger.warn(`Network error detected for ${url}, this may affect cleanup decisions`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
logger.info(`Crawl completed. HTML Pages: ${processedCount}, PDFs: ${pdfProcessedCount}, Skipped (Extension): ${skippedCount}, Skipped (Size): ${skippedSizeCount}, Errors: ${errorCount}`);
|
|
260
|
+
if (hasNetworkErrors) {
|
|
261
|
+
logger.warn('Network errors were encountered during crawling. Cleanup may be skipped to avoid removing valid chunks.');
|
|
262
|
+
}
|
|
263
|
+
return { hasNetworkErrors };
|
|
264
|
+
}
|
|
265
|
+
isNetworkError(error) {
|
|
266
|
+
// Check for common network error patterns
|
|
267
|
+
if (error?.code) {
|
|
268
|
+
// DNS resolution errors
|
|
269
|
+
if (error.code === 'ENOTFOUND')
|
|
270
|
+
return true;
|
|
271
|
+
// Connection refused
|
|
272
|
+
if (error.code === 'ECONNREFUSED')
|
|
273
|
+
return true;
|
|
274
|
+
// Connection timeout
|
|
275
|
+
if (error.code === 'ETIMEDOUT')
|
|
276
|
+
return true;
|
|
277
|
+
// Connection reset
|
|
278
|
+
if (error.code === 'ECONNRESET')
|
|
279
|
+
return true;
|
|
280
|
+
// Host unreachable
|
|
281
|
+
if (error.code === 'EHOSTUNREACH')
|
|
282
|
+
return true;
|
|
283
|
+
// Network unreachable
|
|
284
|
+
if (error.code === 'ENETUNREACH')
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
// Check for axios-specific network errors
|
|
288
|
+
if (error?.isAxiosError) {
|
|
289
|
+
// If there's no response, it's likely a network error
|
|
290
|
+
if (!error.response)
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
// Check error message for network-related terms
|
|
294
|
+
const errorMessage = error?.message?.toLowerCase() || '';
|
|
295
|
+
if (errorMessage.includes('getaddrinfo') ||
|
|
296
|
+
errorMessage.includes('network') ||
|
|
297
|
+
errorMessage.includes('timeout') ||
|
|
298
|
+
errorMessage.includes('connection') ||
|
|
299
|
+
errorMessage.includes('dns')) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
async processPage(url, sourceConfig) {
|
|
305
|
+
const logger = this.logger.child('page-processor');
|
|
306
|
+
logger.debug(`Processing content from ${url}`);
|
|
307
|
+
// Check if this is a PDF URL
|
|
308
|
+
if (utils_1.Utils.isPdfUrl(url)) {
|
|
309
|
+
logger.info(`Processing PDF: ${url}`);
|
|
310
|
+
try {
|
|
311
|
+
const markdown = await this.downloadAndConvertPdfFromUrl(url, logger);
|
|
312
|
+
// Check size limit for PDF content
|
|
313
|
+
if (markdown.length > sourceConfig.max_size) {
|
|
314
|
+
logger.warn(`PDF content (${markdown.length} chars) exceeds max size (${sourceConfig.max_size}). Skipping ${url}.`);
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
return markdown;
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
logger.error(`Failed to process PDF ${url}:`, error);
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Original HTML page processing logic
|
|
325
|
+
let browser = null;
|
|
326
|
+
try {
|
|
327
|
+
browser = await puppeteer_1.default.launch({
|
|
328
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
329
|
+
});
|
|
330
|
+
const page = await browser.newPage();
|
|
331
|
+
logger.debug(`Navigating to ${url}`);
|
|
332
|
+
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
|
|
333
|
+
const htmlContent = await page.evaluate(() => {
|
|
334
|
+
const mainContentElement = document.querySelector('div[role="main"].document') || document.querySelector('main') || document.body;
|
|
335
|
+
return mainContentElement.innerHTML;
|
|
336
|
+
});
|
|
337
|
+
if (htmlContent.length > sourceConfig.max_size) {
|
|
338
|
+
logger.warn(`Raw HTML content (${htmlContent.length} chars) exceeds max size (${sourceConfig.max_size}). Skipping detailed processing for ${url}.`);
|
|
339
|
+
await browser.close();
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
logger.debug(`Got HTML content (${htmlContent.length} chars), creating DOM`);
|
|
343
|
+
const dom = new jsdom_1.JSDOM(htmlContent);
|
|
344
|
+
const document = dom.window.document;
|
|
345
|
+
document.querySelectorAll('pre').forEach((pre) => {
|
|
346
|
+
pre.classList.add('article-content');
|
|
347
|
+
pre.setAttribute('data-readable-content-score', '100');
|
|
348
|
+
this.markCodeParents(pre.parentElement);
|
|
349
|
+
});
|
|
350
|
+
logger.debug(`Applying Readability to extract main content`);
|
|
351
|
+
const reader = new readability_1.Readability(document, {
|
|
352
|
+
charThreshold: 20,
|
|
353
|
+
classesToPreserve: ['article-content'],
|
|
354
|
+
});
|
|
355
|
+
const article = reader.parse();
|
|
356
|
+
if (!article) {
|
|
357
|
+
logger.warn(`Failed to parse article content with Readability for ${url}`);
|
|
358
|
+
await browser.close();
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
logger.debug(`Sanitizing HTML (${article.content.length} chars)`);
|
|
362
|
+
const cleanHtml = (0, sanitize_html_1.default)(article.content, {
|
|
363
|
+
allowedTags: [
|
|
364
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'ul', 'ol',
|
|
365
|
+
'li', 'b', 'i', 'strong', 'em', 'code', 'pre',
|
|
366
|
+
'div', 'span', 'table', 'thead', 'tbody', 'tr', 'th', 'td'
|
|
367
|
+
],
|
|
368
|
+
allowedAttributes: {
|
|
369
|
+
'a': ['href'],
|
|
370
|
+
'pre': ['class', 'data-language'],
|
|
371
|
+
'code': ['class', 'data-language'],
|
|
372
|
+
'div': ['class'],
|
|
373
|
+
'span': ['class']
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
logger.debug(`Converting HTML to Markdown`);
|
|
377
|
+
const markdown = this.turndownService.turndown(cleanHtml);
|
|
378
|
+
logger.debug(`Markdown conversion complete (${markdown.length} chars)`);
|
|
379
|
+
return markdown;
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
logger.error(`Error processing page ${url}:`, error);
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
if (browser && browser.isConnected()) {
|
|
387
|
+
await browser.close();
|
|
388
|
+
logger.debug(`Browser closed for ${url}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
markCodeParents(node) {
|
|
393
|
+
if (!node)
|
|
394
|
+
return;
|
|
395
|
+
if (node.querySelector('pre, code')) {
|
|
396
|
+
node.classList.add('article-content');
|
|
397
|
+
node.setAttribute('data-readable-content-score', '100');
|
|
398
|
+
}
|
|
399
|
+
this.markCodeParents(node.parentElement);
|
|
400
|
+
}
|
|
401
|
+
async convertPdfToMarkdown(filePath, logger) {
|
|
402
|
+
logger.debug(`Converting PDF to markdown: ${filePath}`);
|
|
403
|
+
try {
|
|
404
|
+
// Dynamic import for PDF.js to handle ES module compatibility
|
|
405
|
+
const pdfjsLib = await Promise.resolve().then(() => __importStar(require('pdfjs-dist/legacy/build/pdf.mjs')));
|
|
406
|
+
// Read the PDF file as a buffer and convert to Uint8Array
|
|
407
|
+
const pdfBuffer = fs.readFileSync(filePath);
|
|
408
|
+
const pdfData = new Uint8Array(pdfBuffer);
|
|
409
|
+
// Load the PDF document
|
|
410
|
+
const loadingTask = pdfjsLib.getDocument({
|
|
411
|
+
data: pdfData,
|
|
412
|
+
// Disable worker to avoid issues in Node.js environment
|
|
413
|
+
useWorkerFetch: false,
|
|
414
|
+
isEvalSupported: false,
|
|
415
|
+
useSystemFonts: true
|
|
416
|
+
});
|
|
417
|
+
const pdfDocument = await loadingTask.promise;
|
|
418
|
+
const numPages = pdfDocument.numPages;
|
|
419
|
+
logger.debug(`PDF has ${numPages} pages`);
|
|
420
|
+
let markdown = `# ${path.basename(filePath, '.pdf')}\n\n`;
|
|
421
|
+
// Extract text from each page
|
|
422
|
+
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
|
|
423
|
+
const page = await pdfDocument.getPage(pageNum);
|
|
424
|
+
const textContent = await page.getTextContent();
|
|
425
|
+
// Combine text items into a readable format
|
|
426
|
+
let pageText = '';
|
|
427
|
+
let currentY = -1;
|
|
428
|
+
for (const item of textContent.items) {
|
|
429
|
+
if ('str' in item) {
|
|
430
|
+
// If this is a new line (different Y position), add a line break
|
|
431
|
+
if (currentY !== -1 && Math.abs(item.transform[5] - currentY) > 5) {
|
|
432
|
+
pageText += '\n';
|
|
433
|
+
}
|
|
434
|
+
pageText += item.str;
|
|
435
|
+
// Add space if the next item doesn't start immediately after this one
|
|
436
|
+
if ('width' in item && item.width > 0) {
|
|
437
|
+
pageText += ' ';
|
|
438
|
+
}
|
|
439
|
+
currentY = item.transform[5];
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Clean up the text
|
|
443
|
+
pageText = pageText
|
|
444
|
+
.replace(/\s+/g, ' ') // Replace multiple whitespace with single space
|
|
445
|
+
.replace(/\n\s+/g, '\n') // Clean up line starts
|
|
446
|
+
.trim();
|
|
447
|
+
if (pageText.length > 0) {
|
|
448
|
+
if (numPages > 1) {
|
|
449
|
+
markdown += `## Page ${pageNum}\n\n`;
|
|
450
|
+
}
|
|
451
|
+
markdown += pageText + '\n\n';
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// Clean up the final markdown
|
|
455
|
+
markdown = markdown.replace(/\n{3,}/g, '\n\n').trim();
|
|
456
|
+
logger.debug(`Converted PDF to ${markdown.length} characters of markdown`);
|
|
457
|
+
return markdown;
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
logger.error(`Failed to convert PDF ${filePath}:`, error);
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async downloadAndConvertPdfFromUrl(url, logger) {
|
|
465
|
+
logger.debug(`Downloading and converting PDF from URL: ${url}`);
|
|
466
|
+
try {
|
|
467
|
+
// Download the PDF file
|
|
468
|
+
const response = await axios_1.default.get(url, {
|
|
469
|
+
responseType: 'arraybuffer',
|
|
470
|
+
timeout: 60000, // 60 second timeout
|
|
471
|
+
headers: {
|
|
472
|
+
'User-Agent': 'Mozilla/5.0 (compatible; doc2vec PDF processor)'
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
if (response.status !== 200) {
|
|
476
|
+
throw new Error(`Failed to download PDF: HTTP ${response.status}`);
|
|
477
|
+
}
|
|
478
|
+
logger.debug(`Downloaded PDF (${response.data.byteLength} bytes)`);
|
|
479
|
+
// Dynamic import for PDF.js to handle ES module compatibility
|
|
480
|
+
const pdfjsLib = await Promise.resolve().then(() => __importStar(require('pdfjs-dist/legacy/build/pdf.mjs')));
|
|
481
|
+
// Convert ArrayBuffer to Uint8Array
|
|
482
|
+
const pdfData = new Uint8Array(response.data);
|
|
483
|
+
// Load the PDF document
|
|
484
|
+
const loadingTask = pdfjsLib.getDocument({
|
|
485
|
+
data: pdfData,
|
|
486
|
+
// Disable worker to avoid issues in Node.js environment
|
|
487
|
+
useWorkerFetch: false,
|
|
488
|
+
isEvalSupported: false,
|
|
489
|
+
useSystemFonts: true
|
|
490
|
+
});
|
|
491
|
+
const pdfDocument = await loadingTask.promise;
|
|
492
|
+
const numPages = pdfDocument.numPages;
|
|
493
|
+
logger.debug(`PDF has ${numPages} pages`);
|
|
494
|
+
// Get the filename from URL for the title
|
|
495
|
+
const urlPath = new URL(url).pathname;
|
|
496
|
+
const filename = path.basename(urlPath, '.pdf') || 'document';
|
|
497
|
+
let markdown = `# ${filename}\n\n`;
|
|
498
|
+
// Extract text from each page
|
|
499
|
+
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
|
|
500
|
+
const page = await pdfDocument.getPage(pageNum);
|
|
501
|
+
const textContent = await page.getTextContent();
|
|
502
|
+
// Combine text items into a readable format
|
|
503
|
+
let pageText = '';
|
|
504
|
+
let currentY = -1;
|
|
505
|
+
for (const item of textContent.items) {
|
|
506
|
+
if ('str' in item) {
|
|
507
|
+
// If this is a new line (different Y position), add a line break
|
|
508
|
+
if (currentY !== -1 && Math.abs(item.transform[5] - currentY) > 5) {
|
|
509
|
+
pageText += '\n';
|
|
510
|
+
}
|
|
511
|
+
pageText += item.str;
|
|
512
|
+
// Add space if the next item doesn't start immediately after this one
|
|
513
|
+
if ('width' in item && item.width > 0) {
|
|
514
|
+
pageText += ' ';
|
|
515
|
+
}
|
|
516
|
+
currentY = item.transform[5];
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// Clean up the text
|
|
520
|
+
pageText = pageText
|
|
521
|
+
.replace(/\s+/g, ' ') // Replace multiple whitespace with single space
|
|
522
|
+
.replace(/\n\s+/g, '\n') // Clean up line starts
|
|
523
|
+
.trim();
|
|
524
|
+
if (pageText.length > 0) {
|
|
525
|
+
if (numPages > 1) {
|
|
526
|
+
markdown += `## Page ${pageNum}\n\n`;
|
|
527
|
+
}
|
|
528
|
+
markdown += pageText + '\n\n';
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// Clean up the final markdown
|
|
532
|
+
markdown = markdown.replace(/\n{3,}/g, '\n\n').trim();
|
|
533
|
+
logger.debug(`Converted PDF to ${markdown.length} characters of markdown`);
|
|
534
|
+
return markdown;
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
logger.error(`Failed to download and convert PDF from ${url}:`, error);
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async processDirectory(dirPath, config, processFileContent, parentLogger, visitedPaths = new Set()) {
|
|
542
|
+
const logger = parentLogger.child('directory-processor');
|
|
543
|
+
logger.info(`Processing directory: ${dirPath}`);
|
|
544
|
+
const recursive = config.recursive !== undefined ? config.recursive : true;
|
|
545
|
+
const includeExtensions = config.include_extensions || ['.md', '.txt', '.html', '.htm', '.pdf'];
|
|
546
|
+
const excludeExtensions = config.exclude_extensions || [];
|
|
547
|
+
const encoding = config.encoding || 'utf8';
|
|
548
|
+
try {
|
|
549
|
+
const files = fs.readdirSync(dirPath);
|
|
550
|
+
let processedFiles = 0;
|
|
551
|
+
let skippedFiles = 0;
|
|
552
|
+
for (const file of files) {
|
|
553
|
+
const filePath = path.join(dirPath, file);
|
|
554
|
+
const stat = fs.statSync(filePath);
|
|
555
|
+
// Skip already visited paths
|
|
556
|
+
if (visitedPaths.has(filePath)) {
|
|
557
|
+
logger.debug(`Skipping already visited path: ${filePath}`);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
visitedPaths.add(filePath);
|
|
561
|
+
if (stat.isDirectory()) {
|
|
562
|
+
if (recursive) {
|
|
563
|
+
await this.processDirectory(filePath, config, processFileContent, logger, visitedPaths);
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
logger.debug(`Skipping directory ${filePath} (recursive=false)`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
else if (stat.isFile()) {
|
|
570
|
+
const extension = path.extname(file).toLowerCase();
|
|
571
|
+
// Apply extension filters
|
|
572
|
+
if (excludeExtensions.includes(extension)) {
|
|
573
|
+
logger.debug(`Skipping file with excluded extension: ${filePath}`);
|
|
574
|
+
skippedFiles++;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (includeExtensions.length > 0 && !includeExtensions.includes(extension)) {
|
|
578
|
+
logger.debug(`Skipping file with non-included extension: ${filePath}`);
|
|
579
|
+
skippedFiles++;
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
try {
|
|
583
|
+
logger.info(`Reading file: ${filePath}`);
|
|
584
|
+
let content;
|
|
585
|
+
let processedContent;
|
|
586
|
+
if (extension === '.pdf') {
|
|
587
|
+
// Handle PDF files
|
|
588
|
+
logger.debug(`Processing PDF file: ${filePath}`);
|
|
589
|
+
processedContent = await this.convertPdfToMarkdown(filePath, logger);
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
// Handle text-based files
|
|
593
|
+
content = fs.readFileSync(filePath, { encoding: encoding });
|
|
594
|
+
if (content.length > config.max_size) {
|
|
595
|
+
logger.warn(`File content (${content.length} chars) exceeds max size (${config.max_size}). Skipping ${filePath}.`);
|
|
596
|
+
skippedFiles++;
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
// Convert HTML to Markdown if needed
|
|
600
|
+
if (extension === '.html' || extension === '.htm') {
|
|
601
|
+
logger.debug(`Converting HTML to Markdown for ${filePath}`);
|
|
602
|
+
const cleanHtml = (0, sanitize_html_1.default)(content, {
|
|
603
|
+
allowedTags: [
|
|
604
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'ul', 'ol',
|
|
605
|
+
'li', 'b', 'i', 'strong', 'em', 'code', 'pre',
|
|
606
|
+
'div', 'span', 'table', 'thead', 'tbody', 'tr', 'th', 'td'
|
|
607
|
+
],
|
|
608
|
+
allowedAttributes: {
|
|
609
|
+
'a': ['href'],
|
|
610
|
+
'pre': ['class', 'data-language'],
|
|
611
|
+
'code': ['class', 'data-language'],
|
|
612
|
+
'div': ['class'],
|
|
613
|
+
'span': ['class']
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
processedContent = this.turndownService.turndown(cleanHtml);
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
processedContent = content;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// Check size limit for processed content
|
|
623
|
+
if (processedContent.length > config.max_size) {
|
|
624
|
+
logger.warn(`Processed content (${processedContent.length} chars) exceeds max size (${config.max_size}). Skipping ${filePath}.`);
|
|
625
|
+
skippedFiles++;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
await processFileContent(filePath, processedContent);
|
|
629
|
+
processedFiles++;
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
logger.error(`Error processing file ${filePath}:`, error);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
logger.info(`Directory processed. Processed: ${processedFiles}, Skipped: ${skippedFiles}`);
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
logger.error(`Error reading directory ${dirPath}:`, error);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async chunkMarkdown(markdown, sourceConfig, url) {
|
|
643
|
+
const logger = this.logger.child('chunker');
|
|
644
|
+
logger.debug(`Chunking markdown from ${url} (${markdown.length} chars)`);
|
|
645
|
+
const MAX_TOKENS = 1000;
|
|
646
|
+
const chunks = [];
|
|
647
|
+
const lines = markdown.split("\n");
|
|
648
|
+
let currentChunk = "";
|
|
649
|
+
let headingHierarchy = [];
|
|
650
|
+
const processChunk = () => {
|
|
651
|
+
if (currentChunk.trim()) {
|
|
652
|
+
const tokens = utils_1.Utils.tokenize(currentChunk);
|
|
653
|
+
if (tokens.length > MAX_TOKENS) {
|
|
654
|
+
logger.debug(`Chunk exceeds max token count (${tokens.length}), splitting into smaller chunks`);
|
|
655
|
+
let subChunk = "";
|
|
656
|
+
let tokenCount = 0;
|
|
657
|
+
const overlapSize = Math.floor(MAX_TOKENS * 0.05);
|
|
658
|
+
let lastTokens = [];
|
|
659
|
+
for (const token of tokens) {
|
|
660
|
+
if (tokenCount + 1 > MAX_TOKENS) {
|
|
661
|
+
chunks.push(createDocumentChunk(subChunk, headingHierarchy));
|
|
662
|
+
subChunk = lastTokens.join("") + token;
|
|
663
|
+
tokenCount = lastTokens.length + 1;
|
|
664
|
+
lastTokens = [];
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
subChunk += token;
|
|
668
|
+
tokenCount++;
|
|
669
|
+
lastTokens.push(token);
|
|
670
|
+
if (lastTokens.length > overlapSize) {
|
|
671
|
+
lastTokens.shift();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (subChunk) {
|
|
676
|
+
chunks.push(createDocumentChunk(subChunk, headingHierarchy));
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
chunks.push(createDocumentChunk(currentChunk, headingHierarchy));
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
currentChunk = "";
|
|
684
|
+
};
|
|
685
|
+
const createDocumentChunk = (content, hierarchy) => {
|
|
686
|
+
const chunkId = utils_1.Utils.generateHash(content);
|
|
687
|
+
logger.debug(`Created chunk ${chunkId.substring(0, 8)}... with ${content.length} chars`);
|
|
688
|
+
return {
|
|
689
|
+
content,
|
|
690
|
+
metadata: {
|
|
691
|
+
product_name: sourceConfig.product_name,
|
|
692
|
+
version: sourceConfig.version,
|
|
693
|
+
heading_hierarchy: [...hierarchy],
|
|
694
|
+
section: hierarchy[hierarchy.length - 1] || "Introduction",
|
|
695
|
+
chunk_id: chunkId,
|
|
696
|
+
url: url,
|
|
697
|
+
hash: utils_1.Utils.generateHash(content)
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
};
|
|
701
|
+
for (const line of lines) {
|
|
702
|
+
if (line.startsWith("#")) {
|
|
703
|
+
processChunk();
|
|
704
|
+
const levelMatch = line.match(/^(#+)/);
|
|
705
|
+
let level = levelMatch ? levelMatch[1].length : 1;
|
|
706
|
+
const heading = line.replace(/^#+\s*/, "").trim();
|
|
707
|
+
logger.debug(`Found heading (level ${level}): ${heading}`);
|
|
708
|
+
while (headingHierarchy.length < level - 1) {
|
|
709
|
+
headingHierarchy.push("");
|
|
710
|
+
}
|
|
711
|
+
if (level <= headingHierarchy.length) {
|
|
712
|
+
headingHierarchy = headingHierarchy.slice(0, level - 1);
|
|
713
|
+
}
|
|
714
|
+
headingHierarchy[level - 1] = heading;
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
currentChunk += `${line}\n`;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
processChunk();
|
|
721
|
+
logger.debug(`Chunking complete, created ${chunks.length} chunks`);
|
|
722
|
+
return chunks;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
exports.ContentProcessor = ContentProcessor;
|