doc2vec 2.0.0 → 2.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 +64 -5
- package/dist/code-chunker.js +179 -0
- package/dist/content-processor.js +337 -12
- package/dist/database.js +197 -7
- package/dist/doc2vec.js +472 -2
- package/dist/vitest.config.js +16 -0
- package/package.json +10 -2
package/dist/doc2vec.js
CHANGED
|
@@ -37,11 +37,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
38
|
};
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.Doc2Vec = void 0;
|
|
40
41
|
const axios_1 = __importDefault(require("axios"));
|
|
41
42
|
const crypto_1 = __importDefault(require("crypto"));
|
|
42
43
|
const yaml = __importStar(require("js-yaml"));
|
|
43
44
|
const fs = __importStar(require("fs"));
|
|
44
45
|
const path = __importStar(require("path"));
|
|
46
|
+
const os = __importStar(require("os"));
|
|
47
|
+
const child_process_1 = require("child_process");
|
|
48
|
+
const util_1 = require("util");
|
|
45
49
|
const buffer_1 = require("buffer");
|
|
46
50
|
const openai_1 = require("openai");
|
|
47
51
|
const dotenv = __importStar(require("dotenv"));
|
|
@@ -50,9 +54,11 @@ const utils_1 = require("./utils");
|
|
|
50
54
|
const database_1 = require("./database");
|
|
51
55
|
const content_processor_1 = require("./content-processor");
|
|
52
56
|
const GITHUB_TOKEN = process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
|
|
57
|
+
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
53
58
|
dotenv.config();
|
|
54
59
|
class Doc2Vec {
|
|
55
60
|
constructor(configPath) {
|
|
61
|
+
this.brokenLinksByWebsite = {};
|
|
56
62
|
this.logger = new logger_1.Logger('Doc2Vec', {
|
|
57
63
|
level: logger_1.LogLevel.DEBUG,
|
|
58
64
|
useTimestamp: true,
|
|
@@ -61,7 +67,40 @@ class Doc2Vec {
|
|
|
61
67
|
});
|
|
62
68
|
this.logger.info('Initializing Doc2Vec');
|
|
63
69
|
this.config = this.loadConfig(configPath);
|
|
64
|
-
this.
|
|
70
|
+
this.configDir = path.dirname(path.resolve(configPath));
|
|
71
|
+
// Initialize OpenAI or Azure OpenAI based on configuration
|
|
72
|
+
// Check environment variable if not specified in config
|
|
73
|
+
const embeddingProvider = this.config.embedding?.provider || process.env.EMBEDDING_PROVIDER || 'openai';
|
|
74
|
+
const embeddingConfig = this.config.embedding || { provider: embeddingProvider };
|
|
75
|
+
if (embeddingProvider === 'azure') {
|
|
76
|
+
const azureApiKey = embeddingConfig.azure?.api_key || process.env.AZURE_OPENAI_KEY;
|
|
77
|
+
const azureEndpoint = embeddingConfig.azure?.endpoint || process.env.AZURE_OPENAI_ENDPOINT;
|
|
78
|
+
const azureDeploymentName = embeddingConfig.azure?.deployment_name || process.env.AZURE_OPENAI_DEPLOYMENT_NAME || 'text-embedding-3-large';
|
|
79
|
+
const azureApiVersion = embeddingConfig.azure?.api_version || process.env.AZURE_OPENAI_API_VERSION || '2024-10-21';
|
|
80
|
+
if (!azureApiKey || !azureEndpoint) {
|
|
81
|
+
this.logger.error('Azure OpenAI requires api_key and endpoint to be configured');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
this.openai = new openai_1.AzureOpenAI({
|
|
85
|
+
apiKey: azureApiKey,
|
|
86
|
+
endpoint: azureEndpoint,
|
|
87
|
+
deployment: azureDeploymentName,
|
|
88
|
+
apiVersion: azureApiVersion,
|
|
89
|
+
});
|
|
90
|
+
this.embeddingModel = azureDeploymentName;
|
|
91
|
+
this.logger.info(`Using Azure OpenAI with deployment: ${azureDeploymentName}`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const openaiApiKey = embeddingConfig.openai?.api_key || process.env.OPENAI_API_KEY;
|
|
95
|
+
const openaiModel = embeddingConfig.openai?.model || process.env.OPENAI_MODEL || 'text-embedding-3-large';
|
|
96
|
+
if (!openaiApiKey) {
|
|
97
|
+
this.logger.error('OpenAI requires api_key to be configured');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
this.openai = new openai_1.OpenAI({ apiKey: openaiApiKey });
|
|
101
|
+
this.embeddingModel = openaiModel;
|
|
102
|
+
this.logger.info(`Using OpenAI with model: ${openaiModel}`);
|
|
103
|
+
}
|
|
65
104
|
this.contentProcessor = new content_processor_1.ContentProcessor(this.logger);
|
|
66
105
|
}
|
|
67
106
|
loadConfig(configPath) {
|
|
@@ -81,6 +120,22 @@ class Doc2Vec {
|
|
|
81
120
|
});
|
|
82
121
|
let config = yaml.load(configFile);
|
|
83
122
|
const typedConfig = config;
|
|
123
|
+
for (const source of typedConfig.sources) {
|
|
124
|
+
if (source.type === 'code') {
|
|
125
|
+
if (!source.version || String(source.version).trim().length === 0) {
|
|
126
|
+
if (source.branch && String(source.branch).trim().length > 0) {
|
|
127
|
+
source.version = source.branch;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
source.version = 'local';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (!source.version || String(source.version).trim().length === 0) {
|
|
135
|
+
logger.error(`Missing required version for ${source.type} source: ${source.product_name}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
84
139
|
logger.info(`Configuration loaded successfully, found ${typedConfig.sources.length} sources`);
|
|
85
140
|
return typedConfig;
|
|
86
141
|
}
|
|
@@ -103,6 +158,9 @@ class Doc2Vec {
|
|
|
103
158
|
else if (sourceConfig.type === 'local_directory') {
|
|
104
159
|
await this.processLocalDirectory(sourceConfig, sourceLogger);
|
|
105
160
|
}
|
|
161
|
+
else if (sourceConfig.type === 'code') {
|
|
162
|
+
await this.processCodeSource(sourceConfig, sourceLogger);
|
|
163
|
+
}
|
|
106
164
|
else if (sourceConfig.type === 'zendesk') {
|
|
107
165
|
await this.processZendesk(sourceConfig, sourceLogger);
|
|
108
166
|
}
|
|
@@ -413,6 +471,8 @@ class Doc2Vec {
|
|
|
413
471
|
logger.error(`Error during chunking or embedding for ${url}:`, error);
|
|
414
472
|
}
|
|
415
473
|
}, logger, visitedUrls);
|
|
474
|
+
this.recordBrokenLinks(config.url, crawlResult.brokenLinks);
|
|
475
|
+
this.writeBrokenLinksReport();
|
|
416
476
|
logger.info(`Found ${validChunkIds.size} valid chunks across processed pages for ${config.url}`);
|
|
417
477
|
logger.section('CLEANUP');
|
|
418
478
|
if (crawlResult.hasNetworkErrors) {
|
|
@@ -430,6 +490,38 @@ class Doc2Vec {
|
|
|
430
490
|
}
|
|
431
491
|
logger.info(`Finished processing website: ${config.url}`);
|
|
432
492
|
}
|
|
493
|
+
recordBrokenLinks(baseUrl, brokenLinks) {
|
|
494
|
+
const uniqueByKey = new Map();
|
|
495
|
+
for (const link of brokenLinks) {
|
|
496
|
+
const key = `${link.source} -> ${link.target}`;
|
|
497
|
+
if (!uniqueByKey.has(key)) {
|
|
498
|
+
uniqueByKey.set(key, link);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const unique = Array.from(uniqueByKey.values()).sort((a, b) => {
|
|
502
|
+
const sourceCompare = a.source.localeCompare(b.source);
|
|
503
|
+
if (sourceCompare !== 0)
|
|
504
|
+
return sourceCompare;
|
|
505
|
+
return a.target.localeCompare(b.target);
|
|
506
|
+
});
|
|
507
|
+
this.brokenLinksByWebsite[baseUrl] = unique;
|
|
508
|
+
}
|
|
509
|
+
writeBrokenLinksReport() {
|
|
510
|
+
const reportPath = path.join(this.configDir, '404.yaml');
|
|
511
|
+
const orderedEntries = Object.entries(this.brokenLinksByWebsite)
|
|
512
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
513
|
+
const reportPayload = orderedEntries.map(([website, links]) => ({
|
|
514
|
+
website,
|
|
515
|
+
'broken-links': links
|
|
516
|
+
}));
|
|
517
|
+
try {
|
|
518
|
+
fs.writeFileSync(reportPath, yaml.dump(reportPayload, { noRefs: true }), 'utf8');
|
|
519
|
+
this.logger.info(`Wrote broken link report to ${reportPath}`);
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
this.logger.error(`Failed to write broken link report to ${reportPath}:`, error);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
433
525
|
async processLocalDirectory(config, parentLogger) {
|
|
434
526
|
const logger = parentLogger.child('process');
|
|
435
527
|
logger.info(`Starting processing for local directory: ${config.path}`);
|
|
@@ -549,6 +641,383 @@ class Doc2Vec {
|
|
|
549
641
|
}
|
|
550
642
|
logger.info(`Finished processing local directory: ${config.path}`);
|
|
551
643
|
}
|
|
644
|
+
async processCodeSource(config, parentLogger) {
|
|
645
|
+
const logger = parentLogger.child('process');
|
|
646
|
+
logger.info(`Starting processing for code source (${config.source})`);
|
|
647
|
+
const dbConnection = await database_1.DatabaseManager.initDatabase(config, logger);
|
|
648
|
+
const validChunkIds = new Set();
|
|
649
|
+
const processedFiles = new Set();
|
|
650
|
+
let basePath;
|
|
651
|
+
let cleanupPathConfig;
|
|
652
|
+
let tempDir = null;
|
|
653
|
+
let repoUrlPrefix;
|
|
654
|
+
let repoBranch;
|
|
655
|
+
let incrementalMode = false;
|
|
656
|
+
let deleteUrls = [];
|
|
657
|
+
let allowedFiles;
|
|
658
|
+
let mtimeCutoff;
|
|
659
|
+
let fileListKey;
|
|
660
|
+
let lastMtimeKey;
|
|
661
|
+
let trackedFiles;
|
|
662
|
+
let maxObservedMtime = 0;
|
|
663
|
+
if (config.source === 'local_directory') {
|
|
664
|
+
if (!config.path) {
|
|
665
|
+
logger.error('Code source type local_directory requires a path.');
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
basePath = config.path;
|
|
669
|
+
cleanupPathConfig = config.url_rewrite_prefix
|
|
670
|
+
? { path: basePath, url_rewrite_prefix: config.url_rewrite_prefix }
|
|
671
|
+
: basePath;
|
|
672
|
+
const resolvedPath = path.resolve(basePath);
|
|
673
|
+
const pathKey = resolvedPath.replace(/[^a-zA-Z0-9]+/g, '_');
|
|
674
|
+
lastMtimeKey = `code_last_mtime_${pathKey}`;
|
|
675
|
+
fileListKey = `code_filelist_${pathKey}`;
|
|
676
|
+
await database_1.DatabaseManager.initDatabaseMetadata(dbConnection, logger);
|
|
677
|
+
const lastMtimeValue = await database_1.DatabaseManager.getMetadataValue(dbConnection, lastMtimeKey, '0', logger);
|
|
678
|
+
mtimeCutoff = lastMtimeValue ? parseFloat(lastMtimeValue) : 0;
|
|
679
|
+
trackedFiles = new Set();
|
|
680
|
+
incrementalMode = true;
|
|
681
|
+
}
|
|
682
|
+
else if (config.source === 'github') {
|
|
683
|
+
if (!config.repo) {
|
|
684
|
+
logger.error('Code source type github requires a repo in owner/repo format.');
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const cloneResult = await this.cloneGithubRepo(config, logger);
|
|
688
|
+
basePath = cloneResult.path;
|
|
689
|
+
tempDir = cloneResult.path;
|
|
690
|
+
repoUrlPrefix = cloneResult.urlPrefix;
|
|
691
|
+
repoBranch = cloneResult.branch;
|
|
692
|
+
cleanupPathConfig = { path: basePath, url_rewrite_prefix: repoUrlPrefix };
|
|
693
|
+
await database_1.DatabaseManager.initDatabaseMetadata(dbConnection, logger);
|
|
694
|
+
const shaKey = this.buildCodeShaMetadataKey(config.repo, repoBranch);
|
|
695
|
+
const lastSha = await database_1.DatabaseManager.getMetadataValue(dbConnection, shaKey, undefined, logger);
|
|
696
|
+
const headSha = await this.getRepoHeadSha(basePath, logger);
|
|
697
|
+
if (lastSha && headSha) {
|
|
698
|
+
if (headSha === lastSha) {
|
|
699
|
+
incrementalMode = true;
|
|
700
|
+
allowedFiles = new Set();
|
|
701
|
+
deleteUrls = [];
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
const diffResult = await this.getGitChangedFiles(basePath, lastSha, repoBranch, logger);
|
|
705
|
+
if (diffResult.mode === 'incremental') {
|
|
706
|
+
incrementalMode = true;
|
|
707
|
+
allowedFiles = diffResult.changedFiles;
|
|
708
|
+
deleteUrls = diffResult.deletedPaths
|
|
709
|
+
.map((relativePath) => this.buildCodeFileUrl(path.join(basePath, relativePath), basePath, config, repoUrlPrefix));
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
logger.warn('Falling back to full scan for GitHub code source.');
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
logger.error(`Unknown code source: ${config.source}`);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
logger.section('CODE SCANNING AND EMBEDDING');
|
|
722
|
+
try {
|
|
723
|
+
const scanResult = await this.contentProcessor.processCodeDirectory(basePath, config, async (filePath, content) => {
|
|
724
|
+
processedFiles.add(filePath);
|
|
725
|
+
const relativePath = path.relative(basePath, filePath).replace(/\\/g, '/');
|
|
726
|
+
const fileUrl = this.buildCodeFileUrl(filePath, basePath, config, repoUrlPrefix);
|
|
727
|
+
logger.info(`Processing code from ${relativePath || filePath} (${content.length} chars)`);
|
|
728
|
+
try {
|
|
729
|
+
const chunks = await this.contentProcessor.chunkCode(content, config, fileUrl, relativePath || filePath, repoBranch || config.branch, config.repo);
|
|
730
|
+
logger.info(`Created ${chunks.length} chunks`);
|
|
731
|
+
if (chunks.length > 0) {
|
|
732
|
+
const chunkProgress = logger.progress(`Embedding chunks for ${relativePath || filePath}`, chunks.length);
|
|
733
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
734
|
+
const chunk = chunks[i];
|
|
735
|
+
validChunkIds.add(chunk.metadata.chunk_id);
|
|
736
|
+
const chunkId = chunk.metadata.chunk_id.substring(0, 8) + '...';
|
|
737
|
+
let needsEmbedding = true;
|
|
738
|
+
const chunkHash = utils_1.Utils.generateHash(chunk.content);
|
|
739
|
+
if (dbConnection.type === 'sqlite') {
|
|
740
|
+
const { checkHashStmt } = database_1.DatabaseManager.prepareSQLiteStatements(dbConnection.db);
|
|
741
|
+
const existing = checkHashStmt.get(chunk.metadata.chunk_id);
|
|
742
|
+
if (existing && existing.hash === chunkHash) {
|
|
743
|
+
needsEmbedding = false;
|
|
744
|
+
chunkProgress.update(1, `Skipping unchanged chunk ${chunkId}`);
|
|
745
|
+
logger.info(`Skipping unchanged chunk: ${chunkId}`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
else if (dbConnection.type === 'qdrant') {
|
|
749
|
+
try {
|
|
750
|
+
let pointId;
|
|
751
|
+
try {
|
|
752
|
+
pointId = chunk.metadata.chunk_id;
|
|
753
|
+
if (!utils_1.Utils.isValidUuid(pointId)) {
|
|
754
|
+
pointId = utils_1.Utils.hashToUuid(chunk.metadata.chunk_id);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
catch (e) {
|
|
758
|
+
pointId = crypto_1.default.randomUUID();
|
|
759
|
+
}
|
|
760
|
+
const existingPoints = await dbConnection.client.retrieve(dbConnection.collectionName, {
|
|
761
|
+
ids: [pointId],
|
|
762
|
+
with_payload: true,
|
|
763
|
+
with_vector: false,
|
|
764
|
+
});
|
|
765
|
+
if (existingPoints.length > 0 && existingPoints[0].payload && existingPoints[0].payload.hash === chunkHash) {
|
|
766
|
+
needsEmbedding = false;
|
|
767
|
+
chunkProgress.update(1, `Skipping unchanged chunk ${chunkId}`);
|
|
768
|
+
logger.info(`Skipping unchanged chunk: ${chunkId}`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
catch (error) {
|
|
772
|
+
logger.error(`Error checking existing point in Qdrant:`, error);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (needsEmbedding) {
|
|
776
|
+
const embeddings = await this.createEmbeddings([chunk.content]);
|
|
777
|
+
if (embeddings.length > 0) {
|
|
778
|
+
const embedding = embeddings[0];
|
|
779
|
+
if (dbConnection.type === 'sqlite') {
|
|
780
|
+
database_1.DatabaseManager.insertVectorsSQLite(dbConnection.db, chunk, embedding, logger, chunkHash);
|
|
781
|
+
chunkProgress.update(1, `Stored chunk ${chunkId} in SQLite`);
|
|
782
|
+
}
|
|
783
|
+
else if (dbConnection.type === 'qdrant') {
|
|
784
|
+
await database_1.DatabaseManager.storeChunkInQdrant(dbConnection, chunk, embedding, chunkHash);
|
|
785
|
+
chunkProgress.update(1, `Stored chunk ${chunkId} in Qdrant (${dbConnection.collectionName})`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
logger.error(`Embedding failed for chunk: ${chunkId}`);
|
|
790
|
+
chunkProgress.update(1, `Failed to embed chunk ${chunkId}`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
chunkProgress.complete();
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
catch (error) {
|
|
798
|
+
logger.error(`Error during code chunking or embedding for ${filePath}:`, error);
|
|
799
|
+
}
|
|
800
|
+
}, logger, undefined, {
|
|
801
|
+
allowedFiles,
|
|
802
|
+
mtimeCutoff,
|
|
803
|
+
trackFiles: trackedFiles
|
|
804
|
+
});
|
|
805
|
+
if (trackedFiles) {
|
|
806
|
+
maxObservedMtime = scanResult.maxMtime;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
finally {
|
|
810
|
+
logger.section('CLEANUP');
|
|
811
|
+
if (incrementalMode) {
|
|
812
|
+
if (deleteUrls.length > 0) {
|
|
813
|
+
logger.info(`Cleaning up ${deleteUrls.length} deleted/renamed files`);
|
|
814
|
+
for (const url of deleteUrls) {
|
|
815
|
+
if (dbConnection.type === 'sqlite') {
|
|
816
|
+
database_1.DatabaseManager.removeChunksByUrlSQLite(dbConnection.db, url, logger);
|
|
817
|
+
}
|
|
818
|
+
else if (dbConnection.type === 'qdrant') {
|
|
819
|
+
await database_1.DatabaseManager.removeChunksByUrlQdrant(dbConnection, url, logger);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
logger.info('No deleted/renamed files to clean up.');
|
|
825
|
+
}
|
|
826
|
+
if (trackedFiles && fileListKey) {
|
|
827
|
+
const previousListValue = await database_1.DatabaseManager.getMetadataValue(dbConnection, fileListKey, '[]', logger);
|
|
828
|
+
const previousList = previousListValue ? JSON.parse(previousListValue) : [];
|
|
829
|
+
const currentList = Array.from(trackedFiles);
|
|
830
|
+
const deletedFiles = previousList.filter((filePath) => !trackedFiles?.has(filePath));
|
|
831
|
+
for (const deletedFile of deletedFiles) {
|
|
832
|
+
const url = this.buildCodeFileUrl(deletedFile, basePath, config, repoUrlPrefix);
|
|
833
|
+
if (dbConnection.type === 'sqlite') {
|
|
834
|
+
database_1.DatabaseManager.removeChunksByUrlSQLite(dbConnection.db, url, logger);
|
|
835
|
+
}
|
|
836
|
+
else if (dbConnection.type === 'qdrant') {
|
|
837
|
+
await database_1.DatabaseManager.removeChunksByUrlQdrant(dbConnection, url, logger);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
await database_1.DatabaseManager.setMetadataValue(dbConnection, fileListKey, JSON.stringify(currentList), logger);
|
|
841
|
+
if (lastMtimeKey) {
|
|
842
|
+
const nextMtime = maxObservedMtime > 0 ? maxObservedMtime : Date.now();
|
|
843
|
+
await database_1.DatabaseManager.setMetadataValue(dbConnection, lastMtimeKey, `${nextMtime}`, logger);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
if (dbConnection.type === 'sqlite') {
|
|
849
|
+
logger.info(`Running SQLite cleanup for code source ${basePath}`);
|
|
850
|
+
database_1.DatabaseManager.removeObsoleteFilesSQLite(dbConnection.db, processedFiles, cleanupPathConfig, logger);
|
|
851
|
+
}
|
|
852
|
+
else if (dbConnection.type === 'qdrant') {
|
|
853
|
+
logger.info(`Running Qdrant cleanup for code source ${basePath} in collection ${dbConnection.collectionName}`);
|
|
854
|
+
await database_1.DatabaseManager.removeObsoleteFilesQdrant(dbConnection, processedFiles, cleanupPathConfig, logger);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (config.source === 'github' && basePath && repoBranch) {
|
|
858
|
+
const headSha = await this.getRepoHeadSha(basePath, logger);
|
|
859
|
+
if (headSha) {
|
|
860
|
+
const shaKey = this.buildCodeShaMetadataKey(config.repo, repoBranch);
|
|
861
|
+
await database_1.DatabaseManager.setMetadataValue(dbConnection, shaKey, headSha, logger);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (tempDir) {
|
|
865
|
+
try {
|
|
866
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
867
|
+
logger.debug(`Removed temporary repo at ${tempDir}`);
|
|
868
|
+
}
|
|
869
|
+
catch (error) {
|
|
870
|
+
logger.warn(`Failed to remove temporary repo at ${tempDir}:`, error);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
logger.info(`Finished processing code source (${config.source})`);
|
|
875
|
+
}
|
|
876
|
+
buildCodeShaMetadataKey(repo, branch) {
|
|
877
|
+
const normalizedRepo = repo.replace(/[^a-zA-Z0-9]+/g, '_');
|
|
878
|
+
const normalizedBranch = branch.replace(/[^a-zA-Z0-9]+/g, '_');
|
|
879
|
+
return `code_last_sha_${normalizedRepo}_${normalizedBranch}`;
|
|
880
|
+
}
|
|
881
|
+
async getRepoHeadSha(repoPath, logger) {
|
|
882
|
+
try {
|
|
883
|
+
const { stdout } = await execAsync(`git -C "${repoPath}" rev-parse HEAD`);
|
|
884
|
+
return stdout.trim() || undefined;
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
logger.warn(`Failed to resolve HEAD sha for ${repoPath}:`, error);
|
|
888
|
+
return undefined;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
async getGitChangedFiles(repoPath, lastSha, branch, logger) {
|
|
892
|
+
const diffCommand = `git -C "${repoPath}" diff --name-status ${lastSha}..HEAD`;
|
|
893
|
+
const attemptDiff = async () => {
|
|
894
|
+
const { stdout } = await execAsync(diffCommand);
|
|
895
|
+
return stdout;
|
|
896
|
+
};
|
|
897
|
+
let diffOutput;
|
|
898
|
+
try {
|
|
899
|
+
diffOutput = await attemptDiff();
|
|
900
|
+
}
|
|
901
|
+
catch (error) {
|
|
902
|
+
logger.warn(`Failed to diff against ${lastSha}. Fetching more history...`);
|
|
903
|
+
const fetchDepths = [200, 1000, 5000];
|
|
904
|
+
let fetched = false;
|
|
905
|
+
for (const depth of fetchDepths) {
|
|
906
|
+
try {
|
|
907
|
+
logger.info(`Fetching with --depth=${depth}...`);
|
|
908
|
+
await execAsync(`git -C "${repoPath}" fetch --depth=${depth} origin "${branch}"`);
|
|
909
|
+
diffOutput = await attemptDiff();
|
|
910
|
+
fetched = true;
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
catch (fetchError) {
|
|
914
|
+
logger.warn(`Diff still failed at --depth=${depth}.`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (!fetched) {
|
|
918
|
+
try {
|
|
919
|
+
logger.info(`Attempting full unshallow fetch...`);
|
|
920
|
+
await execAsync(`git -C "${repoPath}" fetch --unshallow origin "${branch}"`);
|
|
921
|
+
diffOutput = await attemptDiff();
|
|
922
|
+
}
|
|
923
|
+
catch (unshallowError) {
|
|
924
|
+
logger.warn(`Failed to diff even after full unshallow. Falling back to full scan.`, unshallowError);
|
|
925
|
+
return { mode: 'full', changedFiles: new Set(), deletedPaths: [] };
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (!diffOutput) {
|
|
930
|
+
logger.warn('No diff output available. Falling back to full scan.');
|
|
931
|
+
return { mode: 'full', changedFiles: new Set(), deletedPaths: [] };
|
|
932
|
+
}
|
|
933
|
+
const changedFiles = new Set();
|
|
934
|
+
const deletedPaths = [];
|
|
935
|
+
for (const line of diffOutput.split('\n')) {
|
|
936
|
+
const trimmed = line.trim();
|
|
937
|
+
if (!trimmed)
|
|
938
|
+
continue;
|
|
939
|
+
const parts = trimmed.split('\t');
|
|
940
|
+
const status = parts[0];
|
|
941
|
+
if (status.startsWith('R')) {
|
|
942
|
+
const oldPath = parts[1];
|
|
943
|
+
const newPath = parts[2];
|
|
944
|
+
if (oldPath)
|
|
945
|
+
deletedPaths.push(oldPath);
|
|
946
|
+
if (newPath)
|
|
947
|
+
changedFiles.add(path.join(repoPath, newPath));
|
|
948
|
+
}
|
|
949
|
+
else if (status === 'D') {
|
|
950
|
+
const deletedPath = parts[1];
|
|
951
|
+
if (deletedPath)
|
|
952
|
+
deletedPaths.push(deletedPath);
|
|
953
|
+
}
|
|
954
|
+
else if (status === 'A' || status === 'M') {
|
|
955
|
+
const changedPath = parts[1];
|
|
956
|
+
if (changedPath)
|
|
957
|
+
changedFiles.add(path.join(repoPath, changedPath));
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
logger.info(`Git diff changes: ${changedFiles.size} modified/added, ${deletedPaths.length} deleted/renamed.`);
|
|
961
|
+
return { mode: 'incremental', changedFiles, deletedPaths };
|
|
962
|
+
}
|
|
963
|
+
buildCodeFileUrl(filePath, basePath, config, repoUrlPrefix) {
|
|
964
|
+
const relativePath = path.relative(basePath, filePath).replace(/\\/g, '/');
|
|
965
|
+
if (repoUrlPrefix) {
|
|
966
|
+
return `${repoUrlPrefix}/${relativePath}`;
|
|
967
|
+
}
|
|
968
|
+
if (config.url_rewrite_prefix) {
|
|
969
|
+
if (relativePath.startsWith('..')) {
|
|
970
|
+
return `file://${filePath}`;
|
|
971
|
+
}
|
|
972
|
+
const prefix = config.url_rewrite_prefix.endsWith('/')
|
|
973
|
+
? config.url_rewrite_prefix.slice(0, -1)
|
|
974
|
+
: config.url_rewrite_prefix;
|
|
975
|
+
return `${prefix}/${relativePath}`;
|
|
976
|
+
}
|
|
977
|
+
return `file://${filePath}`;
|
|
978
|
+
}
|
|
979
|
+
async cloneGithubRepo(config, logger) {
|
|
980
|
+
const repo = config.repo;
|
|
981
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'doc2vec-code-'));
|
|
982
|
+
const requestedBranch = config.branch;
|
|
983
|
+
const encodedToken = GITHUB_TOKEN ? encodeURIComponent(GITHUB_TOKEN) : '';
|
|
984
|
+
const repoUrl = encodedToken
|
|
985
|
+
? `https://x-access-token:${encodedToken}@github.com/${repo}.git`
|
|
986
|
+
: `https://github.com/${repo}.git`;
|
|
987
|
+
const branchArg = requestedBranch ? `--branch "${requestedBranch}"` : '';
|
|
988
|
+
logger.info(`Cloning ${repo} to ${tempDir}`);
|
|
989
|
+
try {
|
|
990
|
+
await execAsync(`git clone --depth 1 ${branchArg} "${repoUrl}" "${tempDir}"`);
|
|
991
|
+
}
|
|
992
|
+
catch (error) {
|
|
993
|
+
try {
|
|
994
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
995
|
+
}
|
|
996
|
+
catch (cleanupError) {
|
|
997
|
+
logger.warn(`Failed to clean up temp dir after clone failure: ${tempDir}`, cleanupError);
|
|
998
|
+
}
|
|
999
|
+
logger.error(`Failed to clone repo ${repo}:`, error);
|
|
1000
|
+
throw error;
|
|
1001
|
+
}
|
|
1002
|
+
let resolvedBranch = requestedBranch;
|
|
1003
|
+
if (!resolvedBranch) {
|
|
1004
|
+
resolvedBranch = await this.getRepoBranch(tempDir, logger);
|
|
1005
|
+
}
|
|
1006
|
+
const branch = resolvedBranch || 'main';
|
|
1007
|
+
const urlPrefix = `https://github.com/${repo}/blob/${branch}`;
|
|
1008
|
+
return { path: tempDir, branch, urlPrefix };
|
|
1009
|
+
}
|
|
1010
|
+
async getRepoBranch(repoPath, logger) {
|
|
1011
|
+
try {
|
|
1012
|
+
const { stdout } = await execAsync(`git -C "${repoPath}" symbolic-ref --short HEAD`);
|
|
1013
|
+
const branch = stdout.trim();
|
|
1014
|
+
return branch || undefined;
|
|
1015
|
+
}
|
|
1016
|
+
catch (error) {
|
|
1017
|
+
logger.warn(`Failed to resolve repo branch for ${repoPath}:`, error);
|
|
1018
|
+
return undefined;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
552
1021
|
async processZendesk(config, parentLogger) {
|
|
553
1022
|
const logger = parentLogger.child('process');
|
|
554
1023
|
logger.info(`Starting processing for Zendesk: ${config.zendesk_subdomain}.zendesk.com`);
|
|
@@ -892,7 +1361,7 @@ class Doc2Vec {
|
|
|
892
1361
|
try {
|
|
893
1362
|
logger.debug(`Creating embeddings for ${texts.length} texts`);
|
|
894
1363
|
const response = await this.openai.embeddings.create({
|
|
895
|
-
model:
|
|
1364
|
+
model: this.embeddingModel,
|
|
896
1365
|
input: texts,
|
|
897
1366
|
});
|
|
898
1367
|
logger.debug(`Successfully created ${response.data.length} embeddings`);
|
|
@@ -904,6 +1373,7 @@ class Doc2Vec {
|
|
|
904
1373
|
}
|
|
905
1374
|
}
|
|
906
1375
|
}
|
|
1376
|
+
exports.Doc2Vec = Doc2Vec;
|
|
907
1377
|
if (require.main === module) {
|
|
908
1378
|
const configPath = process.argv[2] || 'config.yaml';
|
|
909
1379
|
if (!fs.existsSync(configPath)) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const config_1 = require("vitest/config");
|
|
4
|
+
exports.default = (0, config_1.defineConfig)({
|
|
5
|
+
test: {
|
|
6
|
+
globals: true,
|
|
7
|
+
environment: 'node',
|
|
8
|
+
include: ['tests/**/*.test.ts'],
|
|
9
|
+
testTimeout: 30000,
|
|
10
|
+
coverage: {
|
|
11
|
+
provider: 'v8',
|
|
12
|
+
include: ['utils.ts', 'logger.ts', 'content-processor.ts', 'database.ts', 'code-chunker.ts', 'doc2vec.ts'],
|
|
13
|
+
exclude: ['mcp/**', 'dist/**', 'tests/**'],
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doc2vec",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"description": "",
|
|
6
6
|
"main": "dist/doc2vec.js",
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsc && chmod 755 dist/doc2vec.js",
|
|
21
21
|
"start": "node dist/doc2vec.js",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"test:coverage": "vitest run --coverage",
|
|
22
25
|
"prepublishOnly": "npm run build",
|
|
23
26
|
"prepare": "npm run build"
|
|
24
27
|
},
|
|
@@ -26,6 +29,7 @@
|
|
|
26
29
|
"author": "",
|
|
27
30
|
"license": "ISC",
|
|
28
31
|
"dependencies": {
|
|
32
|
+
"@chonkiejs/core": "^0.0.7",
|
|
29
33
|
"@mozilla/readability": "^0.4.4",
|
|
30
34
|
"@qdrant/js-client-rest": "^1.13.0",
|
|
31
35
|
"@qdrant/qdrant-js": "^1.13.0",
|
|
@@ -42,7 +46,9 @@
|
|
|
42
46
|
"puppeteer": "^24.1.1",
|
|
43
47
|
"sanitize-html": "^2.11.0",
|
|
44
48
|
"sqlite-vec": "0.1.7-alpha.2",
|
|
49
|
+
"tree-sitter-wasms": "^0.1.0",
|
|
45
50
|
"turndown": "^7.1.2",
|
|
51
|
+
"web-tree-sitter": "^0.25.4",
|
|
46
52
|
"word-extractor": "^1.0.4"
|
|
47
53
|
},
|
|
48
54
|
"devDependencies": {
|
|
@@ -52,7 +58,9 @@
|
|
|
52
58
|
"@types/node": "^20.10.0",
|
|
53
59
|
"@types/sanitize-html": "^2.9.5",
|
|
54
60
|
"@types/turndown": "^5.0.4",
|
|
61
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
55
62
|
"ts-node": "^10.9.1",
|
|
56
|
-
"typescript": "^5.3.2"
|
|
63
|
+
"typescript": "^5.3.2",
|
|
64
|
+
"vitest": "^4.0.18"
|
|
57
65
|
}
|
|
58
66
|
}
|