doc2vec 1.2.0 → 2.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/README.md +200 -8
- package/dist/content-processor copy.js +819 -0
- package/dist/content-processor.js +306 -60
- package/dist/database.js +13 -17
- package/dist/doc2vec.js +401 -7
- package/dist/electron-app/src/database.js +218 -0
- package/dist/electron-app/src/embeddings.js +80 -0
- package/dist/electron-app/src/main.js +627 -0
- package/dist/electron-app/src/mcp-server.js +496 -0
- package/dist/electron-app/src/preload.js +24 -0
- package/dist/electron-app/src/processor.js +248 -0
- package/dist/electron-app/src/syncer.js +146 -0
- package/dist/macos-app/migrate-to-vec0.js +156 -0
- package/package.json +4 -2
package/dist/doc2vec.js
CHANGED
|
@@ -42,6 +42,7 @@ const crypto_1 = __importDefault(require("crypto"));
|
|
|
42
42
|
const yaml = __importStar(require("js-yaml"));
|
|
43
43
|
const fs = __importStar(require("fs"));
|
|
44
44
|
const path = __importStar(require("path"));
|
|
45
|
+
const buffer_1 = require("buffer");
|
|
45
46
|
const openai_1 = require("openai");
|
|
46
47
|
const dotenv = __importStar(require("dotenv"));
|
|
47
48
|
const logger_1 = require("./logger");
|
|
@@ -67,7 +68,17 @@ class Doc2Vec {
|
|
|
67
68
|
try {
|
|
68
69
|
const logger = this.logger.child('config');
|
|
69
70
|
logger.info(`Loading configuration from ${configPath}`);
|
|
70
|
-
|
|
71
|
+
let configFile = fs.readFileSync(configPath, 'utf8');
|
|
72
|
+
// Substitute environment variables in the format ${VAR_NAME}
|
|
73
|
+
configFile = configFile.replace(/\$\{([^}]+)\}/g, (match, varName) => {
|
|
74
|
+
const envValue = process.env[varName];
|
|
75
|
+
if (envValue === undefined) {
|
|
76
|
+
logger.warn(`Environment variable ${varName} not found, keeping placeholder ${match}`);
|
|
77
|
+
return match;
|
|
78
|
+
}
|
|
79
|
+
logger.debug(`Substituted ${match} with environment variable value`);
|
|
80
|
+
return envValue;
|
|
81
|
+
});
|
|
71
82
|
let config = yaml.load(configFile);
|
|
72
83
|
const typedConfig = config;
|
|
73
84
|
logger.info(`Configuration loaded successfully, found ${typedConfig.sources.length} sources`);
|
|
@@ -92,6 +103,9 @@ class Doc2Vec {
|
|
|
92
103
|
else if (sourceConfig.type === 'local_directory') {
|
|
93
104
|
await this.processLocalDirectory(sourceConfig, sourceLogger);
|
|
94
105
|
}
|
|
106
|
+
else if (sourceConfig.type === 'zendesk') {
|
|
107
|
+
await this.processZendesk(sourceConfig, sourceLogger);
|
|
108
|
+
}
|
|
95
109
|
else {
|
|
96
110
|
sourceLogger.error(`Unknown source type: ${sourceConfig.type}`);
|
|
97
111
|
}
|
|
@@ -109,26 +123,64 @@ class Doc2Vec {
|
|
|
109
123
|
const fetchWithRetry = async (url, params = {}, retries = 5, delay = 5000) => {
|
|
110
124
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
111
125
|
try {
|
|
126
|
+
// Only log on retries to reduce noise during pagination
|
|
127
|
+
if (attempt > 0) {
|
|
128
|
+
logger.debug(`GitHub API retry: ${url} (attempt ${attempt + 1}/${retries})`);
|
|
129
|
+
}
|
|
112
130
|
const response = await axios_1.default.get(url, {
|
|
113
131
|
headers: {
|
|
114
132
|
Authorization: `token ${GITHUB_TOKEN}`,
|
|
115
133
|
Accept: 'application/vnd.github.v3+json',
|
|
116
134
|
},
|
|
117
135
|
params,
|
|
136
|
+
timeout: 30000, // 30 second timeout
|
|
118
137
|
});
|
|
119
138
|
return response.data;
|
|
120
139
|
}
|
|
121
140
|
catch (error) {
|
|
141
|
+
// Enhanced error logging for debugging
|
|
142
|
+
const errorDetails = {
|
|
143
|
+
code: error.code,
|
|
144
|
+
message: error.message,
|
|
145
|
+
status: error.response?.status,
|
|
146
|
+
isTimeout: error.code === 'ECONNABORTED' || error.message?.includes('timeout'),
|
|
147
|
+
isNetworkError: !error.response && error.code,
|
|
148
|
+
};
|
|
149
|
+
logger.debug(`GitHub API error details: ${JSON.stringify(errorDetails)}`);
|
|
122
150
|
if (error.response && error.response.status === 403) {
|
|
151
|
+
// Check if this is actually a rate limit error
|
|
152
|
+
const rateLimitRemaining = error.response.headers['x-ratelimit-remaining'];
|
|
123
153
|
const resetTime = error.response.headers['x-ratelimit-reset'];
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
154
|
+
if (rateLimitRemaining === '0' && resetTime) {
|
|
155
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
156
|
+
const resetTimestamp = parseInt(resetTime, 10);
|
|
157
|
+
let waitTime = (resetTimestamp - currentTime) * 1000;
|
|
158
|
+
// Ensure waitTime is at least 1 second (in case resetTime is in the past)
|
|
159
|
+
if (waitTime < 1000) {
|
|
160
|
+
waitTime = 1000;
|
|
161
|
+
}
|
|
162
|
+
logger.warn(`GitHub rate limit exceeded. Waiting ${Math.ceil(waitTime / 1000)}s (attempt ${attempt + 1}/${retries})`);
|
|
163
|
+
await new Promise(res => setTimeout(res, waitTime));
|
|
164
|
+
// Retry the request after waiting
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// Other 403 errors (e.g., forbidden access)
|
|
169
|
+
logger.error(`GitHub API returned 403 (not rate limit): ${error.message}`);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
128
172
|
}
|
|
129
173
|
else {
|
|
130
|
-
|
|
131
|
-
|
|
174
|
+
// For non-403 errors, wait before retrying (exponential backoff)
|
|
175
|
+
if (attempt < retries - 1) {
|
|
176
|
+
const backoffDelay = delay * Math.pow(2, attempt);
|
|
177
|
+
logger.warn(`GitHub fetch failed (attempt ${attempt + 1}/${retries}): ${error.message}. Retrying in ${backoffDelay}ms`);
|
|
178
|
+
await new Promise(res => setTimeout(res, backoffDelay));
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
logger.error(`GitHub fetch failed after ${retries} attempts: ${error.message} (code: ${error.code || 'unknown'})`);
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
132
184
|
}
|
|
133
185
|
}
|
|
134
186
|
}
|
|
@@ -140,6 +192,10 @@ class Doc2Vec {
|
|
|
140
192
|
const perPage = 100;
|
|
141
193
|
const sinceTimestamp = new Date(sinceDate);
|
|
142
194
|
while (true) {
|
|
195
|
+
// Log progress every 10 pages to reduce noise
|
|
196
|
+
if (page === 1 || page % 10 === 0) {
|
|
197
|
+
logger.debug(`Fetching issues page ${page}... (${issues.length} issues so far)`);
|
|
198
|
+
}
|
|
143
199
|
const data = await fetchWithRetry(GITHUB_API_URL, {
|
|
144
200
|
per_page: perPage,
|
|
145
201
|
page,
|
|
@@ -493,6 +549,344 @@ class Doc2Vec {
|
|
|
493
549
|
}
|
|
494
550
|
logger.info(`Finished processing local directory: ${config.path}`);
|
|
495
551
|
}
|
|
552
|
+
async processZendesk(config, parentLogger) {
|
|
553
|
+
const logger = parentLogger.child('process');
|
|
554
|
+
logger.info(`Starting processing for Zendesk: ${config.zendesk_subdomain}.zendesk.com`);
|
|
555
|
+
const dbConnection = await database_1.DatabaseManager.initDatabase(config, logger);
|
|
556
|
+
// Initialize metadata storage
|
|
557
|
+
await database_1.DatabaseManager.initDatabaseMetadata(dbConnection, logger);
|
|
558
|
+
const fetchTickets = config.fetch_tickets !== false; // default true
|
|
559
|
+
const fetchArticles = config.fetch_articles !== false; // default true
|
|
560
|
+
if (fetchTickets) {
|
|
561
|
+
logger.section('ZENDESK TICKETS');
|
|
562
|
+
await this.fetchAndProcessZendeskTickets(config, dbConnection, logger);
|
|
563
|
+
}
|
|
564
|
+
if (fetchArticles) {
|
|
565
|
+
logger.section('ZENDESK ARTICLES');
|
|
566
|
+
await this.fetchAndProcessZendeskArticles(config, dbConnection, logger);
|
|
567
|
+
}
|
|
568
|
+
logger.info(`Finished processing Zendesk: ${config.zendesk_subdomain}.zendesk.com`);
|
|
569
|
+
}
|
|
570
|
+
async fetchAndProcessZendeskTickets(config, dbConnection, logger) {
|
|
571
|
+
const baseUrl = `https://${config.zendesk_subdomain}.zendesk.com/api/v2`;
|
|
572
|
+
const auth = buffer_1.Buffer.from(`${config.email}/token:${config.api_token}`).toString('base64');
|
|
573
|
+
// Get the last run date from the database
|
|
574
|
+
const startDate = config.start_date || `${new Date().getFullYear()}-01-01`;
|
|
575
|
+
const lastRunDate = await database_1.DatabaseManager.getLastRunDate(dbConnection, `zendesk_tickets_${config.zendesk_subdomain}`, `${startDate}T00:00:00Z`, logger);
|
|
576
|
+
const fetchWithRetry = async (url, retries = 3) => {
|
|
577
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
578
|
+
try {
|
|
579
|
+
const response = await axios_1.default.get(url, {
|
|
580
|
+
headers: {
|
|
581
|
+
'Authorization': `Basic ${auth}`,
|
|
582
|
+
'Content-Type': 'application/json',
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
if (response.status === 429) {
|
|
586
|
+
const retryAfter = parseInt(response.headers['retry-after'] || '60');
|
|
587
|
+
logger.warn(`Rate limited, waiting ${retryAfter}s before retry`);
|
|
588
|
+
await new Promise(res => setTimeout(res, retryAfter * 1000));
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
return response.data;
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
logger.error(`Zendesk API error (attempt ${attempt + 1}):`, error.message);
|
|
595
|
+
if (attempt === retries - 1)
|
|
596
|
+
throw error;
|
|
597
|
+
await new Promise(res => setTimeout(res, 2000 * (attempt + 1)));
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
const generateMarkdownForTicket = (ticket, comments) => {
|
|
602
|
+
let md = `# Ticket #${ticket.id}: ${ticket.subject}\n\n`;
|
|
603
|
+
md += `- **Status:** ${ticket.status}\n`;
|
|
604
|
+
md += `- **Priority:** ${ticket.priority || 'None'}\n`;
|
|
605
|
+
md += `- **Type:** ${ticket.type || 'None'}\n`;
|
|
606
|
+
md += `- **Requester:** ${ticket.requester_id}\n`;
|
|
607
|
+
md += `- **Assignee:** ${ticket.assignee_id || 'Unassigned'}\n`;
|
|
608
|
+
md += `- **Created:** ${new Date(ticket.created_at).toDateString()}\n`;
|
|
609
|
+
md += `- **Updated:** ${new Date(ticket.updated_at).toDateString()}\n`;
|
|
610
|
+
if (ticket.tags && ticket.tags.length > 0) {
|
|
611
|
+
md += `- **Tags:** ${ticket.tags.map((tag) => `\`${tag}\``).join(', ')}\n`;
|
|
612
|
+
}
|
|
613
|
+
// Handle ticket description
|
|
614
|
+
const description = ticket.description || '';
|
|
615
|
+
const cleanDescription = description || '_No description._';
|
|
616
|
+
md += `\n## Description\n\n${cleanDescription}\n\n`;
|
|
617
|
+
if (comments && comments.length > 0) {
|
|
618
|
+
md += `## Comments\n\n`;
|
|
619
|
+
for (const comment of comments) {
|
|
620
|
+
if (comment.public) {
|
|
621
|
+
md += `### ${comment.author_id} - ${new Date(comment.created_at).toDateString()}\n\n`;
|
|
622
|
+
// Handle comment body
|
|
623
|
+
const rawBody = comment.plain_body || comment.html_body || comment.body || '';
|
|
624
|
+
const commentBody = rawBody.replace(/ /g, " ") || '_No content._';
|
|
625
|
+
md += `${commentBody}\n\n---\n\n`;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
md += `## Comments\n\n_No comments._\n`;
|
|
631
|
+
}
|
|
632
|
+
return md;
|
|
633
|
+
};
|
|
634
|
+
const processTicket = async (ticket) => {
|
|
635
|
+
const ticketId = ticket.id;
|
|
636
|
+
const url = `https://${config.zendesk_subdomain}.zendesk.com/agent/tickets/${ticketId}`;
|
|
637
|
+
logger.info(`Processing ticket #${ticketId}`);
|
|
638
|
+
// Fetch ticket comments
|
|
639
|
+
const commentsUrl = `${baseUrl}/tickets/${ticketId}/comments.json`;
|
|
640
|
+
const commentsData = await fetchWithRetry(commentsUrl);
|
|
641
|
+
const comments = commentsData?.comments || [];
|
|
642
|
+
// Generate markdown for the ticket
|
|
643
|
+
const markdown = generateMarkdownForTicket(ticket, comments);
|
|
644
|
+
// Chunk the markdown content
|
|
645
|
+
const ticketConfig = {
|
|
646
|
+
...config,
|
|
647
|
+
product_name: config.product_name || `zendesk_${config.zendesk_subdomain}`,
|
|
648
|
+
max_size: config.max_size || Infinity
|
|
649
|
+
};
|
|
650
|
+
const chunks = await this.contentProcessor.chunkMarkdown(markdown, ticketConfig, url);
|
|
651
|
+
logger.info(`Ticket #${ticketId}: Created ${chunks.length} chunks`);
|
|
652
|
+
// Process and store each chunk
|
|
653
|
+
for (const chunk of chunks) {
|
|
654
|
+
const chunkHash = utils_1.Utils.generateHash(chunk.content);
|
|
655
|
+
const chunkId = chunk.metadata.chunk_id.substring(0, 8) + '...';
|
|
656
|
+
if (dbConnection.type === 'sqlite') {
|
|
657
|
+
const { checkHashStmt } = database_1.DatabaseManager.prepareSQLiteStatements(dbConnection.db);
|
|
658
|
+
const existing = checkHashStmt.get(chunk.metadata.chunk_id);
|
|
659
|
+
if (existing && existing.hash === chunkHash) {
|
|
660
|
+
logger.info(`Skipping unchanged chunk: ${chunkId}`);
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
const embeddings = await this.createEmbeddings([chunk.content]);
|
|
664
|
+
if (embeddings.length) {
|
|
665
|
+
database_1.DatabaseManager.insertVectorsSQLite(dbConnection.db, chunk, embeddings[0], logger, chunkHash);
|
|
666
|
+
logger.debug(`Stored chunk ${chunkId} in SQLite`);
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
logger.error(`Embedding failed for chunk: ${chunkId}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
else if (dbConnection.type === 'qdrant') {
|
|
673
|
+
try {
|
|
674
|
+
let pointId;
|
|
675
|
+
try {
|
|
676
|
+
pointId = chunk.metadata.chunk_id;
|
|
677
|
+
if (!utils_1.Utils.isValidUuid(pointId)) {
|
|
678
|
+
pointId = utils_1.Utils.hashToUuid(chunk.metadata.chunk_id);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
catch (e) {
|
|
682
|
+
pointId = crypto_1.default.randomUUID();
|
|
683
|
+
}
|
|
684
|
+
const existingPoints = await dbConnection.client.retrieve(dbConnection.collectionName, {
|
|
685
|
+
ids: [pointId],
|
|
686
|
+
with_payload: true,
|
|
687
|
+
with_vector: false,
|
|
688
|
+
});
|
|
689
|
+
if (existingPoints.length > 0 && existingPoints[0].payload && existingPoints[0].payload.hash === chunkHash) {
|
|
690
|
+
logger.info(`Skipping unchanged chunk: ${chunkId}`);
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
const embeddings = await this.createEmbeddings([chunk.content]);
|
|
694
|
+
if (embeddings.length) {
|
|
695
|
+
await database_1.DatabaseManager.storeChunkInQdrant(dbConnection, chunk, embeddings[0], chunkHash);
|
|
696
|
+
logger.debug(`Stored chunk ${chunkId} in Qdrant (${dbConnection.collectionName})`);
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
logger.error(`Embedding failed for chunk: ${chunkId}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
logger.error(`Error processing chunk in Qdrant:`, error);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
logger.info(`Fetching Zendesk tickets updated since ${lastRunDate}`);
|
|
709
|
+
// Build query parameters
|
|
710
|
+
const statusFilter = config.ticket_status || ['new', 'open', 'pending', 'hold', 'solved'];
|
|
711
|
+
const query = `updated>${lastRunDate.split('T')[0]} status:${statusFilter.join(',status:')}`;
|
|
712
|
+
let nextPage = `${baseUrl}/search.json?query=${encodeURIComponent(query)}&sort_by=updated_at&sort_order=asc`;
|
|
713
|
+
let totalTickets = 0;
|
|
714
|
+
while (nextPage) {
|
|
715
|
+
const data = await fetchWithRetry(nextPage);
|
|
716
|
+
const tickets = data.results || [];
|
|
717
|
+
logger.info(`Processing batch of ${tickets.length} tickets`);
|
|
718
|
+
for (const ticket of tickets) {
|
|
719
|
+
await processTicket(ticket);
|
|
720
|
+
totalTickets++;
|
|
721
|
+
}
|
|
722
|
+
nextPage = data.next_page;
|
|
723
|
+
if (nextPage) {
|
|
724
|
+
logger.debug(`Fetching next page: ${nextPage}`);
|
|
725
|
+
// Rate limiting: wait between requests
|
|
726
|
+
await new Promise(res => setTimeout(res, 1000));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// Update the last run date in the database
|
|
730
|
+
await database_1.DatabaseManager.updateLastRunDate(dbConnection, `zendesk_tickets_${config.zendesk_subdomain}`, logger);
|
|
731
|
+
logger.info(`Successfully processed ${totalTickets} tickets`);
|
|
732
|
+
}
|
|
733
|
+
async fetchAndProcessZendeskArticles(config, dbConnection, logger) {
|
|
734
|
+
const baseUrl = `https://${config.zendesk_subdomain}.zendesk.com/api/v2/help_center`;
|
|
735
|
+
const auth = buffer_1.Buffer.from(`${config.email}/token:${config.api_token}`).toString('base64');
|
|
736
|
+
// Get the start date for filtering
|
|
737
|
+
const startDate = config.start_date || `${new Date().getFullYear()}-01-01`;
|
|
738
|
+
const startDateObj = new Date(startDate);
|
|
739
|
+
const fetchWithRetry = async (url, retries = 3) => {
|
|
740
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
741
|
+
try {
|
|
742
|
+
const response = await axios_1.default.get(url, {
|
|
743
|
+
headers: {
|
|
744
|
+
'Authorization': `Basic ${auth}`,
|
|
745
|
+
'Content-Type': 'application/json',
|
|
746
|
+
},
|
|
747
|
+
});
|
|
748
|
+
if (response.status === 429) {
|
|
749
|
+
const retryAfter = parseInt(response.headers['retry-after'] || '60');
|
|
750
|
+
logger.warn(`Rate limited, waiting ${retryAfter}s before retry`);
|
|
751
|
+
await new Promise(res => setTimeout(res, retryAfter * 1000));
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
return response.data;
|
|
755
|
+
}
|
|
756
|
+
catch (error) {
|
|
757
|
+
logger.error(`Zendesk API error (attempt ${attempt + 1}):`, error.message);
|
|
758
|
+
if (attempt === retries - 1)
|
|
759
|
+
throw error;
|
|
760
|
+
await new Promise(res => setTimeout(res, 2000 * (attempt + 1)));
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
const generateMarkdownForArticle = (article) => {
|
|
765
|
+
let md = `# ${article.title}\n\n`;
|
|
766
|
+
md += `- **Author:** ${article.author_id}\n`;
|
|
767
|
+
md += `- **Section:** ${article.section_id}\n`;
|
|
768
|
+
md += `- **Created:** ${new Date(article.created_at).toDateString()}\n`;
|
|
769
|
+
md += `- **Updated:** ${new Date(article.updated_at).toDateString()}\n`;
|
|
770
|
+
md += `- **Vote Sum:** ${article.vote_sum || 0}\n`;
|
|
771
|
+
md += `- **Vote Count:** ${article.vote_count || 0}\n`;
|
|
772
|
+
if (article.label_names && article.label_names.length > 0) {
|
|
773
|
+
md += `- **Labels:** ${article.label_names.map((label) => `\`${label}\``).join(', ')}\n`;
|
|
774
|
+
}
|
|
775
|
+
// Handle article content - convert HTML to markdown
|
|
776
|
+
const articleBody = article.body || '';
|
|
777
|
+
let cleanContent = '_No content._';
|
|
778
|
+
if (articleBody.trim()) {
|
|
779
|
+
if (articleBody.includes('<')) {
|
|
780
|
+
// HTML content - use ContentProcessor to convert to markdown
|
|
781
|
+
cleanContent = this.contentProcessor.convertHtmlToMarkdown(articleBody);
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
// Plain text content
|
|
785
|
+
cleanContent = articleBody;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
md += `\n## Content\n\n${cleanContent}\n`;
|
|
789
|
+
return md;
|
|
790
|
+
};
|
|
791
|
+
const processArticle = async (article) => {
|
|
792
|
+
const articleId = article.id;
|
|
793
|
+
const url = article.html_url || `https://${config.zendesk_subdomain}.zendesk.com/hc/articles/${articleId}`;
|
|
794
|
+
logger.info(`Processing article #${articleId}: ${article.title}`);
|
|
795
|
+
// Generate markdown for the article
|
|
796
|
+
const markdown = generateMarkdownForArticle(article);
|
|
797
|
+
// Chunk the markdown content
|
|
798
|
+
const articleConfig = {
|
|
799
|
+
...config,
|
|
800
|
+
product_name: config.product_name || `zendesk_${config.zendesk_subdomain}`,
|
|
801
|
+
max_size: config.max_size || Infinity
|
|
802
|
+
};
|
|
803
|
+
const chunks = await this.contentProcessor.chunkMarkdown(markdown, articleConfig, url);
|
|
804
|
+
logger.info(`Article #${articleId}: Created ${chunks.length} chunks`);
|
|
805
|
+
// Process and store each chunk (similar to ticket processing)
|
|
806
|
+
for (const chunk of chunks) {
|
|
807
|
+
const chunkHash = utils_1.Utils.generateHash(chunk.content);
|
|
808
|
+
const chunkId = chunk.metadata.chunk_id.substring(0, 8) + '...';
|
|
809
|
+
if (dbConnection.type === 'sqlite') {
|
|
810
|
+
const { checkHashStmt } = database_1.DatabaseManager.prepareSQLiteStatements(dbConnection.db);
|
|
811
|
+
const existing = checkHashStmt.get(chunk.metadata.chunk_id);
|
|
812
|
+
if (existing && existing.hash === chunkHash) {
|
|
813
|
+
logger.info(`Skipping unchanged chunk: ${chunkId}`);
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
const embeddings = await this.createEmbeddings([chunk.content]);
|
|
817
|
+
if (embeddings.length) {
|
|
818
|
+
database_1.DatabaseManager.insertVectorsSQLite(dbConnection.db, chunk, embeddings[0], logger, chunkHash);
|
|
819
|
+
logger.debug(`Stored chunk ${chunkId} in SQLite`);
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
logger.error(`Embedding failed for chunk: ${chunkId}`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
else if (dbConnection.type === 'qdrant') {
|
|
826
|
+
try {
|
|
827
|
+
let pointId;
|
|
828
|
+
try {
|
|
829
|
+
pointId = chunk.metadata.chunk_id;
|
|
830
|
+
if (!utils_1.Utils.isValidUuid(pointId)) {
|
|
831
|
+
pointId = utils_1.Utils.hashToUuid(chunk.metadata.chunk_id);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
catch (e) {
|
|
835
|
+
pointId = crypto_1.default.randomUUID();
|
|
836
|
+
}
|
|
837
|
+
const existingPoints = await dbConnection.client.retrieve(dbConnection.collectionName, {
|
|
838
|
+
ids: [pointId],
|
|
839
|
+
with_payload: true,
|
|
840
|
+
with_vector: false,
|
|
841
|
+
});
|
|
842
|
+
if (existingPoints.length > 0 && existingPoints[0].payload && existingPoints[0].payload.hash === chunkHash) {
|
|
843
|
+
logger.info(`Skipping unchanged chunk: ${chunkId}`);
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const embeddings = await this.createEmbeddings([chunk.content]);
|
|
847
|
+
if (embeddings.length) {
|
|
848
|
+
await database_1.DatabaseManager.storeChunkInQdrant(dbConnection, chunk, embeddings[0], chunkHash);
|
|
849
|
+
logger.debug(`Stored chunk ${chunkId} in Qdrant (${dbConnection.collectionName})`);
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
logger.error(`Embedding failed for chunk: ${chunkId}`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
catch (error) {
|
|
856
|
+
logger.error(`Error processing chunk in Qdrant:`, error);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
logger.info(`Fetching Zendesk help center articles updated since ${startDate}`);
|
|
862
|
+
let nextPage = `${baseUrl}/articles.json`;
|
|
863
|
+
let totalArticles = 0;
|
|
864
|
+
let processedArticles = 0;
|
|
865
|
+
while (nextPage) {
|
|
866
|
+
const data = await fetchWithRetry(nextPage);
|
|
867
|
+
const articles = data.articles || [];
|
|
868
|
+
logger.info(`Processing batch of ${articles.length} articles`);
|
|
869
|
+
for (const article of articles) {
|
|
870
|
+
totalArticles++;
|
|
871
|
+
// Check if article was updated since the start date
|
|
872
|
+
const updatedAt = new Date(article.updated_at);
|
|
873
|
+
if (updatedAt >= startDateObj) {
|
|
874
|
+
await processArticle(article);
|
|
875
|
+
processedArticles++;
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
logger.debug(`Skipping article #${article.id} (updated ${article.updated_at}, before ${startDate})`);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
nextPage = data.next_page;
|
|
882
|
+
if (nextPage) {
|
|
883
|
+
logger.debug(`Fetching next page: ${nextPage}`);
|
|
884
|
+
// Rate limiting: wait between requests
|
|
885
|
+
await new Promise(res => setTimeout(res, 1000));
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
logger.info(`Successfully processed ${processedArticles} of ${totalArticles} articles (filtered by date >= ${startDate})`);
|
|
889
|
+
}
|
|
496
890
|
async createEmbeddings(texts) {
|
|
497
891
|
const logger = this.logger.child('embeddings');
|
|
498
892
|
try {
|
|
@@ -0,0 +1,218 @@
|
|
|
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.DatabaseManager = void 0;
|
|
40
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
41
|
+
const sqliteVec = __importStar(require("sqlite-vec"));
|
|
42
|
+
class DatabaseManager {
|
|
43
|
+
constructor(dbPath) {
|
|
44
|
+
// Cache counts for immediate UI feedback
|
|
45
|
+
this._trackedFilesCount = 0;
|
|
46
|
+
this._totalChunksCount = 0;
|
|
47
|
+
this.db = new better_sqlite3_1.default(dbPath);
|
|
48
|
+
sqliteVec.load(this.db);
|
|
49
|
+
this.createTables();
|
|
50
|
+
// Prepare statements for insert/update (matching doc2vec's approach)
|
|
51
|
+
this.insertStmt = this.db.prepare(`
|
|
52
|
+
INSERT INTO vec_items (embedding, version, heading_hierarchy, section, chunk_id, content, url, hash, chunk_index, total_chunks)
|
|
53
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
54
|
+
`);
|
|
55
|
+
this.updateStmt = this.db.prepare(`
|
|
56
|
+
UPDATE vec_items SET embedding = ?, version = ?, heading_hierarchy = ?, section = ?, content = ?, url = ?, hash = ?, chunk_index = ?, total_chunks = ?
|
|
57
|
+
WHERE chunk_id = ?
|
|
58
|
+
`);
|
|
59
|
+
// Initialize counts from database
|
|
60
|
+
this._trackedFilesCount = this._queryTrackedFilesCount();
|
|
61
|
+
this._totalChunksCount = this._queryTotalChunksCount();
|
|
62
|
+
console.log(`Database initialized at: ${dbPath}`);
|
|
63
|
+
}
|
|
64
|
+
_queryTrackedFilesCount() {
|
|
65
|
+
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM files');
|
|
66
|
+
const row = stmt.get();
|
|
67
|
+
return Number(row.count);
|
|
68
|
+
}
|
|
69
|
+
_queryTotalChunksCount() {
|
|
70
|
+
const stmt = this.db.prepare('SELECT COUNT(*) as count FROM vec_items');
|
|
71
|
+
const row = stmt.get();
|
|
72
|
+
return Number(row.count);
|
|
73
|
+
}
|
|
74
|
+
createTables() {
|
|
75
|
+
// Create vec0 virtual table (sqlite-vec)
|
|
76
|
+
this.db.exec(`
|
|
77
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_items USING vec0(
|
|
78
|
+
embedding FLOAT[3072],
|
|
79
|
+
version TEXT,
|
|
80
|
+
heading_hierarchy TEXT,
|
|
81
|
+
section TEXT,
|
|
82
|
+
chunk_id TEXT UNIQUE,
|
|
83
|
+
content TEXT,
|
|
84
|
+
url TEXT,
|
|
85
|
+
hash TEXT,
|
|
86
|
+
chunk_index INTEGER,
|
|
87
|
+
total_chunks INTEGER
|
|
88
|
+
)
|
|
89
|
+
`);
|
|
90
|
+
// Create files tracking table
|
|
91
|
+
this.db.exec(`
|
|
92
|
+
CREATE TABLE IF NOT EXISTS files (
|
|
93
|
+
path TEXT PRIMARY KEY,
|
|
94
|
+
hash TEXT,
|
|
95
|
+
modified_at TEXT,
|
|
96
|
+
chunk_count INTEGER
|
|
97
|
+
)
|
|
98
|
+
`);
|
|
99
|
+
console.log('Database tables created');
|
|
100
|
+
}
|
|
101
|
+
insertChunk(chunk, embedding) {
|
|
102
|
+
const embeddingData = embedding ? new Float32Array(embedding) : new Float32Array(3072).fill(0);
|
|
103
|
+
// Use JSON.stringify for heading_hierarchy to match doc2vec format
|
|
104
|
+
const headingHierarchyJson = JSON.stringify(chunk.headingHierarchy);
|
|
105
|
+
try {
|
|
106
|
+
this.insertStmt.run(embeddingData, chunk.version, headingHierarchyJson, chunk.section, chunk.chunkId, chunk.content, chunk.url, chunk.hash, BigInt(chunk.chunkIndex), BigInt(chunk.totalChunks));
|
|
107
|
+
this._totalChunksCount++;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
// If insert fails due to UNIQUE constraint, update instead
|
|
111
|
+
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE' || error.message?.includes('UNIQUE constraint failed')) {
|
|
112
|
+
this.updateStmt.run(embeddingData, chunk.version, headingHierarchyJson, chunk.section, chunk.content, chunk.url, chunk.hash, BigInt(chunk.chunkIndex), BigInt(chunk.totalChunks), chunk.chunkId);
|
|
113
|
+
// Update doesn't change count
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
removeChunksForFile(filePath) {
|
|
121
|
+
const url = `file://${filePath}`;
|
|
122
|
+
// Get count before delete
|
|
123
|
+
const countStmt = this.db.prepare('SELECT COUNT(*) as count FROM vec_items WHERE url = ?');
|
|
124
|
+
const countRow = countStmt.get(url);
|
|
125
|
+
const deletedCount = Number(countRow.count);
|
|
126
|
+
const stmt = this.db.prepare('DELETE FROM vec_items WHERE url = ?');
|
|
127
|
+
stmt.run(url);
|
|
128
|
+
this._totalChunksCount -= deletedCount;
|
|
129
|
+
}
|
|
130
|
+
getExistingChunkHash(chunkId) {
|
|
131
|
+
const stmt = this.db.prepare('SELECT hash FROM vec_items WHERE chunk_id = ?');
|
|
132
|
+
const row = stmt.get(chunkId);
|
|
133
|
+
return row?.hash ?? null;
|
|
134
|
+
}
|
|
135
|
+
getTotalChunksCount() {
|
|
136
|
+
return this._totalChunksCount;
|
|
137
|
+
}
|
|
138
|
+
upsertFileInfo(filePath, hash, modifiedAt, chunkCount) {
|
|
139
|
+
// Check if file already exists
|
|
140
|
+
const checkStmt = this.db.prepare('SELECT 1 FROM files WHERE path = ?');
|
|
141
|
+
const exists = checkStmt.get(filePath);
|
|
142
|
+
const stmt = this.db.prepare(`
|
|
143
|
+
INSERT OR REPLACE INTO files (path, hash, modified_at, chunk_count)
|
|
144
|
+
VALUES (?, ?, ?, ?)
|
|
145
|
+
`);
|
|
146
|
+
stmt.run(filePath, hash, modifiedAt.toISOString(), chunkCount);
|
|
147
|
+
// Only increment if this is a new file
|
|
148
|
+
if (!exists) {
|
|
149
|
+
this._trackedFilesCount++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
getFileInfo(filePath) {
|
|
153
|
+
const stmt = this.db.prepare('SELECT * FROM files WHERE path = ?');
|
|
154
|
+
const row = stmt.get(filePath);
|
|
155
|
+
if (!row)
|
|
156
|
+
return null;
|
|
157
|
+
return {
|
|
158
|
+
path: row.path,
|
|
159
|
+
hash: row.hash,
|
|
160
|
+
modifiedAt: new Date(row.modified_at),
|
|
161
|
+
chunkCount: row.chunk_count
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
removeFileInfo(filePath) {
|
|
165
|
+
// Check if file exists before deleting
|
|
166
|
+
const checkStmt = this.db.prepare('SELECT 1 FROM files WHERE path = ?');
|
|
167
|
+
const exists = checkStmt.get(filePath);
|
|
168
|
+
const stmt = this.db.prepare('DELETE FROM files WHERE path = ?');
|
|
169
|
+
stmt.run(filePath);
|
|
170
|
+
if (exists) {
|
|
171
|
+
this._trackedFilesCount--;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
getAllTrackedFiles() {
|
|
175
|
+
const stmt = this.db.prepare('SELECT path FROM files');
|
|
176
|
+
const rows = stmt.all();
|
|
177
|
+
return rows.map(r => r.path);
|
|
178
|
+
}
|
|
179
|
+
getAllTrackedFilesWithInfo() {
|
|
180
|
+
const stmt = this.db.prepare('SELECT path, chunk_count, modified_at FROM files ORDER BY modified_at DESC');
|
|
181
|
+
const rows = stmt.all();
|
|
182
|
+
return rows.map(r => ({
|
|
183
|
+
path: r.path,
|
|
184
|
+
chunkCount: r.chunk_count,
|
|
185
|
+
modifiedAt: r.modified_at
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
getChunksForFile(filePath) {
|
|
189
|
+
const url = `file://${filePath}`;
|
|
190
|
+
const stmt = this.db.prepare(`
|
|
191
|
+
SELECT chunk_id, content, section, chunk_index
|
|
192
|
+
FROM vec_items
|
|
193
|
+
WHERE url = ?
|
|
194
|
+
ORDER BY chunk_index
|
|
195
|
+
`);
|
|
196
|
+
const rows = stmt.all(url);
|
|
197
|
+
return rows.map(r => ({
|
|
198
|
+
chunkId: r.chunk_id,
|
|
199
|
+
content: r.content,
|
|
200
|
+
section: r.section,
|
|
201
|
+
chunkIndex: Number(r.chunk_index)
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
getTrackedFilesCount() {
|
|
205
|
+
return this._trackedFilesCount;
|
|
206
|
+
}
|
|
207
|
+
clearAllData() {
|
|
208
|
+
this.db.exec('DELETE FROM vec_items');
|
|
209
|
+
this.db.exec('DELETE FROM files');
|
|
210
|
+
this._trackedFilesCount = 0;
|
|
211
|
+
this._totalChunksCount = 0;
|
|
212
|
+
console.log('Cleared all data');
|
|
213
|
+
}
|
|
214
|
+
close() {
|
|
215
|
+
this.db.close();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
exports.DatabaseManager = DatabaseManager;
|