doc2vec 2.14.2 → 2.15.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/dist/content-processor.js +10 -8
- package/dist/doc2vec.js +86 -68
- package/dist/types.js +12 -0
- package/package.json +10 -7
|
@@ -72,12 +72,12 @@ exports.BrowserLaunchError = BrowserLaunchError;
|
|
|
72
72
|
* Chrome for Testing is not published)
|
|
73
73
|
* 4. undefined — let puppeteer decide
|
|
74
74
|
*/
|
|
75
|
-
function resolveBrowserExecutablePath() {
|
|
75
|
+
async function resolveBrowserExecutablePath() {
|
|
76
76
|
const override = process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
77
77
|
if (override)
|
|
78
78
|
return override;
|
|
79
79
|
try {
|
|
80
|
-
const bundled = puppeteer_1.default.executablePath();
|
|
80
|
+
const bundled = await puppeteer_1.default.executablePath();
|
|
81
81
|
if (bundled && fs.existsSync(bundled))
|
|
82
82
|
return bundled;
|
|
83
83
|
}
|
|
@@ -421,7 +421,7 @@ class ContentProcessor {
|
|
|
421
421
|
let browser = null;
|
|
422
422
|
let page = null;
|
|
423
423
|
const launchBrowser = async () => {
|
|
424
|
-
const executablePath = resolveBrowserExecutablePath();
|
|
424
|
+
const executablePath = await resolveBrowserExecutablePath();
|
|
425
425
|
const launchOptions = {
|
|
426
426
|
executablePath,
|
|
427
427
|
args: [
|
|
@@ -487,7 +487,7 @@ class ContentProcessor {
|
|
|
487
487
|
// If protocol errors are persisting after page recreation, the browser
|
|
488
488
|
// process itself is degraded — a full restart is needed.
|
|
489
489
|
const needsBrowserRestart = pageNeedsRecreation && consecutiveProtocolErrors >= 1;
|
|
490
|
-
if (!browser || !browser.
|
|
490
|
+
if (!browser || !browser.connected || needsBrowserRestart) {
|
|
491
491
|
const reason = needsBrowserRestart
|
|
492
492
|
? `protocol errors persisting after page recreation (${consecutiveProtocolErrors} consecutive)`
|
|
493
493
|
: browser ? 'browser disconnected' : 'initial launch';
|
|
@@ -793,7 +793,9 @@ class ContentProcessor {
|
|
|
793
793
|
// A browser that cannot launch is fatal for the whole crawl —
|
|
794
794
|
// rethrow so the source (and the run) is reported as failed
|
|
795
795
|
// rather than logging an error per URL and finishing "green".
|
|
796
|
-
|
|
796
|
+
// Same for a permanent embedding failure (bad API key):
|
|
797
|
+
// every remaining page would fail identically.
|
|
798
|
+
if (error instanceof BrowserLaunchError || error?.name === 'FatalEmbeddingError') {
|
|
797
799
|
logger.error(`Aborting crawl of ${baseUrl}: ${error.message}`);
|
|
798
800
|
throw error;
|
|
799
801
|
}
|
|
@@ -858,7 +860,7 @@ class ContentProcessor {
|
|
|
858
860
|
finally {
|
|
859
861
|
// Close the shared browser instance when the crawl is done
|
|
860
862
|
const browserToClose = browser;
|
|
861
|
-
if (browserToClose && browserToClose.
|
|
863
|
+
if (browserToClose && browserToClose.connected) {
|
|
862
864
|
await browserToClose.close();
|
|
863
865
|
logger.debug('Shared browser closed after crawl completed');
|
|
864
866
|
}
|
|
@@ -962,7 +964,7 @@ class ContentProcessor {
|
|
|
962
964
|
else {
|
|
963
965
|
// Standalone mode: launch a browser for this single page
|
|
964
966
|
browser = await puppeteer_1.default.launch({
|
|
965
|
-
executablePath: resolveBrowserExecutablePath(),
|
|
967
|
+
executablePath: await resolveBrowserExecutablePath(),
|
|
966
968
|
args: [
|
|
967
969
|
'--no-sandbox',
|
|
968
970
|
'--disable-setuid-sandbox',
|
|
@@ -1143,7 +1145,7 @@ class ContentProcessor {
|
|
|
1143
1145
|
}
|
|
1144
1146
|
finally {
|
|
1145
1147
|
// Only close the browser if we launched it ourselves (standalone mode)
|
|
1146
|
-
if (ownsTheBrowser && browser && browser.
|
|
1148
|
+
if (ownsTheBrowser && browser && browser.connected) {
|
|
1147
1149
|
await browser.close();
|
|
1148
1150
|
logger.debug(`Browser closed for ${url}`);
|
|
1149
1151
|
}
|
package/dist/doc2vec.js
CHANGED
|
@@ -39,7 +39,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
40
|
exports.Doc2Vec = void 0;
|
|
41
41
|
const axios_1 = __importDefault(require("axios"));
|
|
42
|
-
const crypto_1 = __importDefault(require("crypto"));
|
|
43
42
|
const yaml = __importStar(require("js-yaml"));
|
|
44
43
|
const fs = __importStar(require("fs"));
|
|
45
44
|
const path = __importStar(require("path"));
|
|
@@ -481,6 +480,30 @@ class Doc2Vec {
|
|
|
481
480
|
};
|
|
482
481
|
const chunks = await this.contentProcessor.chunkMarkdown(markdown, issueConfig, url);
|
|
483
482
|
logger.info(`Issue #${issueNumber}: Created ${chunks.length} chunks`);
|
|
483
|
+
// Embed every chunk BEFORE touching stored data, so an embedding
|
|
484
|
+
// failure can't purge the issue's existing chunks and store nothing
|
|
485
|
+
// back. A FatalEmbeddingError (bad API key) propagates and aborts
|
|
486
|
+
// the source; transient failures skip this issue but count toward
|
|
487
|
+
// failedChunks so the last-run date is not advanced.
|
|
488
|
+
const embedded = [];
|
|
489
|
+
let failedEmbeddings = 0;
|
|
490
|
+
for (const chunk of chunks) {
|
|
491
|
+
const chunkId = chunk.metadata.chunk_id.substring(0, 8) + '...';
|
|
492
|
+
const embeddings = await this.createEmbeddings([chunk.content]);
|
|
493
|
+
if (embeddings.length) {
|
|
494
|
+
embedded.push(embeddings[0]);
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
failedEmbeddings++;
|
|
498
|
+
logger.error(`Embedding failed for chunk: ${chunkId}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (failedEmbeddings > 0) {
|
|
502
|
+
failedChunks += failedEmbeddings;
|
|
503
|
+
logger.error(`Issue #${issueNumber}: ${failedEmbeddings}/${chunks.length} chunks failed to embed; ` +
|
|
504
|
+
`keeping its existing chunks so the next run retries`);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
484
507
|
// Purge the issue's existing chunks before inserting the fresh set.
|
|
485
508
|
// chunk_id is a content hash, so when an issue changes (closed/reopened,
|
|
486
509
|
// edited, new comments) the regenerated chunks get new ids and the old
|
|
@@ -501,57 +524,21 @@ class Doc2Vec {
|
|
|
501
524
|
else {
|
|
502
525
|
this.counters.items_new++;
|
|
503
526
|
}
|
|
504
|
-
//
|
|
505
|
-
for (
|
|
527
|
+
// Store each chunk
|
|
528
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
529
|
+
const chunk = chunks[i];
|
|
506
530
|
const chunkHash = utils_1.Utils.generateHash(chunk.content);
|
|
507
531
|
const chunkId = chunk.metadata.chunk_id.substring(0, 8) + '...';
|
|
508
532
|
if (dbConnection.type === 'sqlite') {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
logger.info(`Skipping unchanged chunk: ${chunkId}`);
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
const embeddings = await this.createEmbeddings([chunk.content]);
|
|
516
|
-
if (embeddings.length) {
|
|
517
|
-
database_1.DatabaseManager.insertVectorsSQLite(dbConnection.db, chunk, embeddings[0], logger, chunkHash);
|
|
518
|
-
this.counters.chunks_added++;
|
|
519
|
-
logger.debug(`Stored chunk ${chunkId} in SQLite`);
|
|
520
|
-
}
|
|
521
|
-
else {
|
|
522
|
-
logger.error(`Embedding failed for chunk: ${chunkId}`);
|
|
523
|
-
}
|
|
533
|
+
database_1.DatabaseManager.insertVectorsSQLite(dbConnection.db, chunk, embedded[i], logger, chunkHash);
|
|
534
|
+
this.counters.chunks_added++;
|
|
535
|
+
logger.debug(`Stored chunk ${chunkId} in SQLite`);
|
|
524
536
|
}
|
|
525
537
|
else if (dbConnection.type === 'qdrant') {
|
|
526
538
|
try {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (!utils_1.Utils.isValidUuid(pointId)) {
|
|
531
|
-
pointId = utils_1.Utils.hashToUuid(chunk.metadata.chunk_id);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
catch (e) {
|
|
535
|
-
pointId = crypto_1.default.randomUUID();
|
|
536
|
-
}
|
|
537
|
-
const existingPoints = await dbConnection.client.retrieve(dbConnection.collectionName, {
|
|
538
|
-
ids: [pointId],
|
|
539
|
-
with_payload: true,
|
|
540
|
-
with_vector: false,
|
|
541
|
-
});
|
|
542
|
-
if (existingPoints.length > 0 && existingPoints[0].payload && existingPoints[0].payload.hash === chunkHash) {
|
|
543
|
-
logger.info(`Skipping unchanged chunk: ${chunkId}`);
|
|
544
|
-
continue;
|
|
545
|
-
}
|
|
546
|
-
const embeddings = await this.createEmbeddings([chunk.content]);
|
|
547
|
-
if (embeddings.length) {
|
|
548
|
-
await database_1.DatabaseManager.storeChunkInQdrant(dbConnection, chunk, embeddings[0], chunkHash);
|
|
549
|
-
this.counters.chunks_added++;
|
|
550
|
-
logger.debug(`Stored chunk ${chunkId} in Qdrant (${dbConnection.collectionName})`);
|
|
551
|
-
}
|
|
552
|
-
else {
|
|
553
|
-
logger.error(`Embedding failed for chunk: ${chunkId}`);
|
|
554
|
-
}
|
|
539
|
+
await database_1.DatabaseManager.storeChunkInQdrant(dbConnection, chunk, embedded[i], chunkHash);
|
|
540
|
+
this.counters.chunks_added++;
|
|
541
|
+
logger.debug(`Stored chunk ${chunkId} in Qdrant (${dbConnection.collectionName})`);
|
|
555
542
|
}
|
|
556
543
|
catch (error) {
|
|
557
544
|
// Keep going so one bad chunk can't cost the whole repo,
|
|
@@ -680,6 +667,8 @@ class Doc2Vec {
|
|
|
680
667
|
return true;
|
|
681
668
|
}
|
|
682
669
|
catch (error) {
|
|
670
|
+
if (error instanceof types_1.FatalEmbeddingError)
|
|
671
|
+
throw error;
|
|
683
672
|
logger.error(`Error during chunking or embedding for ${url}:`, error);
|
|
684
673
|
return false;
|
|
685
674
|
}
|
|
@@ -829,6 +818,8 @@ class Doc2Vec {
|
|
|
829
818
|
await this.processChunksForUrl(chunks, fileUrl, dbConnection, logger);
|
|
830
819
|
}
|
|
831
820
|
catch (error) {
|
|
821
|
+
if (error instanceof types_1.FatalEmbeddingError)
|
|
822
|
+
throw error;
|
|
832
823
|
logger.error(`Error during chunking or embedding for ${filePath}:`, error);
|
|
833
824
|
}
|
|
834
825
|
}, logger);
|
|
@@ -999,6 +990,8 @@ class Doc2Vec {
|
|
|
999
990
|
processedCount++;
|
|
1000
991
|
}
|
|
1001
992
|
catch (error) {
|
|
993
|
+
if (error instanceof types_1.FatalEmbeddingError)
|
|
994
|
+
throw error;
|
|
1002
995
|
logger.error(`Error processing S3 object ${obj.key}:`, error);
|
|
1003
996
|
}
|
|
1004
997
|
}
|
|
@@ -1155,6 +1148,8 @@ class Doc2Vec {
|
|
|
1155
1148
|
await this.processChunksForUrl(chunks, fileUrl, dbConnection, logger);
|
|
1156
1149
|
}
|
|
1157
1150
|
catch (error) {
|
|
1151
|
+
if (error instanceof types_1.FatalEmbeddingError)
|
|
1152
|
+
throw error;
|
|
1158
1153
|
logger.error(`Error during code chunking or embedding for ${filePath}:`, error);
|
|
1159
1154
|
}
|
|
1160
1155
|
}, logger, undefined, {
|
|
@@ -1594,6 +1589,8 @@ class Doc2Vec {
|
|
|
1594
1589
|
totalTickets++;
|
|
1595
1590
|
}
|
|
1596
1591
|
catch (error) {
|
|
1592
|
+
if (error instanceof types_1.FatalEmbeddingError)
|
|
1593
|
+
throw error;
|
|
1597
1594
|
failedTickets++;
|
|
1598
1595
|
logger.error(`Failed to process ticket #${ticket.id}, will retry next run: ${error.message}`);
|
|
1599
1596
|
}
|
|
@@ -1781,9 +1778,36 @@ class Doc2Vec {
|
|
|
1781
1778
|
this.counters.items_unchanged++;
|
|
1782
1779
|
return 0;
|
|
1783
1780
|
}
|
|
1784
|
-
// 4. Content changed —
|
|
1781
|
+
// 4. Content changed — embed all new chunks BEFORE touching stored
|
|
1782
|
+
// data, so an embedding failure (e.g. a bad API key) can't delete the
|
|
1783
|
+
// existing chunks and then store nothing back. Throwing here also
|
|
1784
|
+
// signals the caller that the item failed: for websites that means the
|
|
1785
|
+
// ETag/lastmod is not stored and the page is retried next run.
|
|
1786
|
+
const chunkProgress = logger.progress(`Embedding chunks for ${url}`, chunks.length);
|
|
1787
|
+
const embedded = [];
|
|
1788
|
+
let failedEmbeddings = 0;
|
|
1789
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1790
|
+
const chunk = chunks[i];
|
|
1791
|
+
const chunkId = chunk.metadata.chunk_id.substring(0, 8) + '...';
|
|
1792
|
+
const embeddings = await this.createEmbeddings([chunk.content]);
|
|
1793
|
+
if (embeddings.length > 0) {
|
|
1794
|
+
embedded[i] = embeddings[0];
|
|
1795
|
+
chunkProgress.update(1, `Embedded chunk ${chunkId}`);
|
|
1796
|
+
}
|
|
1797
|
+
else {
|
|
1798
|
+
failedEmbeddings++;
|
|
1799
|
+
logger.error(`Embedding failed for chunk: ${chunkId}`);
|
|
1800
|
+
chunkProgress.update(1, `Failed to embed chunk ${chunkId}`);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (failedEmbeddings > 0) {
|
|
1804
|
+
chunkProgress.complete();
|
|
1805
|
+
throw new Error(`embedding failed for ${failedEmbeddings}/${chunks.length} chunks of ${url}; ` +
|
|
1806
|
+
`keeping the ${existingHashesSorted.length} existing chunks so the next run retries`);
|
|
1807
|
+
}
|
|
1808
|
+
// 5. All embeddings succeeded — delete the old chunks and insert the new set
|
|
1785
1809
|
if (existingHashesSorted.length > 0) {
|
|
1786
|
-
logger.info(`Content changed for ${url} (${existingHashesSorted.length} old chunks → ${chunks.length} new chunks),
|
|
1810
|
+
logger.info(`Content changed for ${url} (${existingHashesSorted.length} old chunks → ${chunks.length} new chunks), replacing all`);
|
|
1787
1811
|
if (dbConnection.type === 'sqlite') {
|
|
1788
1812
|
database_1.DatabaseManager.removeChunksByUrlSQLite(dbConnection.db, url, logger);
|
|
1789
1813
|
}
|
|
@@ -1796,34 +1820,22 @@ class Doc2Vec {
|
|
|
1796
1820
|
else {
|
|
1797
1821
|
this.counters.items_new++;
|
|
1798
1822
|
}
|
|
1799
|
-
// 5. Embed and insert all new chunks
|
|
1800
|
-
let embeddedCount = 0;
|
|
1801
|
-
const chunkProgress = logger.progress(`Embedding chunks for ${url}`, chunks.length);
|
|
1802
1823
|
for (let i = 0; i < chunks.length; i++) {
|
|
1803
1824
|
const chunk = chunks[i];
|
|
1804
1825
|
const chunkHash = newHashes[i];
|
|
1805
1826
|
const chunkId = chunk.metadata.chunk_id.substring(0, 8) + '...';
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
if (dbConnection.type === 'sqlite') {
|
|
1810
|
-
database_1.DatabaseManager.insertVectorsSQLite(dbConnection.db, chunk, embedding, logger, chunkHash);
|
|
1811
|
-
chunkProgress.update(1, `Stored chunk ${chunkId} in SQLite`);
|
|
1812
|
-
}
|
|
1813
|
-
else if (dbConnection.type === 'qdrant') {
|
|
1814
|
-
await database_1.DatabaseManager.storeChunkInQdrant(dbConnection, chunk, embedding, chunkHash);
|
|
1815
|
-
chunkProgress.update(1, `Stored chunk ${chunkId} in Qdrant (${dbConnection.collectionName})`);
|
|
1816
|
-
}
|
|
1817
|
-
embeddedCount++;
|
|
1827
|
+
if (dbConnection.type === 'sqlite') {
|
|
1828
|
+
database_1.DatabaseManager.insertVectorsSQLite(dbConnection.db, chunk, embedded[i], logger, chunkHash);
|
|
1829
|
+
logger.debug(`Stored chunk ${chunkId} in SQLite`);
|
|
1818
1830
|
}
|
|
1819
|
-
else {
|
|
1820
|
-
|
|
1821
|
-
|
|
1831
|
+
else if (dbConnection.type === 'qdrant') {
|
|
1832
|
+
await database_1.DatabaseManager.storeChunkInQdrant(dbConnection, chunk, embedded[i], chunkHash);
|
|
1833
|
+
logger.debug(`Stored chunk ${chunkId} in Qdrant (${dbConnection.collectionName})`);
|
|
1822
1834
|
}
|
|
1823
1835
|
}
|
|
1824
1836
|
chunkProgress.complete();
|
|
1825
|
-
this.counters.chunks_added +=
|
|
1826
|
-
return
|
|
1837
|
+
this.counters.chunks_added += chunks.length;
|
|
1838
|
+
return chunks.length;
|
|
1827
1839
|
}
|
|
1828
1840
|
async createEmbeddings(texts) {
|
|
1829
1841
|
const logger = this.logger.child('embeddings');
|
|
@@ -1852,6 +1864,12 @@ class Doc2Vec {
|
|
|
1852
1864
|
}
|
|
1853
1865
|
catch (error) {
|
|
1854
1866
|
logger.error('Failed to create embeddings:', error);
|
|
1867
|
+
// An auth rejection is permanent: every later request in this run
|
|
1868
|
+
// fails the same way, so abort the source instead of returning []
|
|
1869
|
+
// and letting each chunk degrade into a logged-and-skipped failure.
|
|
1870
|
+
if (error?.status === 401 || error?.status === 403) {
|
|
1871
|
+
throw new types_1.FatalEmbeddingError(`embedding request rejected with HTTP ${error.status}: ${error?.message ?? error}`);
|
|
1872
|
+
}
|
|
1855
1873
|
return [];
|
|
1856
1874
|
}
|
|
1857
1875
|
}
|
package/dist/types.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FatalEmbeddingError = void 0;
|
|
3
4
|
exports.newSourceRunCounters = newSourceRunCounters;
|
|
5
|
+
// A permanent embedding failure (e.g. an invalid API key): every subsequent
|
|
6
|
+
// request in the run would fail identically, so callers must abort the source
|
|
7
|
+
// instead of degrading each chunk into a logged-and-skipped failure.
|
|
8
|
+
class FatalEmbeddingError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'FatalEmbeddingError';
|
|
12
|
+
Object.setPrototypeOf(this, FatalEmbeddingError.prototype);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.FatalEmbeddingError = FatalEmbeddingError;
|
|
4
16
|
function newSourceRunCounters(itemsKind) {
|
|
5
17
|
return {
|
|
6
18
|
items_kind: itemsKind,
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doc2vec",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"type": "commonjs",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22"
|
|
7
|
+
},
|
|
5
8
|
"description": "",
|
|
6
9
|
"main": "dist/doc2vec.js",
|
|
7
10
|
"private": false,
|
|
@@ -33,23 +36,23 @@
|
|
|
33
36
|
"@aws-sdk/client-s3": "^3.1004.0",
|
|
34
37
|
"@chonkiejs/core": "^0.0.7",
|
|
35
38
|
"@mozilla/readability": "^0.4.4",
|
|
36
|
-
"@qdrant/js-client-rest": "^1.
|
|
37
|
-
"@qdrant/qdrant-js": "^1.
|
|
38
|
-
"axios": "^1.
|
|
39
|
-
"better-sqlite3": "^
|
|
39
|
+
"@qdrant/js-client-rest": "^1.19.0",
|
|
40
|
+
"@qdrant/qdrant-js": "^1.19.0",
|
|
41
|
+
"axios": "^1.19.0",
|
|
42
|
+
"better-sqlite3": "^13.0.3",
|
|
40
43
|
"chalk": "^5.4.1",
|
|
41
44
|
"cheerio": "^1.0.0-rc.12",
|
|
42
45
|
"commander": "^15.0.0",
|
|
43
46
|
"croner": "^10.0.1",
|
|
44
47
|
"dotenv": "^16.3.1",
|
|
45
48
|
"express": "^5.2.1",
|
|
46
|
-
"js-yaml": "^4.1
|
|
49
|
+
"js-yaml": "^4.3.1",
|
|
47
50
|
"jsdom": "^26.0.0",
|
|
48
51
|
"mammoth": "^1.11.0",
|
|
49
52
|
"openai": "^4.20.1",
|
|
50
53
|
"pdfjs-dist": "^5.3.31",
|
|
51
54
|
"pg": "^8.18.0",
|
|
52
|
-
"puppeteer": "^
|
|
55
|
+
"puppeteer": "^25.8.0",
|
|
53
56
|
"sanitize-html": "^2.11.0",
|
|
54
57
|
"sqlite-vec": "0.1.7-alpha.2",
|
|
55
58
|
"tree-sitter-wasms": "^0.1.0",
|