doc2vec 2.7.0 → 2.8.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/doc2vec.js +56 -70
- package/package.json +1 -1
package/dist/doc2vec.js
CHANGED
|
@@ -1170,6 +1170,9 @@ class Doc2Vec {
|
|
|
1170
1170
|
// Get the last run date from the database
|
|
1171
1171
|
const startDate = config.start_date || `${new Date().getFullYear()}-01-01`;
|
|
1172
1172
|
const lastRunDate = await database_1.DatabaseManager.getLastRunDate(dbConnection, `zendesk_tickets_${config.zendesk_subdomain}`, `${startDate}T00:00:00Z`, logger);
|
|
1173
|
+
// Status filter applied client-side (includes 'closed' by default so tickets
|
|
1174
|
+
// transitioning to closed get updated rather than left stale)
|
|
1175
|
+
const statusFilter = new Set(config.ticket_status || ['new', 'open', 'pending', 'hold', 'solved', 'closed']);
|
|
1173
1176
|
const fetchWithRetry = async (url, retries = 3) => {
|
|
1174
1177
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
1175
1178
|
try {
|
|
@@ -1179,15 +1182,19 @@ class Doc2Vec {
|
|
|
1179
1182
|
'Content-Type': 'application/json',
|
|
1180
1183
|
},
|
|
1181
1184
|
});
|
|
1182
|
-
if (response.status === 429) {
|
|
1183
|
-
const retryAfter = parseInt(response.headers['retry-after'] || '60');
|
|
1184
|
-
logger.warn(`Rate limited, waiting ${retryAfter}s before retry`);
|
|
1185
|
-
await new Promise(res => setTimeout(res, retryAfter * 1000));
|
|
1186
|
-
continue;
|
|
1187
|
-
}
|
|
1188
1185
|
return response.data;
|
|
1189
1186
|
}
|
|
1190
1187
|
catch (error) {
|
|
1188
|
+
// 403 is a permissions error — retrying won't help
|
|
1189
|
+
if (error.response?.status === 403)
|
|
1190
|
+
throw error;
|
|
1191
|
+
if (error.response?.status === 429) {
|
|
1192
|
+
const retryAfter = parseInt(error.response.headers?.['retry-after'] || '60', 10);
|
|
1193
|
+
logger.warn(`Rate limited by Zendesk, waiting ${retryAfter}s before retry`);
|
|
1194
|
+
await new Promise(res => setTimeout(res, retryAfter * 1000));
|
|
1195
|
+
attempt--; // Don't burn a retry on rate-limit waits
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1191
1198
|
logger.error(`Zendesk API error (attempt ${attempt + 1}):`, error.message);
|
|
1192
1199
|
if (attempt === retries - 1)
|
|
1193
1200
|
throw error;
|
|
@@ -1231,6 +1238,22 @@ class Doc2Vec {
|
|
|
1231
1238
|
const processTicket = async (ticket) => {
|
|
1232
1239
|
const ticketId = ticket.id;
|
|
1233
1240
|
const url = `https://${config.zendesk_subdomain}.zendesk.com/agent/tickets/${ticketId}`;
|
|
1241
|
+
// Deleted tickets — remove their chunks and stop
|
|
1242
|
+
if (ticket.status === 'deleted') {
|
|
1243
|
+
logger.info(`Ticket #${ticketId} was deleted in Zendesk — removing its chunks`);
|
|
1244
|
+
if (dbConnection.type === 'sqlite') {
|
|
1245
|
+
database_1.DatabaseManager.removeChunksByUrlSQLite(dbConnection.db, url, logger);
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
await database_1.DatabaseManager.removeChunksByUrlQdrant(dbConnection, url, logger);
|
|
1249
|
+
}
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
// Skip tickets whose status is outside the configured filter
|
|
1253
|
+
if (!statusFilter.has(ticket.status)) {
|
|
1254
|
+
logger.debug(`Ticket #${ticketId} has status '${ticket.status}' outside configured filter — skipping`);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1234
1257
|
logger.info(`Processing ticket #${ticketId}`);
|
|
1235
1258
|
// Fetch ticket comments
|
|
1236
1259
|
const commentsUrl = `${baseUrl}/tickets/${ticketId}/comments.json`;
|
|
@@ -1246,86 +1269,49 @@ class Doc2Vec {
|
|
|
1246
1269
|
};
|
|
1247
1270
|
const chunks = await this.contentProcessor.chunkMarkdown(markdown, ticketConfig, url);
|
|
1248
1271
|
logger.info(`Ticket #${ticketId}: Created ${chunks.length} chunks`);
|
|
1249
|
-
//
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
if (dbConnection.type === 'sqlite') {
|
|
1254
|
-
const { checkHashStmt } = database_1.DatabaseManager.prepareSQLiteStatements(dbConnection.db);
|
|
1255
|
-
const existing = checkHashStmt.get(chunk.metadata.chunk_id);
|
|
1256
|
-
if (existing && existing.hash === chunkHash) {
|
|
1257
|
-
logger.info(`Skipping unchanged chunk: ${chunkId}`);
|
|
1258
|
-
continue;
|
|
1259
|
-
}
|
|
1260
|
-
const embeddings = await this.createEmbeddings([chunk.content]);
|
|
1261
|
-
if (embeddings.length) {
|
|
1262
|
-
database_1.DatabaseManager.insertVectorsSQLite(dbConnection.db, chunk, embeddings[0], logger, chunkHash);
|
|
1263
|
-
logger.debug(`Stored chunk ${chunkId} in SQLite`);
|
|
1264
|
-
}
|
|
1265
|
-
else {
|
|
1266
|
-
logger.error(`Embedding failed for chunk: ${chunkId}`);
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
else if (dbConnection.type === 'qdrant') {
|
|
1270
|
-
try {
|
|
1271
|
-
let pointId;
|
|
1272
|
-
try {
|
|
1273
|
-
pointId = chunk.metadata.chunk_id;
|
|
1274
|
-
if (!utils_1.Utils.isValidUuid(pointId)) {
|
|
1275
|
-
pointId = utils_1.Utils.hashToUuid(chunk.metadata.chunk_id);
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
catch (e) {
|
|
1279
|
-
pointId = crypto_1.default.randomUUID();
|
|
1280
|
-
}
|
|
1281
|
-
const existingPoints = await dbConnection.client.retrieve(dbConnection.collectionName, {
|
|
1282
|
-
ids: [pointId],
|
|
1283
|
-
with_payload: true,
|
|
1284
|
-
with_vector: false,
|
|
1285
|
-
});
|
|
1286
|
-
if (existingPoints.length > 0 && existingPoints[0].payload && existingPoints[0].payload.hash === chunkHash) {
|
|
1287
|
-
logger.info(`Skipping unchanged chunk: ${chunkId}`);
|
|
1288
|
-
continue;
|
|
1289
|
-
}
|
|
1290
|
-
const embeddings = await this.createEmbeddings([chunk.content]);
|
|
1291
|
-
if (embeddings.length) {
|
|
1292
|
-
await database_1.DatabaseManager.storeChunkInQdrant(dbConnection, chunk, embeddings[0], chunkHash);
|
|
1293
|
-
logger.debug(`Stored chunk ${chunkId} in Qdrant (${dbConnection.collectionName})`);
|
|
1294
|
-
}
|
|
1295
|
-
else {
|
|
1296
|
-
logger.error(`Embedding failed for chunk: ${chunkId}`);
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
catch (error) {
|
|
1300
|
-
logger.error(`Error processing chunk in Qdrant:`, error);
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1272
|
+
// Use processChunksForUrl which performs a URL-level diff:
|
|
1273
|
+
// deletes all existing chunks for this URL before reinserting,
|
|
1274
|
+
// so stale chunks from previous versions are never left behind.
|
|
1275
|
+
await this.processChunksForUrl(chunks, url, dbConnection, logger);
|
|
1304
1276
|
};
|
|
1305
1277
|
logger.info(`Fetching Zendesk tickets updated since ${lastRunDate}`);
|
|
1306
|
-
// Build query parameters
|
|
1307
|
-
const
|
|
1308
|
-
const query = `updated>${lastRunDate.split('T')[0]} status:${
|
|
1278
|
+
// Build query parameters — use the status filter for the search query
|
|
1279
|
+
const statusList = Array.from(statusFilter);
|
|
1280
|
+
const query = `updated>${lastRunDate.split('T')[0]} status:${statusList.join(',status:')}`;
|
|
1309
1281
|
let nextPage = `${baseUrl}/search.json?query=${encodeURIComponent(query)}&sort_by=updated_at&sort_order=asc`;
|
|
1310
1282
|
let totalTickets = 0;
|
|
1283
|
+
let failedTickets = 0;
|
|
1311
1284
|
while (nextPage) {
|
|
1312
1285
|
const data = await fetchWithRetry(nextPage);
|
|
1313
1286
|
const tickets = data.results || [];
|
|
1314
1287
|
logger.info(`Processing batch of ${tickets.length} tickets`);
|
|
1315
1288
|
for (const ticket of tickets) {
|
|
1316
|
-
|
|
1317
|
-
|
|
1289
|
+
try {
|
|
1290
|
+
await processTicket(ticket);
|
|
1291
|
+
totalTickets++;
|
|
1292
|
+
}
|
|
1293
|
+
catch (error) {
|
|
1294
|
+
failedTickets++;
|
|
1295
|
+
logger.error(`Failed to process ticket #${ticket.id}, will retry next run: ${error.message}`);
|
|
1296
|
+
}
|
|
1318
1297
|
}
|
|
1319
|
-
nextPage = data.next_page;
|
|
1298
|
+
nextPage = data.next_page || null;
|
|
1320
1299
|
if (nextPage) {
|
|
1321
1300
|
logger.debug(`Fetching next page: ${nextPage}`);
|
|
1322
1301
|
// Rate limiting: wait between requests
|
|
1323
1302
|
await new Promise(res => setTimeout(res, 1000));
|
|
1324
1303
|
}
|
|
1325
1304
|
}
|
|
1326
|
-
//
|
|
1327
|
-
|
|
1328
|
-
|
|
1305
|
+
// Only advance the watermark when all tickets succeeded
|
|
1306
|
+
if (failedTickets === 0) {
|
|
1307
|
+
await database_1.DatabaseManager.updateLastRunDate(dbConnection, `zendesk_tickets_${config.zendesk_subdomain}`, logger, this.embeddingDimension, syncStartDate);
|
|
1308
|
+
logger.info(`Successfully processed ${totalTickets} tickets`);
|
|
1309
|
+
}
|
|
1310
|
+
else {
|
|
1311
|
+
logger.warn(`Run completed with ${failedTickets} ticket failure(s). ` +
|
|
1312
|
+
`Watermark NOT advanced — failed tickets will be retried next run. ` +
|
|
1313
|
+
`Successfully processed: ${totalTickets}.`);
|
|
1314
|
+
}
|
|
1329
1315
|
}
|
|
1330
1316
|
async fetchAndProcessZendeskArticles(config, dbConnection, logger) {
|
|
1331
1317
|
const baseUrl = `https://${config.zendesk_subdomain}.zendesk.com/api/v2/help_center`;
|