doc2vec 2.9.2 → 2.10.2
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 +3 -0
- package/dist/doc2vec.js +85 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -218,6 +218,8 @@ Configuration is managed through two files:
|
|
|
218
218
|
* `start_date`: (Optional) Only process tickets/articles updated since this date (e.g., `'2025-01-01'`).
|
|
219
219
|
* `ticket_status`: (Optional) Filter tickets by status (defaults to `['new', 'open', 'pending', 'hold', 'solved']`).
|
|
220
220
|
* `ticket_priority`: (Optional) Filter tickets by priority (defaults to all priorities).
|
|
221
|
+
* `excluded_organizations`: (Optional) An array of Zendesk organization names whose tickets should be skipped. The sync will abort if any name cannot be resolved.
|
|
222
|
+
* `include_internal_comments`: (Optional) Include non-public (internal/agent-only) comments in the indexed content (defaults to `false`, i.e. only public comments are indexed). Internal comments are labeled `(internal)` in the generated markdown.
|
|
221
223
|
|
|
222
224
|
For S3 buckets (`type: 's3'`):
|
|
223
225
|
* `bucket`: The S3 bucket name.
|
|
@@ -359,6 +361,7 @@ Configuration is managed through two files:
|
|
|
359
361
|
start_date: '2025-01-01'
|
|
360
362
|
ticket_status: ['open', 'pending']
|
|
361
363
|
ticket_priority: ['high']
|
|
364
|
+
excluded_organizations: ['Acme Corp', 'Internal Testing']
|
|
362
365
|
max_size: 1048576
|
|
363
366
|
database_config:
|
|
364
367
|
type: 'sqlite'
|
package/dist/doc2vec.js
CHANGED
|
@@ -1195,6 +1195,8 @@ class Doc2Vec {
|
|
|
1195
1195
|
// Status filter applied client-side (includes 'closed' by default so tickets
|
|
1196
1196
|
// transitioning to closed get updated rather than left stale)
|
|
1197
1197
|
const statusFilter = new Set(config.ticket_status || ['new', 'open', 'pending', 'hold', 'solved', 'closed']);
|
|
1198
|
+
const excludedOrgNames = new Set((config.excluded_organizations || []).map(n => n.toLowerCase()));
|
|
1199
|
+
const excludedOrgIds = new Set();
|
|
1198
1200
|
const fetchWithRetry = async (url, retries = 3) => {
|
|
1199
1201
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
1200
1202
|
try {
|
|
@@ -1243,13 +1245,16 @@ class Doc2Vec {
|
|
|
1243
1245
|
if (comments && comments.length > 0) {
|
|
1244
1246
|
md += `## Comments\n\n`;
|
|
1245
1247
|
for (const comment of comments) {
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
const rawBody = comment.plain_body || comment.html_body || comment.body || '';
|
|
1250
|
-
const commentBody = rawBody.replace(/ /g, " ") || '_No content._';
|
|
1251
|
-
md += `${commentBody}\n\n---\n\n`;
|
|
1248
|
+
// Skip non-public comments unless internal comments are explicitly enabled
|
|
1249
|
+
if (!comment.public && !config.include_internal_comments) {
|
|
1250
|
+
continue;
|
|
1252
1251
|
}
|
|
1252
|
+
const visibility = comment.public ? '' : ' (internal)';
|
|
1253
|
+
md += `### ${comment.author_id} - ${new Date(comment.created_at).toDateString()}${visibility}\n\n`;
|
|
1254
|
+
// Handle comment body
|
|
1255
|
+
const rawBody = comment.plain_body || comment.html_body || comment.body || '';
|
|
1256
|
+
const commentBody = rawBody.replace(/ /g, " ") || '_No content._';
|
|
1257
|
+
md += `${commentBody}\n\n---\n\n`;
|
|
1253
1258
|
}
|
|
1254
1259
|
}
|
|
1255
1260
|
else {
|
|
@@ -1271,16 +1276,29 @@ class Doc2Vec {
|
|
|
1271
1276
|
}
|
|
1272
1277
|
return;
|
|
1273
1278
|
}
|
|
1279
|
+
// Skip tickets belonging to excluded organizations
|
|
1280
|
+
if (ticket.organization_id && excludedOrgIds.has(ticket.organization_id)) {
|
|
1281
|
+
logger.debug(`Ticket #${ticketId} belongs to excluded organization ${ticket.organization_id} — skipping`);
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1274
1284
|
// Skip tickets whose status is outside the configured filter
|
|
1275
1285
|
if (!statusFilter.has(ticket.status)) {
|
|
1276
1286
|
logger.debug(`Ticket #${ticketId} has status '${ticket.status}' outside configured filter — skipping`);
|
|
1277
1287
|
return;
|
|
1278
1288
|
}
|
|
1279
1289
|
logger.info(`Processing ticket #${ticketId}`);
|
|
1280
|
-
// Fetch ticket comments
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
const comments =
|
|
1290
|
+
// Fetch ticket comments. Zendesk returns at most 100 comments per page
|
|
1291
|
+
// (ordered oldest-first), so follow next_page to capture the newest
|
|
1292
|
+
// comments on tickets with more than 100 — otherwise they're silently dropped.
|
|
1293
|
+
const comments = [];
|
|
1294
|
+
let commentsUrl = `${baseUrl}/tickets/${ticketId}/comments.json`;
|
|
1295
|
+
while (commentsUrl) {
|
|
1296
|
+
const commentsData = await fetchWithRetry(commentsUrl);
|
|
1297
|
+
comments.push(...(commentsData?.comments || []));
|
|
1298
|
+
commentsUrl = commentsData?.next_page || null;
|
|
1299
|
+
if (commentsUrl)
|
|
1300
|
+
await new Promise(res => setTimeout(res, 1000));
|
|
1301
|
+
}
|
|
1284
1302
|
// Generate markdown for the ticket
|
|
1285
1303
|
const markdown = generateMarkdownForTicket(ticket, comments);
|
|
1286
1304
|
// Chunk the markdown content
|
|
@@ -1296,18 +1314,37 @@ class Doc2Vec {
|
|
|
1296
1314
|
// so stale chunks from previous versions are never left behind.
|
|
1297
1315
|
await this.processChunksForUrl(chunks, url, dbConnection, logger);
|
|
1298
1316
|
};
|
|
1317
|
+
if (excludedOrgNames.size > 0) {
|
|
1318
|
+
logger.info(`Resolving ${excludedOrgNames.size} excluded organization name(s) to IDs`);
|
|
1319
|
+
const resolvedNames = new Set();
|
|
1320
|
+
let orgsUrl = `${baseUrl}/organizations.json?page[size]=100`;
|
|
1321
|
+
while (orgsUrl) {
|
|
1322
|
+
const orgsData = await fetchWithRetry(orgsUrl);
|
|
1323
|
+
for (const org of orgsData?.organizations || []) {
|
|
1324
|
+
const orgName = (org.name || '').toLowerCase();
|
|
1325
|
+
if (excludedOrgNames.has(orgName)) {
|
|
1326
|
+
excludedOrgIds.add(org.id);
|
|
1327
|
+
resolvedNames.add(orgName);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
orgsUrl = orgsData?.meta?.has_more ? orgsData?.links?.next : null;
|
|
1331
|
+
}
|
|
1332
|
+
logger.info(`Excluding tickets from ${excludedOrgIds.size} organization(s): ${[...excludedOrgIds].join(', ')}`);
|
|
1333
|
+
const unresolved = [...excludedOrgNames].filter(name => !resolvedNames.has(name));
|
|
1334
|
+
if (unresolved.length > 0) {
|
|
1335
|
+
throw new Error(`Cannot resolve excluded organization(s): ${unresolved.join(', ')}. Aborting to avoid syncing data for them.`);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1299
1338
|
logger.info(`Fetching Zendesk tickets updated since ${lastRunDate}`);
|
|
1300
1339
|
// Build query parameters — use the status filter for the search query
|
|
1301
1340
|
const statusList = Array.from(statusFilter);
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1341
|
+
const statusClause = `status:${statusList.join(',status:')}`;
|
|
1342
|
+
// Zendesk /search.json caps results at 1000 (10 pages * 100). Bisect the date range
|
|
1343
|
+
// into smaller windows whenever a window would exceed that cap, so we never hit page 11.
|
|
1304
1344
|
let totalTickets = 0;
|
|
1305
1345
|
let failedTickets = 0;
|
|
1306
|
-
|
|
1307
|
-
const
|
|
1308
|
-
const tickets = data.results || [];
|
|
1309
|
-
logger.info(`Processing batch of ${tickets.length} tickets`);
|
|
1310
|
-
for (const ticket of tickets) {
|
|
1346
|
+
const processResults = async (results) => {
|
|
1347
|
+
for (const ticket of results) {
|
|
1311
1348
|
try {
|
|
1312
1349
|
await processTicket(ticket);
|
|
1313
1350
|
totalTickets++;
|
|
@@ -1317,11 +1354,39 @@ class Doc2Vec {
|
|
|
1317
1354
|
logger.error(`Failed to process ticket #${ticket.id}, will retry next run: ${error.message}`);
|
|
1318
1355
|
}
|
|
1319
1356
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1357
|
+
};
|
|
1358
|
+
// Work queue of [start, end) date windows (ISO strings). Seed with [lastRunDate, syncStartDate).
|
|
1359
|
+
const windows = [[lastRunDate, syncStartDate]];
|
|
1360
|
+
const MAX_PER_WINDOW = 1000;
|
|
1361
|
+
while (windows.length > 0) {
|
|
1362
|
+
const [wStart, wEnd] = windows.shift();
|
|
1363
|
+
const q = `updated>${wStart} updated<${wEnd} ${statusClause}`;
|
|
1364
|
+
const firstUrl = `${baseUrl}/search.json?query=${encodeURIComponent(q)}&sort_by=updated_at&sort_order=asc`;
|
|
1365
|
+
logger.debug(`Searching window ${wStart} .. ${wEnd}`);
|
|
1366
|
+
const firstData = await fetchWithRetry(firstUrl);
|
|
1367
|
+
const count = firstData?.count ?? 0;
|
|
1368
|
+
if (count > MAX_PER_WINDOW) {
|
|
1369
|
+
const startMs = new Date(wStart).getTime();
|
|
1370
|
+
const endMs = new Date(wEnd).getTime();
|
|
1371
|
+
if (endMs - startMs <= 1000) {
|
|
1372
|
+
logger.warn(`Window ${wStart} .. ${wEnd} has ${count} tickets but is already ≤1s wide — processing first 1000 only, some may be missed`);
|
|
1373
|
+
}
|
|
1374
|
+
else {
|
|
1375
|
+
const midIso = new Date(startMs + Math.floor((endMs - startMs) / 2)).toISOString();
|
|
1376
|
+
logger.debug(`Window has ${count} tickets (>${MAX_PER_WINDOW}) — bisecting at ${midIso}`);
|
|
1377
|
+
windows.unshift([wStart, midIso], [midIso, wEnd]);
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
logger.info(`Processing window ${wStart}..${wEnd}: ${count} tickets`);
|
|
1382
|
+
await processResults(firstData.results || []);
|
|
1383
|
+
let nextPage = firstData.next_page || null;
|
|
1384
|
+
while (nextPage) {
|
|
1322
1385
|
logger.debug(`Fetching next page: ${nextPage}`);
|
|
1323
|
-
// Rate limiting: wait between requests
|
|
1324
1386
|
await new Promise(res => setTimeout(res, 1000));
|
|
1387
|
+
const data = await fetchWithRetry(nextPage);
|
|
1388
|
+
await processResults(data.results || []);
|
|
1389
|
+
nextPage = data.next_page || null;
|
|
1325
1390
|
}
|
|
1326
1391
|
}
|
|
1327
1392
|
// Only advance the watermark when all tickets succeeded
|