callkaro-cli-unofficial 0.7.1 → 0.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/commands/agentVersions.js +153 -0
- package/dist/commands/aiTools.js +64 -0
- package/dist/commands/analytics.js +66 -0
- package/dist/commands/analyzers.js +99 -0
- package/dist/commands/dnd.js +105 -0
- package/dist/commands/feedback.js +84 -0
- package/dist/commands/knowledge.js +33 -0
- package/dist/commands/kpi.js +631 -12
- package/dist/commands/simulations.js +68 -0
- package/dist/index.js +310 -1
- package/dist/lib/dashboardApi.js +262 -0
- package/dist/lib/kpiReport.js +363 -0
- package/package.json +1 -1
package/dist/lib/dashboardApi.js
CHANGED
|
@@ -107,6 +107,56 @@ class DashboardApi {
|
|
|
107
107
|
}
|
|
108
108
|
return data;
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Like `request()`, but returns the raw bytes and suggested filename when the
|
|
112
|
+
* server answers with a binary download (PDF exports). Non-2xx still parses JSON
|
|
113
|
+
* and throws `DashboardError`.
|
|
114
|
+
*/
|
|
115
|
+
async requestBinary(method, path, body) {
|
|
116
|
+
if (!path.startsWith('/'))
|
|
117
|
+
throw new Error(`Dashboard path must start with "/" (got "${path}").`);
|
|
118
|
+
const { token, sessionToken } = await (0, dashboardAuth_1.requireDashboard)();
|
|
119
|
+
const headers = {};
|
|
120
|
+
if (body !== undefined)
|
|
121
|
+
headers['Content-Type'] = 'application/json';
|
|
122
|
+
if (usesCookieAuth(path)) {
|
|
123
|
+
headers.Cookie = `access_token=${sessionToken}`;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
headers.Authorization = `Bearer ${token}`;
|
|
127
|
+
}
|
|
128
|
+
let res;
|
|
129
|
+
try {
|
|
130
|
+
res = await fetch(`${this.baseUrl}${path}`, {
|
|
131
|
+
method,
|
|
132
|
+
headers,
|
|
133
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
134
|
+
redirect: 'error',
|
|
135
|
+
signal: AbortSignal.timeout(60_000),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
throw new Error(`Cannot reach ${this.baseUrl} (${err.message})`);
|
|
140
|
+
}
|
|
141
|
+
if (!res.ok) {
|
|
142
|
+
const text = await res.text();
|
|
143
|
+
let data;
|
|
144
|
+
try {
|
|
145
|
+
data = text ? JSON.parse(text) : undefined;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
data = { msg: text.slice(0, 200) };
|
|
149
|
+
}
|
|
150
|
+
throw new DashboardError(res.status, data?.msg || data?.message || `Request failed (${res.status})`, data);
|
|
151
|
+
}
|
|
152
|
+
const disp = typeof res.headers?.get === 'function' ? res.headers.get('content-disposition') || '' : '';
|
|
153
|
+
const m = /filename="?([^";]+)"?/i.exec(disp);
|
|
154
|
+
const contentType = typeof res.headers?.get === 'function' ? res.headers.get('content-type') || '' : '';
|
|
155
|
+
const ab = typeof res.arrayBuffer === 'function'
|
|
156
|
+
? await res.arrayBuffer()
|
|
157
|
+
: Buffer.from(await res.text());
|
|
158
|
+
return { bytes: Buffer.from(ab), filename: m?.[1], contentType };
|
|
159
|
+
}
|
|
110
160
|
/**
|
|
111
161
|
* `GET /v2/agents/` — the one route that wants the body token as a bearer.
|
|
112
162
|
* The only place `agentFolders` is exposed, which `/cli/agents` does not return.
|
|
@@ -225,6 +275,218 @@ class DashboardApi {
|
|
|
225
275
|
runKpiRca(agentId, filters) {
|
|
226
276
|
return this.request('POST', `/kpi-manager/${encodeURIComponent(agentId)}/rca/run-now`, { filters });
|
|
227
277
|
}
|
|
278
|
+
/** Live-evaluate a KPI Report configuration (values, labels, chart series, source_fields). */
|
|
279
|
+
previewKpiReport(agentId, reportConfig) {
|
|
280
|
+
return this.request('POST', `/kpi-manager/${encodeURIComponent(agentId)}/report/preview`, {
|
|
281
|
+
report_config: { ...reportConfig, recipients: [] },
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/** Render a KPI Report configuration to a PDF binary. */
|
|
285
|
+
exportKpiReport(agentId, reportConfig) {
|
|
286
|
+
return this.requestBinary('POST', `/kpi-manager/${encodeURIComponent(agentId)}/report/export`, {
|
|
287
|
+
report_config: reportConfig,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/** Draft or refine an RCA prompt with AI (`POST /openai-tools/generate-rca-prompt-draft`). */
|
|
291
|
+
generateRcaPromptDraft(body) {
|
|
292
|
+
return this.request('POST', '/openai-tools/generate-rca-prompt-draft', body);
|
|
293
|
+
}
|
|
294
|
+
/** Verify Python data source code before saving (`POST /utils/verify-code`). */
|
|
295
|
+
verifyKpiPythonCode(sourceCode, name = 'generate_kpi_data') {
|
|
296
|
+
return this.request('POST', '/utils/verify-code', {
|
|
297
|
+
name,
|
|
298
|
+
source_code: sourceCode,
|
|
299
|
+
type: 'kpi_data_source',
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/** Bulk-import pending task cards (`POST /pending-tasks/import`). */
|
|
303
|
+
importPendingTasks(rows) {
|
|
304
|
+
return this.request('POST', '/pending-tasks/import', { rows });
|
|
305
|
+
}
|
|
306
|
+
/** Daywise RCA reports (`GET /v1/audit-reports/daywise`). */
|
|
307
|
+
daywiseRcaReports(query = {}) {
|
|
308
|
+
return this.request('GET', `/v1/audit-reports/daywise${qs(query)}`);
|
|
309
|
+
}
|
|
310
|
+
/** Day-by-day RCA execution timeline (`GET /v1/audit-reports/daywise/:agentId/timeline`). */
|
|
311
|
+
daywiseRcaTimeline(agentId, query = {}) {
|
|
312
|
+
return this.request('GET', `/v1/audit-reports/daywise/${encodeURIComponent(agentId)}/timeline${qs(query)}`);
|
|
313
|
+
}
|
|
314
|
+
/** Download a daywise RCA report PDF (`GET /v1/audit-reports/daywise/:agentId/:rcaDate/pdf`). */
|
|
315
|
+
daywiseRcaPdf(agentId, rcaDate, source) {
|
|
316
|
+
const q = source === 'manual' ? { source: 'manual' } : {};
|
|
317
|
+
return this.requestBinary('GET', `/v1/audit-reports/daywise/${encodeURIComponent(agentId)}/${encodeURIComponent(rcaDate)}/pdf${qs(q)}`);
|
|
318
|
+
}
|
|
319
|
+
// --- Agent version commits, diff, revert & v2 save -------------------------
|
|
320
|
+
/** List commits for an agent (`GET /v2/commits/:agentId?version=&page=&pageSize=`). */
|
|
321
|
+
agentCommits(agentId, query = {}) {
|
|
322
|
+
return this.request('GET', `/v2/commits/${encodeURIComponent(agentId)}${qs(query)}`);
|
|
323
|
+
}
|
|
324
|
+
/** Fetch one commit in full (`GET /v2/commits/single/:commitId`). */
|
|
325
|
+
agentCommit(commitId) {
|
|
326
|
+
return this.request('GET', `/v2/commits/single/${encodeURIComponent(commitId)}`);
|
|
327
|
+
}
|
|
328
|
+
/** Compare two commits of an agent (`GET /v2/commits/:agentId/compare?commitA=&commitB=`). */
|
|
329
|
+
compareAgentCommits(agentId, commitA, commitB) {
|
|
330
|
+
return this.request('GET', `/v2/commits/${encodeURIComponent(agentId)}/compare${qs({ commitA, commitB })}`);
|
|
331
|
+
}
|
|
332
|
+
/** Revert an agent version to a previous commit (`POST /v2/commits/revert/:agentId/:commitId`). */
|
|
333
|
+
revertAgentCommit(agentId, commitId) {
|
|
334
|
+
return this.request('POST', `/v2/commits/revert/${encodeURIComponent(agentId)}/${encodeURIComponent(commitId)}`, {});
|
|
335
|
+
}
|
|
336
|
+
/** Compare two agent versions (`POST /v2/agents/compareVersions` with `{ versionIds: [a, b] }`). */
|
|
337
|
+
compareAgentVersions(versionIds) {
|
|
338
|
+
return this.request('POST', '/v2/agents/compareVersions', { versionIds });
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Dashboard `/v2/agents/:agentId?version=:versionId` save — accepts function
|
|
342
|
+
* `source_code` and `commitMessage` that `/cli/agents` rejects.
|
|
343
|
+
*/
|
|
344
|
+
updateAgentV2(agentId, versionId, body) {
|
|
345
|
+
const q = versionId ? qs({ version: versionId }) : '';
|
|
346
|
+
return this.request('PUT', `/v2/agents/${encodeURIComponent(agentId)}${q}`, body);
|
|
347
|
+
}
|
|
348
|
+
/** Delete an agent version (`DELETE /v2/agents/deleteversion/:agentId?version=:versionId`). */
|
|
349
|
+
deleteAgentVersionV2(agentId, versionId) {
|
|
350
|
+
return this.request('DELETE', `/v2/agents/deleteversion/${encodeURIComponent(agentId)}${qs({ version: versionId })}`);
|
|
351
|
+
}
|
|
352
|
+
/** Duplicate an agent (`PUT /v2/agents/duplicate/:agentId` with `{ name, versions }`). */
|
|
353
|
+
duplicateAgentV2(agentId, body) {
|
|
354
|
+
return this.request('PUT', `/v2/agents/duplicate/${encodeURIComponent(agentId)}`, body);
|
|
355
|
+
}
|
|
356
|
+
// --- Audit summary drilldowns (`/v1/audit-summary/*`) ----------------------
|
|
357
|
+
/** Summary rollup for a strategy or batch (`GET /v1/audit-summary/:scope/:id/summary`). */
|
|
358
|
+
auditSummary(scope, id, query = {}) {
|
|
359
|
+
return this.request('GET', `/v1/audit-summary/${scope}/${encodeURIComponent(id)}/summary${qs(query)}`);
|
|
360
|
+
}
|
|
361
|
+
/** Audited calls in a dimension/category (`GET /v1/audit-summary/:scope/:id/categories/:dimension/:category/calls`). */
|
|
362
|
+
auditCategoryCalls(scope, id, dimension, category, query = {}) {
|
|
363
|
+
return this.request('GET', `/v1/audit-summary/${scope}/${encodeURIComponent(id)}/categories/${encodeURIComponent(dimension)}/${encodeURIComponent(category)}/calls${qs(query)}`);
|
|
364
|
+
}
|
|
365
|
+
/** Audited calls at a severity level (`GET /v1/audit-summary/:scope/:id/severities/:severity/calls`). */
|
|
366
|
+
auditSeverityCalls(scope, id, severity, query = {}) {
|
|
367
|
+
return this.request('GET', `/v1/audit-summary/${scope}/${encodeURIComponent(id)}/severities/${encodeURIComponent(severity)}/calls${qs(query)}`);
|
|
368
|
+
}
|
|
369
|
+
/** Audited calls for an extra-variable value (`GET /v1/audit-summary/:scope/:id/extra-variables/:name/:value/calls`). */
|
|
370
|
+
auditExtraVariableCalls(scope, id, name, value, query = {}) {
|
|
371
|
+
return this.request('GET', `/v1/audit-summary/${scope}/${encodeURIComponent(id)}/extra-variables/${encodeURIComponent(name)}/${encodeURIComponent(value)}/calls${qs(query)}`);
|
|
372
|
+
}
|
|
373
|
+
/** Audited calls scoring below 80 (`GET /v1/audit-summary/:scope/:id/scores/below-80/calls`). */
|
|
374
|
+
auditBelow80Calls(scope, id, query = {}) {
|
|
375
|
+
return this.request('GET', `/v1/audit-summary/${scope}/${encodeURIComponent(id)}/scores/below-80/calls${qs(query)}`);
|
|
376
|
+
}
|
|
377
|
+
/** Single audited call report (`GET /v1/audit-summary/:scope/:id/calls/:callId`). */
|
|
378
|
+
auditCallDetail(scope, id, callId, query = {}) {
|
|
379
|
+
return this.request('GET', `/v1/audit-summary/${scope}/${encodeURIComponent(id)}/calls/${encodeURIComponent(callId)}${qs(query)}`);
|
|
380
|
+
}
|
|
381
|
+
// --- Diagnostic analytics (`/analytics/*`) ---------------------------------
|
|
382
|
+
/** Failed LLM/TTS response buckets (`GET /analytics/failed-responses`). */
|
|
383
|
+
failedResponses(query = {}) {
|
|
384
|
+
return this.request('GET', `/analytics/failed-responses${qs(query)}`);
|
|
385
|
+
}
|
|
386
|
+
/** Calls matching a failed response bucket (`GET /analytics/failed-response-calls`). */
|
|
387
|
+
failedResponseCalls(query = {}) {
|
|
388
|
+
return this.request('GET', `/analytics/failed-response-calls${qs(query)}`);
|
|
389
|
+
}
|
|
390
|
+
/** Per-function execution analytics (`GET /analytics/functions`). */
|
|
391
|
+
functionAnalytics(query = {}) {
|
|
392
|
+
return this.request('GET', `/analytics/functions${qs(query)}`);
|
|
393
|
+
}
|
|
394
|
+
/** Chat agent analytics (`GET /analytics/chat`). */
|
|
395
|
+
chatAnalytics(query = {}) {
|
|
396
|
+
return this.request('GET', `/analytics/chat${qs(query)}`);
|
|
397
|
+
}
|
|
398
|
+
// --- Do-Not-Disturb numbers (`/dnd-phone-numbers/*`) -----------------------
|
|
399
|
+
/** List DND numbers (`GET /dnd-phone-numbers`). */
|
|
400
|
+
listDndNumbers(query = {}) {
|
|
401
|
+
return this.request('GET', `/dnd-phone-numbers${qs(query)}`);
|
|
402
|
+
}
|
|
403
|
+
/** Add a single DND phone number (`POST /dnd-phone-numbers/add`). */
|
|
404
|
+
addDndNumber(phoneNumber) {
|
|
405
|
+
return this.request('POST', '/dnd-phone-numbers/add', { phoneNumber });
|
|
406
|
+
}
|
|
407
|
+
/** Update a DND record (`PUT /dnd-phone-numbers/:id`). */
|
|
408
|
+
updateDndNumber(id, phoneNumber) {
|
|
409
|
+
return this.request('PUT', `/dnd-phone-numbers/${encodeURIComponent(id)}`, { phoneNumber });
|
|
410
|
+
}
|
|
411
|
+
/** Remove a DND record by id (`DELETE /dnd-phone-numbers/:id`). */
|
|
412
|
+
removeDndNumber(id) {
|
|
413
|
+
return this.request('DELETE', `/dnd-phone-numbers/${encodeURIComponent(id)}`);
|
|
414
|
+
}
|
|
415
|
+
/** Remove multiple DND records by id (`POST /dnd-phone-numbers/delete-multiple`). */
|
|
416
|
+
removeDndNumbers(ids) {
|
|
417
|
+
return this.request('POST', '/dnd-phone-numbers/delete-multiple', { ids });
|
|
418
|
+
}
|
|
419
|
+
/** Bulk import DND phone numbers (`POST /dnd-phone-numbers/import`). */
|
|
420
|
+
importDndNumbers(phoneNumbers) {
|
|
421
|
+
return this.request('POST', '/dnd-phone-numbers/import', { phoneNumbers });
|
|
422
|
+
}
|
|
423
|
+
// --- Call QA Feedback tickets (`/feedback/*`) ------------------------------
|
|
424
|
+
/** List feedback tickets (`GET /feedback?status=processing|in-review|resolved&page=1`). */
|
|
425
|
+
listFeedback(query = {}) {
|
|
426
|
+
return this.request('GET', `/feedback${qs({ status: query.status || 'processing', page: query.page || 1 })}`);
|
|
427
|
+
}
|
|
428
|
+
/** Create a feedback ticket (`POST /feedback`). */
|
|
429
|
+
createFeedback(body) {
|
|
430
|
+
return this.request('POST', '/feedback', body);
|
|
431
|
+
}
|
|
432
|
+
/** Comment on a feedback ticket (`PUT /feedback/:id/comment`). */
|
|
433
|
+
commentFeedback(id, text) {
|
|
434
|
+
return this.request('PUT', `/feedback/${encodeURIComponent(id)}/comment`, { text });
|
|
435
|
+
}
|
|
436
|
+
/** Submit a feedback ticket for review (`PUT /feedback/:id/submit-review`). */
|
|
437
|
+
submitFeedbackReview(id, body) {
|
|
438
|
+
return this.request('PUT', `/feedback/${encodeURIComponent(id)}/submit-review`, body);
|
|
439
|
+
}
|
|
440
|
+
/** Send a feedback ticket back to processing (`PUT /feedback/:id/back-to-processing`). */
|
|
441
|
+
reopenFeedback(id, comment) {
|
|
442
|
+
return this.request('PUT', `/feedback/${encodeURIComponent(id)}/back-to-processing`, { comment });
|
|
443
|
+
}
|
|
444
|
+
/** Resolve a feedback ticket (`PUT /feedback/:id/resolve`). */
|
|
445
|
+
resolveFeedback(id, comment) {
|
|
446
|
+
return this.request('PUT', `/feedback/${encodeURIComponent(id)}/resolve`, { comment });
|
|
447
|
+
}
|
|
448
|
+
// --- Knowledge Base (`/knowledge/*`) ---------------------------------------
|
|
449
|
+
/** List knowledge base items for the user (`GET /knowledge/user`). */
|
|
450
|
+
listKnowledge() {
|
|
451
|
+
return this.request('GET', '/knowledge/user');
|
|
452
|
+
}
|
|
453
|
+
/** Delete a knowledge base item (`DELETE /knowledge/:id`). */
|
|
454
|
+
deleteKnowledge(id) {
|
|
455
|
+
return this.request('DELETE', `/knowledge/${encodeURIComponent(id)}`);
|
|
456
|
+
}
|
|
457
|
+
// --- OpenAI / AI Copilot generators (`/openai-tools/*`) --------------------
|
|
458
|
+
/** Draft a simulation test case from a real call (`POST /openai-tools/generate-simulation-testcase-draft`). */
|
|
459
|
+
generateSimFromCall(body) {
|
|
460
|
+
return this.request('POST', '/openai-tools/generate-simulation-testcase-draft', {
|
|
461
|
+
includeMetadata: true,
|
|
462
|
+
includeFunctions: true,
|
|
463
|
+
...body,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
/** Generate or refine a simulation test case from a hint (`POST /openai-tools/generate-simulation-testcase-from-prompt`). */
|
|
467
|
+
generateSimFromPrompt(body) {
|
|
468
|
+
return this.request('POST', '/openai-tools/generate-simulation-testcase-from-prompt', body);
|
|
469
|
+
}
|
|
470
|
+
/** Generate an audit strategy draft (`POST /openai-tools/generate-audit-strategy-draft`). */
|
|
471
|
+
generateAuditStrategyDraft(body) {
|
|
472
|
+
return this.request('POST', '/openai-tools/generate-audit-strategy-draft', body);
|
|
473
|
+
}
|
|
474
|
+
/** Generate a callflow summary draft (`POST /openai-tools/generate-callflow-summary-draft`). */
|
|
475
|
+
generateCallflowSummaryDraft(body) {
|
|
476
|
+
return this.request('POST', '/openai-tools/generate-callflow-summary-draft', body);
|
|
477
|
+
}
|
|
478
|
+
/** Generate or edit a voice agent function (`POST /openai-tools/generate-function`). */
|
|
479
|
+
generateFunctionCode(body) {
|
|
480
|
+
return this.request('POST', '/openai-tools/generate-function', body);
|
|
481
|
+
}
|
|
482
|
+
/** Generate post-call analysis config (`POST /openai-tools/generate-postcall-config`). */
|
|
483
|
+
generatePostcallConfig(body) {
|
|
484
|
+
return this.request('POST', '/openai-tools/generate-postcall-config', body);
|
|
485
|
+
}
|
|
486
|
+
/** Generate workflow code step (`POST /openai-tools/generate-workflow-code`). */
|
|
487
|
+
generateWorkflowCode(body) {
|
|
488
|
+
return this.request('POST', '/openai-tools/generate-workflow-code', body);
|
|
489
|
+
}
|
|
228
490
|
// --- Call analyzers -------------------------------------------------------
|
|
229
491
|
listAnalyzers() { return this.request('GET', '/v1/call-analyzers'); }
|
|
230
492
|
// --- Audit reports (dashboard side) ---------------------------------------
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* KPI Report canvas & config helpers, mirrored from the dashboard bundle (`XIr` / `sVe`).
|
|
4
|
+
*
|
|
5
|
+
* The backend replaces the whole `/kpi-manager/:agentId` document on `PUT`, so
|
|
6
|
+
* every edit here works on a normalized `KpiAgentConfig` and preserves all 10
|
|
7
|
+
* top-level fields (`api_endpoints`, `python_data_sources`, `expression_data_sources`,
|
|
8
|
+
* `kpi_metrics`, `rca_model`, `rca_llms`, `rca_prompt`, `nightly_rca_enabled`,
|
|
9
|
+
* `report_enabled`, `report_config`).
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.REPORT_SCHEMA_REFERENCE = exports.newElementId = exports.LOCKED_METRIC = exports.CHART_TYPES = exports.BUILTIN_DIMENSIONS = exports.BUILTIN_VARIABLES = exports.INTERVAL_HOURS = exports.CHART_HEIGHTS = exports.DEFAULT_WIDTHS = exports.MIN_H = exports.MIN_W = exports.MARGIN = exports.PAGE_H = exports.PAGE_W = void 0;
|
|
13
|
+
exports.rcaLlmsFor = rcaLlmsFor;
|
|
14
|
+
exports.normalizeMetrics = normalizeMetrics;
|
|
15
|
+
exports.canonicalRef = canonicalRef;
|
|
16
|
+
exports.shortToken = shortToken;
|
|
17
|
+
exports.parseRef = parseRef;
|
|
18
|
+
exports.defaultChartAxes = defaultChartAxes;
|
|
19
|
+
exports.defaultReportConfig = defaultReportConfig;
|
|
20
|
+
exports.nextElementY = nextElementY;
|
|
21
|
+
exports.normalizeElement = normalizeElement;
|
|
22
|
+
exports.normalizeReportConfig = normalizeReportConfig;
|
|
23
|
+
exports.buildFullKpiPayload = buildFullKpiPayload;
|
|
24
|
+
exports.formatPreviewValue = formatPreviewValue;
|
|
25
|
+
exports.interpolateReportText = interpolateReportText;
|
|
26
|
+
exports.PAGE_W = 794;
|
|
27
|
+
exports.PAGE_H = 1123;
|
|
28
|
+
exports.MARGIN = 40;
|
|
29
|
+
exports.MIN_W = 80;
|
|
30
|
+
exports.MIN_H = 40;
|
|
31
|
+
exports.DEFAULT_WIDTHS = { chart: 520, table: 480, text: 360 };
|
|
32
|
+
exports.CHART_HEIGHTS = {
|
|
33
|
+
tiles: 130, bar: 320, line: 320, pie: 260, funnel: 300, table: 260,
|
|
34
|
+
};
|
|
35
|
+
exports.INTERVAL_HOURS = [1, 2, 3, 4, 6, 8, 12, 24, 48, 72, 168];
|
|
36
|
+
exports.BUILTIN_VARIABLES = [
|
|
37
|
+
{ key: 'total_calls', label: 'Total triggered calls', valueKind: 'count' },
|
|
38
|
+
{ key: 'connected_calls', label: 'Total connected calls', valueKind: 'count' },
|
|
39
|
+
{ key: 'connected_pct', label: 'Connected %', valueKind: 'pct' },
|
|
40
|
+
{ key: 'unique_receipts', label: 'Unique receipts', valueKind: 'count' },
|
|
41
|
+
{ key: 'unique_connected', label: 'Unique connected receipts', valueKind: 'count' },
|
|
42
|
+
{ key: 'unique_connected_pct', label: 'Receipt connectivity %', valueKind: 'pct' },
|
|
43
|
+
{ key: 'converted_calls', label: 'Converted calls', valueKind: 'count' },
|
|
44
|
+
{ key: 'converted_pct', label: 'Converted % (of connected)', valueKind: 'pct' },
|
|
45
|
+
{ key: 'unique_converted', label: 'Unique converted receipts', valueKind: 'count' },
|
|
46
|
+
{ key: 'avg_connected_duration', label: 'Avg connected duration (s)', valueKind: 'number' },
|
|
47
|
+
{ key: 'drop_off_reasons', label: 'Drop-off reasons', valueKind: 'breakdown' },
|
|
48
|
+
{ key: 'hangup_reasons', label: 'Hangup reasons', valueKind: 'breakdown' },
|
|
49
|
+
];
|
|
50
|
+
exports.BUILTIN_DIMENSIONS = [
|
|
51
|
+
{ value: 'dim:time_hour', label: 'Hour' },
|
|
52
|
+
{ value: 'dim:time_day', label: 'Day' },
|
|
53
|
+
{ value: 'dim:hangup_reason', label: 'Hangup reason' },
|
|
54
|
+
{ value: 'dim:drop_off_reason', label: 'Drop-off reason' },
|
|
55
|
+
{ value: 'dim:is_connected', label: 'Connected / not connected' },
|
|
56
|
+
{ value: 'dim:converted', label: 'Converted / not converted' },
|
|
57
|
+
{ value: 'dim:direction', label: 'Direction' },
|
|
58
|
+
];
|
|
59
|
+
exports.CHART_TYPES = ['tiles', 'bar', 'line', 'pie', 'funnel', 'table'];
|
|
60
|
+
const CHART_META = {
|
|
61
|
+
bar: { x: 'X axis', y: 'Y axis', stack: 'Stack by', multiY: true, metricsX: true },
|
|
62
|
+
line: { x: 'X axis', y: 'Y axis', stack: 'Split by', multiY: true },
|
|
63
|
+
pie: { x: 'Slices', y: 'Value', multiY: false, metricsX: true },
|
|
64
|
+
funnel: { x: 'Split by', y: 'Steps', multiY: true, optionalX: true, direction: true },
|
|
65
|
+
tiles: { y: 'Metrics', multiY: true },
|
|
66
|
+
table: { x: 'Rows', y: 'Columns', multiY: true, metricsX: true },
|
|
67
|
+
};
|
|
68
|
+
const RCA_MODELS = [
|
|
69
|
+
{ provider: 'callkaro', name: 'callkaro/krishna-2.5' },
|
|
70
|
+
{ provider: 'azure-openai', name: 'gpt-5.6-luna' },
|
|
71
|
+
{ provider: 'bedrock', name: 'arn:aws:bedrock:ap-south-2:525856616822:inference-profile/global.anthropic.claude-sonnet-4-6' },
|
|
72
|
+
{ provider: 'bedrock', name: 'arn:aws:bedrock:ap-south-2:525856616822:inference-profile/global.anthropic.claude-opus-4-6-v1' },
|
|
73
|
+
{ provider: 'azure-openai', name: 'gpt-5.6-sol' },
|
|
74
|
+
];
|
|
75
|
+
const DEFAULT_RCA_MODEL = { provider: 'azure-openai', name: 'gpt-5.6-luna' };
|
|
76
|
+
exports.LOCKED_METRIC = 'connected_to_conversion_percentage';
|
|
77
|
+
function rcaLlmsFor(modelName) {
|
|
78
|
+
const name = (modelName || '').trim() || DEFAULT_RCA_MODEL.name;
|
|
79
|
+
const hit = RCA_MODELS.find((m) => m.name === name);
|
|
80
|
+
if (hit)
|
|
81
|
+
return { provider: hit.provider, name };
|
|
82
|
+
if (name.startsWith('callkaro/'))
|
|
83
|
+
return { provider: 'callkaro', name };
|
|
84
|
+
if (name.startsWith('arn:aws:bedrock'))
|
|
85
|
+
return { provider: 'bedrock', name };
|
|
86
|
+
return { provider: DEFAULT_RCA_MODEL.provider, name };
|
|
87
|
+
}
|
|
88
|
+
function normalizeMetrics(metrics) {
|
|
89
|
+
const list = Array.isArray(metrics) ? metrics : [];
|
|
90
|
+
const locked = list.find((m) => m?.name === exports.LOCKED_METRIC || m?.name === 'connected_to_conversion');
|
|
91
|
+
const rest = list
|
|
92
|
+
.filter((m) => m && m.name !== exports.LOCKED_METRIC && m.name !== 'connected_to_conversion')
|
|
93
|
+
.map((m) => ({ name: String(m.name || ''), source: String(m.source || ''), value: String(m.value ?? '') }));
|
|
94
|
+
return [
|
|
95
|
+
{ name: exports.LOCKED_METRIC, source: exports.LOCKED_METRIC, value: String(locked?.value ?? '6') },
|
|
96
|
+
...rest,
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
const newElementId = () => `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
100
|
+
exports.newElementId = newElementId;
|
|
101
|
+
/** Bare token (`total_calls`) becomes `builtin:total_calls`; scoped refs stay as-is. */
|
|
102
|
+
function canonicalRef(raw) {
|
|
103
|
+
const trimmed = String(raw || '').trim();
|
|
104
|
+
if (!trimmed)
|
|
105
|
+
return '';
|
|
106
|
+
return trimmed.includes(':') ? trimmed : `builtin:${trimmed}`;
|
|
107
|
+
}
|
|
108
|
+
/** Short token inside `${...}`: strips `builtin:`, keeps `custom:...`, `postcall:...`, etc. */
|
|
109
|
+
function shortToken(ref) {
|
|
110
|
+
const s = String(ref || '').trim();
|
|
111
|
+
return s.startsWith('builtin:') ? s.slice(8) : s;
|
|
112
|
+
}
|
|
113
|
+
function parseRef(ref) {
|
|
114
|
+
const [left, base = 'all'] = String(ref || '').split('#');
|
|
115
|
+
const idx = left.indexOf(':');
|
|
116
|
+
return idx >= 0
|
|
117
|
+
? { kind: left.slice(0, idx), id: left.slice(idx + 1), base }
|
|
118
|
+
: { kind: 'builtin', id: left, base };
|
|
119
|
+
}
|
|
120
|
+
const BREAKDOWN_TO_DIM = {
|
|
121
|
+
'builtin:hangup_reasons': 'dim:hangup_reason',
|
|
122
|
+
'builtin:drop_off_reasons': 'dim:drop_off_reason',
|
|
123
|
+
};
|
|
124
|
+
const BASE_TO_MEASURE = {
|
|
125
|
+
all: 'builtin:total_calls',
|
|
126
|
+
connected: 'builtin:connected_calls',
|
|
127
|
+
converted: 'builtin:converted_calls',
|
|
128
|
+
unique: 'builtin:unique_receipts',
|
|
129
|
+
unique_connected: 'builtin:unique_connected',
|
|
130
|
+
};
|
|
131
|
+
function autoBreakdownAxes(ref) {
|
|
132
|
+
if (BREAKDOWN_TO_DIM[ref])
|
|
133
|
+
return { axis: BREAKDOWN_TO_DIM[ref], measure: 'builtin:total_calls' };
|
|
134
|
+
const { kind, id, base } = parseRef(ref);
|
|
135
|
+
if (kind === 'postcall') {
|
|
136
|
+
return { axis: `dim:postcall:${id}`, measure: BASE_TO_MEASURE[base] || 'builtin:total_calls' };
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function defaultChartAxes(chartType, refs = []) {
|
|
141
|
+
const meta = CHART_META[chartType] || CHART_META.table;
|
|
142
|
+
const clean = refs.map(canonicalRef).filter(Boolean);
|
|
143
|
+
if (!clean.length)
|
|
144
|
+
return { x_axis: null, y_axis: [], stack_by: null };
|
|
145
|
+
const single = clean.length === 1 ? autoBreakdownAxes(clean[0]) : null;
|
|
146
|
+
if (single && meta.x)
|
|
147
|
+
return { x_axis: single.axis, y_axis: [single.measure], stack_by: null };
|
|
148
|
+
if (!meta.x || meta.optionalX)
|
|
149
|
+
return { x_axis: null, y_axis: clean, stack_by: null };
|
|
150
|
+
if (chartType === 'line')
|
|
151
|
+
return { x_axis: 'dim:time_hour', y_axis: clean, stack_by: null };
|
|
152
|
+
return {
|
|
153
|
+
x_axis: 'metrics',
|
|
154
|
+
y_axis: meta.multiY ? clean : clean.slice(0, 1),
|
|
155
|
+
stack_by: null,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function defaultReportConfig() {
|
|
159
|
+
return {
|
|
160
|
+
recipients: [],
|
|
161
|
+
min_trigger_time: '08:00',
|
|
162
|
+
max_trigger_time: '08:00',
|
|
163
|
+
interval_hours: 24,
|
|
164
|
+
anchor_at: '',
|
|
165
|
+
custom_variables: [],
|
|
166
|
+
elements: [],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function estimatedHeight(el) {
|
|
170
|
+
if (el?.h && Number.isFinite(Number(el.h)))
|
|
171
|
+
return Number(el.h);
|
|
172
|
+
if (el?.type === 'table')
|
|
173
|
+
return ((el.cells?.length || 1) * 36);
|
|
174
|
+
if (el?.type === 'text')
|
|
175
|
+
return 64;
|
|
176
|
+
return exports.CHART_HEIGHTS[el?.chart_type] || 240;
|
|
177
|
+
}
|
|
178
|
+
function nextElementY(elements) {
|
|
179
|
+
if (!Array.isArray(elements) || !elements.length)
|
|
180
|
+
return exports.MARGIN;
|
|
181
|
+
return elements.reduce((max, el) => Math.max(max, (Number(el.y) || 0) + estimatedHeight(el) + 24), exports.MARGIN);
|
|
182
|
+
}
|
|
183
|
+
function normalizeElement(el) {
|
|
184
|
+
if (!el || typeof el !== 'object')
|
|
185
|
+
return el;
|
|
186
|
+
if (el.type !== 'chart')
|
|
187
|
+
return el;
|
|
188
|
+
const chartType = (exports.CHART_TYPES.includes(el.chart_type) ? el.chart_type : 'tiles');
|
|
189
|
+
const h = el.h ?? (exports.CHART_HEIGHTS[chartType] || 280);
|
|
190
|
+
if (el.x_axis !== undefined) {
|
|
191
|
+
const yAxis = Array.isArray(el.y_axis) ? el.y_axis.map(canonicalRef) : (el.refs || []).map(canonicalRef);
|
|
192
|
+
return { ...el, chart_type: chartType, h, y_axis: yAxis, refs: yAxis };
|
|
193
|
+
}
|
|
194
|
+
const axes = defaultChartAxes(chartType, el.refs || el.y_axis || []);
|
|
195
|
+
return { ...el, chart_type: chartType, h, ...axes, refs: axes.y_axis };
|
|
196
|
+
}
|
|
197
|
+
function normalizeReportConfig(raw) {
|
|
198
|
+
const src = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
199
|
+
const { widgets, ...rest } = src;
|
|
200
|
+
const rawElements = Array.isArray(src.elements)
|
|
201
|
+
? src.elements
|
|
202
|
+
: Array.isArray(widgets)
|
|
203
|
+
? widgets.map((w) => ({
|
|
204
|
+
id: w.id || (0, exports.newElementId)(),
|
|
205
|
+
type: 'chart',
|
|
206
|
+
chart_type: w.type === 'stats' ? 'tiles' : w.type,
|
|
207
|
+
title: w.title || '',
|
|
208
|
+
refs: w.refs || [],
|
|
209
|
+
x: exports.MARGIN,
|
|
210
|
+
y: exports.MARGIN,
|
|
211
|
+
w: exports.PAGE_W - exports.MARGIN * 2,
|
|
212
|
+
options: w.options || {},
|
|
213
|
+
}))
|
|
214
|
+
: [];
|
|
215
|
+
return {
|
|
216
|
+
...defaultReportConfig(),
|
|
217
|
+
...rest,
|
|
218
|
+
recipients: Array.isArray(src.recipients) ? src.recipients : [],
|
|
219
|
+
custom_variables: Array.isArray(src.custom_variables) ? src.custom_variables : [],
|
|
220
|
+
elements: rawElements.map(normalizeElement),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Turn a `GET /kpi-manager/:agentId` response (or `{}` when 404) into a complete
|
|
225
|
+
* 10-field `PUT /kpi-manager/:agentId` body so partial CLI edits never wipe
|
|
226
|
+
* sibling fields (`api_endpoints`, `report_config`, `rca_prompt`, etc.).
|
|
227
|
+
*/
|
|
228
|
+
function buildFullKpiPayload(current = {}, patch = {}) {
|
|
229
|
+
const rcaModel = String(patch.rca_model ?? current?.rca_model ?? DEFAULT_RCA_MODEL.name);
|
|
230
|
+
return {
|
|
231
|
+
api_endpoints: patch.api_endpoints ?? (Array.isArray(current?.api_endpoints) ? current.api_endpoints : []),
|
|
232
|
+
python_data_sources: patch.python_data_sources ?? (Array.isArray(current?.python_data_sources) ? current.python_data_sources : []),
|
|
233
|
+
expression_data_sources: patch.expression_data_sources ??
|
|
234
|
+
(Array.isArray(current?.expression_data_sources) ? current.expression_data_sources : []),
|
|
235
|
+
kpi_metrics: normalizeMetrics(patch.kpi_metrics ?? current?.kpi_metrics),
|
|
236
|
+
rca_model: rcaModel,
|
|
237
|
+
rca_llms: patch.rca_llms ?? rcaLlmsFor(rcaModel),
|
|
238
|
+
rca_prompt: String(patch.rca_prompt ?? current?.rca_prompt ?? ''),
|
|
239
|
+
nightly_rca_enabled: patch.nightly_rca_enabled !== undefined
|
|
240
|
+
? Boolean(patch.nightly_rca_enabled)
|
|
241
|
+
: current?.nightly_rca_enabled !== false,
|
|
242
|
+
report_enabled: patch.report_enabled !== undefined ? Boolean(patch.report_enabled) : current?.report_enabled === true,
|
|
243
|
+
report_config: normalizeReportConfig(patch.report_config ?? current?.report_config),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/** Format a preview value object (`{kind, value, actual, rows, ...}`) for terminal output. */
|
|
247
|
+
function formatPreviewValue(val) {
|
|
248
|
+
if (!val)
|
|
249
|
+
return '—';
|
|
250
|
+
const fmt = (n) => n === null || n === undefined || n === '' || Number.isNaN(Number(n))
|
|
251
|
+
? '—'
|
|
252
|
+
: Number(n).toLocaleString('en-IN', { maximumFractionDigits: 2 });
|
|
253
|
+
switch (val.kind) {
|
|
254
|
+
case 'pct':
|
|
255
|
+
return val.value === null || val.value === undefined ? '—' : `${fmt(val.value)}%`;
|
|
256
|
+
case 'count':
|
|
257
|
+
return val.pct !== null && val.pct !== undefined
|
|
258
|
+
? `${fmt(val.value)} (${fmt(val.pct)}% of ${fmt(val.base_value)})`
|
|
259
|
+
: fmt(val.value);
|
|
260
|
+
case 'metric':
|
|
261
|
+
return val.actual === null || val.actual === undefined
|
|
262
|
+
? `— (target ${fmt(val.target)}%)`
|
|
263
|
+
: `${fmt(val.actual)}% (target ${fmt(val.target)}%)`;
|
|
264
|
+
case 'text':
|
|
265
|
+
return val.value === null || val.value === undefined ? val.note || '—' : String(val.value);
|
|
266
|
+
case 'breakdown':
|
|
267
|
+
return Array.isArray(val.rows) && val.rows.length
|
|
268
|
+
? val.rows.map((r) => `${r.label}: ${fmt(r.value)} (${fmt(r.pct)}%)`).join(', ')
|
|
269
|
+
: '—';
|
|
270
|
+
case 'error':
|
|
271
|
+
return `[ERROR: ${val.error}]`;
|
|
272
|
+
default:
|
|
273
|
+
return fmt(val.value);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/** Interpolate `${token}` inside a text box or table cell using `preview.values`. */
|
|
277
|
+
function interpolateReportText(text, values = {}) {
|
|
278
|
+
return String(text || '').replace(/\$\{([^}]+)\}/g, (_m, raw) => {
|
|
279
|
+
const val = values[canonicalRef(raw)] ?? values[String(raw).trim()];
|
|
280
|
+
if (!val)
|
|
281
|
+
return `\${${raw}}`;
|
|
282
|
+
if (val.kind === 'count') {
|
|
283
|
+
return val.value === null || val.value === undefined
|
|
284
|
+
? '—'
|
|
285
|
+
: Number(val.value).toLocaleString('en-IN', { maximumFractionDigits: 2 });
|
|
286
|
+
}
|
|
287
|
+
return formatPreviewValue(val);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
exports.REPORT_SCHEMA_REFERENCE = `KPI Report schema & reference (PUT /kpi-manager/<agentId>)
|
|
291
|
+
|
|
292
|
+
Top-level report fields on /kpi-manager/<agentId>:
|
|
293
|
+
"report_enabled": boolean Auto Report toggle (emails report on schedule)
|
|
294
|
+
"report_config": {
|
|
295
|
+
"recipients": ["a@b.com"], Up to 10 valid email addresses
|
|
296
|
+
"min_trigger_time": "08:00", HH:mm IST (15-min steps)
|
|
297
|
+
"max_trigger_time": "20:00", HH:mm IST (if interval_hours >= 24, equals min_trigger_time)
|
|
298
|
+
"interval_hours": 24, One of: 1, 2, 3, 4, 6, 8, 12, 24, 48, 72, 168
|
|
299
|
+
"anchor_at": "", Set by server on save
|
|
300
|
+
"page_count": 1, Number of A4 pages (794 x 1123 px per page, margin 40)
|
|
301
|
+
"custom_variables": [...], Custom call counters (conditions or expression)
|
|
302
|
+
"elements": [...] Canvas items: text, table, chart
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
Variable references (use in chart y_axis/refs, or as \${...} inside text & table cells):
|
|
306
|
+
Built-in (in \${...} you may omit "builtin:", e.g. \${total_calls}):
|
|
307
|
+
builtin:total_calls Total triggered calls (count)
|
|
308
|
+
builtin:connected_calls Total connected calls (count)
|
|
309
|
+
builtin:connected_pct Connected % (pct)
|
|
310
|
+
builtin:unique_receipts Unique receipts (count)
|
|
311
|
+
builtin:unique_connected Unique connected receipts (count)
|
|
312
|
+
builtin:unique_connected_pct Receipt connectivity % (pct)
|
|
313
|
+
builtin:converted_calls Converted calls (count)
|
|
314
|
+
builtin:converted_pct Converted % of connected (pct)
|
|
315
|
+
builtin:unique_converted Unique converted receipts (count)
|
|
316
|
+
builtin:avg_connected_duration Avg connected duration in seconds (number)
|
|
317
|
+
builtin:drop_off_reasons Drop-off reasons (breakdown)
|
|
318
|
+
builtin:hangup_reasons Hangup reasons (breakdown)
|
|
319
|
+
|
|
320
|
+
Post-call variables (<base> is: all | connected | unique | unique_connected; #all may be omitted):
|
|
321
|
+
postcall:<varName> e.g. postcall:car_name, postcall:test_drive_booked#connected
|
|
322
|
+
|
|
323
|
+
Custom variables (<base> is: all | unique; #all may be omitted):
|
|
324
|
+
custom:<varName> e.g. custom:booked_test_drives, custom:booked_test_drives#unique
|
|
325
|
+
|
|
326
|
+
Data sources (from api_endpoints, python_data_sources, expression_data_sources):
|
|
327
|
+
source:<apiName>.data.<field> API Endpoint response field
|
|
328
|
+
source:<pyName>.<field> Python Code return dict key
|
|
329
|
+
source:<exprName>.count Expression data source (.count, .total, or .pct)
|
|
330
|
+
|
|
331
|
+
KPI metrics:
|
|
332
|
+
metric:<metricName> e.g. metric:connected_to_conversion_percentage
|
|
333
|
+
|
|
334
|
+
Chart dimensions (for x_axis and stack_by on bar, line, pie, funnel, table charts):
|
|
335
|
+
"metrics" Each metric (compare y_axis metrics side by side)
|
|
336
|
+
"dim:time_hour" Hour
|
|
337
|
+
"dim:time_day" Day
|
|
338
|
+
"dim:hangup_reason" Hangup reason
|
|
339
|
+
"dim:drop_off_reason" Drop-off reason
|
|
340
|
+
"dim:is_connected" Connected / not connected
|
|
341
|
+
"dim:converted" Converted / not converted
|
|
342
|
+
"dim:direction" Direction
|
|
343
|
+
"dim:postcall:<varName>" Any post-call variable on the agent
|
|
344
|
+
"dim:metadata:<key>" Any {{variable}} in the agent's prompt
|
|
345
|
+
|
|
346
|
+
Element types (A4 sheet is 794px wide x 1123px tall per page; page 2 starts at y = 1123):
|
|
347
|
+
1. text:
|
|
348
|
+
{"id":"t1","type":"text","content":"Calls: \${total_calls}","x":40,"y":40,"w":360,
|
|
349
|
+
"style":{"bold":false,"italic":false,"underline":false,"size":14,"color":"#111827","align":"left"}}
|
|
350
|
+
2. table (1..8 rows x 1..8 cols; row 0 is bold header when header !== false):
|
|
351
|
+
{"id":"tb1","type":"table","header":true,"x":40,"y":120,"w":480,
|
|
352
|
+
"cells":[["Metric","Value"],["Triggered","\${total_calls}"],["Connected","\${connected_pct}"]]}
|
|
353
|
+
3. chart (chart_type: tiles | bar | line | pie | funnel | table):
|
|
354
|
+
{"id":"c1","type":"chart","chart_type":"tiles","title":"Headline","x_axis":null,
|
|
355
|
+
"y_axis":["builtin:total_calls","builtin:connected_calls","builtin:connected_pct"],
|
|
356
|
+
"refs":["builtin:total_calls","builtin:connected_calls","builtin:connected_pct"],
|
|
357
|
+
"stack_by":null,"options":{},"x":40,"y":240,"w":714,"h":130}
|
|
358
|
+
|
|
359
|
+
Custom variable & Expression data source syntax:
|
|
360
|
+
Fields: post_call.<name>, metadata.<key>, is_connected, is_converted, hangup_reason, call_duration
|
|
361
|
+
Operators: && || ! == != > >= < <= and in ['a', 'b'] (case-insensitive strings; bare field = is true)
|
|
362
|
+
Example: (is_converted==true) && post_call.test_drive_booked == 'YES' && is_connected
|
|
363
|
+
`;
|