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.
Files changed (2) hide show
  1. package/dist/doc2vec.js +56 -70
  2. 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
- // Process and store each chunk
1250
- for (const chunk of chunks) {
1251
- const chunkHash = utils_1.Utils.generateHash(chunk.content);
1252
- const chunkId = chunk.metadata.chunk_id.substring(0, 8) + '...';
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 statusFilter = config.ticket_status || ['new', 'open', 'pending', 'hold', 'solved'];
1308
- const query = `updated>${lastRunDate.split('T')[0]} status:${statusFilter.join(',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
- await processTicket(ticket);
1317
- totalTickets++;
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
- // Update the last run date in the database
1327
- await database_1.DatabaseManager.updateLastRunDate(dbConnection, `zendesk_tickets_${config.zendesk_subdomain}`, logger, this.embeddingDimension, syncStartDate);
1328
- logger.info(`Successfully processed ${totalTickets} tickets`);
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`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doc2vec",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "type": "commonjs",
5
5
  "description": "",
6
6
  "main": "dist/doc2vec.js",