@velaro/cli 1.4.73 → 1.4.75
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/bin/velaro.js +12 -0
- package/lib/commands/ai-field-extraction.js +106 -0
- package/lib/commands/executive-insights.js +91 -0
- package/lib/commands/keyword-intelligence.js +53 -0
- package/lib/commands/quality-reports.js +120 -0
- package/lib/commands/skill-routing.js +61 -0
- package/package.json +1 -1
package/bin/velaro.js
CHANGED
|
@@ -46,7 +46,12 @@ import { reportBackfillCommand } from '../lib/commands/report-backfill.js';
|
|
|
46
46
|
import { staffCommand } from '../lib/commands/staff.js';
|
|
47
47
|
import { reportExplainCommand } from '../lib/commands/report-explain.js';
|
|
48
48
|
import { conversationEfficiencyCommand } from '../lib/commands/conversation-efficiency.js';
|
|
49
|
+
import { executiveInsightsCommand } from '../lib/commands/executive-insights.js';
|
|
49
50
|
import { dbCommand } from '../lib/commands/db.js';
|
|
51
|
+
import { skillRoutingCommand } from '../lib/commands/skill-routing.js';
|
|
52
|
+
import { keywordIntelligenceCommand } from '../lib/commands/keyword-intelligence.js';
|
|
53
|
+
import { aiFieldExtractionCommand } from '../lib/commands/ai-field-extraction.js';
|
|
54
|
+
import { sentimentCommand, qualityCommand, chatIntelligenceThemesCommand } from '../lib/commands/quality-reports.js';
|
|
50
55
|
import { focusCommand } from '../lib/commands/focus.js';
|
|
51
56
|
import { triageCommand } from '../lib/commands/triage.js';
|
|
52
57
|
import { automationDraftCommand } from '../lib/commands/automation-draft.js';
|
|
@@ -147,6 +152,7 @@ await yargs(hideBin(process.argv))
|
|
|
147
152
|
.command(bookingCommand)
|
|
148
153
|
.command(reportCommand)
|
|
149
154
|
.command(conversationEfficiencyCommand)
|
|
155
|
+
.command(executiveInsightsCommand)
|
|
150
156
|
.command(scheduleAdherenceCommand)
|
|
151
157
|
.command(experienceImpactCommand)
|
|
152
158
|
.command(reportSubscriptionCommand)
|
|
@@ -154,6 +160,12 @@ await yargs(hideBin(process.argv))
|
|
|
154
160
|
.command({ command: 'staff', desc: 'Velaro staff: cross-site tools (backfill)', builder: staffCommand, handler: () => {} })
|
|
155
161
|
.command(reportExplainCommand)
|
|
156
162
|
.command(dbCommand)
|
|
163
|
+
.command(skillRoutingCommand)
|
|
164
|
+
.command(keywordIntelligenceCommand)
|
|
165
|
+
.command(aiFieldExtractionCommand)
|
|
166
|
+
.command(sentimentCommand)
|
|
167
|
+
.command(qualityCommand)
|
|
168
|
+
.command(chatIntelligenceThemesCommand)
|
|
157
169
|
.command(focusCommand)
|
|
158
170
|
.command(triageCommand)
|
|
159
171
|
.command(automationDraftCommand)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { messagingGet, messagingPost, messagingPut, messagingDel } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
// -- velaro ai-field-extraction -------------------------------------------------------
|
|
5
|
+
// Post-resolve AI field extraction settings and agent corrections
|
|
6
|
+
// (velaro-messaging /AiFieldExtraction/*). Site comes from auth.
|
|
7
|
+
|
|
8
|
+
const print = (data) => console.log(JSON.stringify(data, null, 2));
|
|
9
|
+
|
|
10
|
+
const get = {
|
|
11
|
+
command: 'get',
|
|
12
|
+
describe: 'Show the extraction config',
|
|
13
|
+
handler: runCommand(async () => print(await messagingGet('/AiFieldExtraction/Config'))),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const FLAG_KEYS = {
|
|
17
|
+
enabled: 'Enabled',
|
|
18
|
+
extractLineOfBusiness: 'ExtractLineOfBusiness',
|
|
19
|
+
extractIntent: 'ExtractIntent',
|
|
20
|
+
extractState: 'ExtractState',
|
|
21
|
+
extractIdentifiers: 'ExtractIdentifiers',
|
|
22
|
+
createThemeTags: 'CreateThemeTags',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Partial update: only fields the caller passed are sent.
|
|
26
|
+
function body(a) {
|
|
27
|
+
const b = {};
|
|
28
|
+
for (const [key, pascal] of Object.entries(FLAG_KEYS)) {
|
|
29
|
+
if (a[key] !== undefined) b[pascal] = a[key];
|
|
30
|
+
}
|
|
31
|
+
if (a.monthlyCap !== undefined) b.MonthlyCap = a.monthlyCap;
|
|
32
|
+
if (a.minConfidence !== undefined) b.MinConfidence = a.minConfidence;
|
|
33
|
+
return b;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const builder = (y) => {
|
|
37
|
+
y.option('enabled', { type: 'boolean', describe: 'Opt-in switch (default off)' });
|
|
38
|
+
y.option('extract-line-of-business', { type: 'boolean' });
|
|
39
|
+
y.option('extract-intent', { type: 'boolean' });
|
|
40
|
+
y.option('extract-state', { type: 'boolean' });
|
|
41
|
+
y.option('extract-identifiers', { type: 'boolean' });
|
|
42
|
+
y.option('create-theme-tags', { type: 'boolean' });
|
|
43
|
+
y.option('monthly-cap', { type: 'number', describe: '1..50000' });
|
|
44
|
+
y.option('min-confidence', { type: 'number', describe: '0..1' });
|
|
45
|
+
return y;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const create = {
|
|
49
|
+
command: 'create',
|
|
50
|
+
describe: 'Create the extraction config (opt-in; extraction stays off until --enabled)',
|
|
51
|
+
builder,
|
|
52
|
+
handler: runCommand(async (a) => print(await messagingPost('/AiFieldExtraction/Config', body(a)))),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const update = {
|
|
56
|
+
command: 'update',
|
|
57
|
+
describe: 'Partial update of the extraction config (only passed fields change)',
|
|
58
|
+
builder,
|
|
59
|
+
handler: runCommand(async (a) => print(await messagingPut('/AiFieldExtraction/Config', body(a)))),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const del = {
|
|
63
|
+
command: 'delete',
|
|
64
|
+
describe: 'Delete the extraction config',
|
|
65
|
+
handler: runCommand(async () => {
|
|
66
|
+
await messagingDel('/AiFieldExtraction/Config');
|
|
67
|
+
console.log('Deleted');
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const fields = {
|
|
72
|
+
command: 'fields <conversationId>',
|
|
73
|
+
describe: 'Show extracted fields for a conversation',
|
|
74
|
+
builder: (y) => y.positional('conversationId', { type: 'number' }),
|
|
75
|
+
handler: runCommand(async (a) => print(await messagingGet(`/AiFieldExtraction/Conversation/${a.conversationId}/Fields`))),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const correct = {
|
|
79
|
+
command: 'correct <conversationId> <fieldName>',
|
|
80
|
+
describe: 'Agent correction of one extracted field',
|
|
81
|
+
builder: (y) => y
|
|
82
|
+
.positional('conversationId', { type: 'number' })
|
|
83
|
+
.positional('fieldName', { type: 'string' })
|
|
84
|
+
.option('value', { type: 'string', demandOption: true }),
|
|
85
|
+
handler: runCommand(async (a) => {
|
|
86
|
+
await messagingPut(`/AiFieldExtraction/Conversation/${a.conversationId}/Fields/${encodeURIComponent(a.fieldName)}`, { Value: a.value });
|
|
87
|
+
console.log('Corrected');
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const extract = {
|
|
92
|
+
command: 'extract <conversationId>',
|
|
93
|
+
describe: 'Run extraction now for one conversation',
|
|
94
|
+
builder: (y) => y.positional('conversationId', { type: 'number' }),
|
|
95
|
+
handler: runCommand(async (a) => print(await messagingPost(`/AiFieldExtraction/Conversation/${a.conversationId}/Extract`, {}))),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const aiFieldExtractionCommand = {
|
|
99
|
+
command: 'ai-field-extraction',
|
|
100
|
+
describe: 'AI field extraction: opt-in settings, monthly cap, extracted fields and corrections',
|
|
101
|
+
builder: (y) => y
|
|
102
|
+
.command(get).command(create).command(update).command(del)
|
|
103
|
+
.command(fields).command(correct).command(extract)
|
|
104
|
+
.demandCommand(1, 'Specify a subcommand. Run "velaro ai-field-extraction --help".'),
|
|
105
|
+
handler: () => {},
|
|
106
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { messagingGet, messagingPost } from '../api.js';
|
|
3
|
+
import { runCommand } from '../run.js';
|
|
4
|
+
|
|
5
|
+
// -- velaro executive-insights ---------------------------------------------------
|
|
6
|
+
// CLI for the Friction Radar, the Executive Brief and the CSV data export. Endpoints live in
|
|
7
|
+
// velaro-messaging at /api/executive-insights/*; the site always comes from the login, never from
|
|
8
|
+
// CLI args. Gated on the Chat Intelligence entitlement (403 when not entitled).
|
|
9
|
+
// Scheduling reuses report subscriptions: `velaro executive-insights subscribe`.
|
|
10
|
+
|
|
11
|
+
function rangeArgs(argv) {
|
|
12
|
+
const days = Math.min(Math.max(Number(argv.days) || 7, 1), 92);
|
|
13
|
+
const to = argv.to ? new Date(argv.to) : new Date();
|
|
14
|
+
const from = argv.from ? new Date(argv.from) : new Date(to.getTime() - days * 86400000);
|
|
15
|
+
return new URLSearchParams({ from: from.toISOString(), to: to.toISOString() });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const rangeOptions = (y) => y
|
|
19
|
+
.option('days', { type: 'number', default: 7, describe: 'Look-back window in days (max 92)' })
|
|
20
|
+
.option('from', { type: 'string', describe: 'ISO start (overrides --days)' })
|
|
21
|
+
.option('to', { type: 'string', describe: 'ISO end (default now)' });
|
|
22
|
+
|
|
23
|
+
const frictionCommand = {
|
|
24
|
+
command: 'friction-radar',
|
|
25
|
+
describe: 'Topics ranked by estimated agent cost with week-over-week growth and example chats',
|
|
26
|
+
builder: (y) => rangeOptions(y).option('limit', { type: 'number', default: 20 }),
|
|
27
|
+
handler: runCommand(async (argv) => {
|
|
28
|
+
const qs = rangeArgs(argv);
|
|
29
|
+
qs.set('limit', String(argv.limit ?? 20));
|
|
30
|
+
const r = await messagingGet(`/executive-insights/friction-radar?${qs}`);
|
|
31
|
+
console.log(`Friction radar: ${r.conversationsScanned} conversations scanned${r.truncated ? ' (truncated)' : ''}`);
|
|
32
|
+
for (const t of r.topics ?? []) {
|
|
33
|
+
const growth = t.isNew ? 'new' : t.growthPct == null ? 'n/a' : `${t.growthPct}%`;
|
|
34
|
+
console.log(` #${t.costRank} ${t.topic}: ${t.volume} chats (volume rank ${t.volumeRank}, ${growth}), ${t.escalations} escalated, ${t.abandoned} abandoned, ~${Math.round(t.estimatedCostMinutes)} agent min, examples ${t.exampleConversationIds.join(', ') || '-'}`);
|
|
35
|
+
}
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const briefCommand = {
|
|
40
|
+
command: 'brief',
|
|
41
|
+
describe: 'Executive brief: volume, sentiment, QA, Experience Impact, rising keywords, top friction',
|
|
42
|
+
builder: rangeOptions,
|
|
43
|
+
handler: runCommand(async (argv) => {
|
|
44
|
+
const r = await messagingGet(`/executive-insights/brief?${rangeArgs(argv)}`);
|
|
45
|
+
console.log(JSON.stringify(r.brief, null, 2));
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const exportCommand = {
|
|
50
|
+
command: 'export',
|
|
51
|
+
describe: 'CSV of conversations with sentiment, QA score, tag, escalations, handle time and keywords',
|
|
52
|
+
builder: (y) => rangeOptions(y).option('out', { type: 'string', describe: 'Write the CSV to this file (default: stdout)' }),
|
|
53
|
+
handler: runCommand(async (argv) => {
|
|
54
|
+
const r = await messagingGet(`/executive-insights/export?${rangeArgs(argv)}`);
|
|
55
|
+
if (argv.out) {
|
|
56
|
+
writeFileSync(argv.out, r.csv, 'utf8');
|
|
57
|
+
console.log(`Wrote ${r.rows} rows to ${argv.out}${r.truncated ? ' (truncated at the row cap)' : ''}`);
|
|
58
|
+
} else {
|
|
59
|
+
process.stdout.write(r.csv);
|
|
60
|
+
}
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const subscribeCommand = {
|
|
65
|
+
command: 'subscribe <type>',
|
|
66
|
+
describe: 'Email yourself the executive-brief (Monday 7 AM site time by default) or data-export on a schedule',
|
|
67
|
+
builder: (y) => y
|
|
68
|
+
.positional('type', { choices: ['executive-brief', 'data-export'] })
|
|
69
|
+
.option('frequency', { type: 'string', default: 'weekly', choices: ['daily', 'biweekly', 'weekly', 'monthly'] })
|
|
70
|
+
.option('hour', { type: 'number', default: 7, describe: 'Site-local delivery hour 0-23' }),
|
|
71
|
+
handler: runCommand(async (argv) => {
|
|
72
|
+
await messagingPost('/report-subscriptions', {
|
|
73
|
+
reportType: argv.type,
|
|
74
|
+
frequency: argv.frequency,
|
|
75
|
+
deliveryHour: argv.hour,
|
|
76
|
+
});
|
|
77
|
+
console.log(`Subscribed to ${argv.type} (${argv.frequency}, ${argv.hour}:00 site time).`);
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const executiveInsightsCommand = {
|
|
82
|
+
command: 'executive-insights',
|
|
83
|
+
describe: 'Friction radar, executive brief and data export (Chat Intelligence)',
|
|
84
|
+
builder: (y) => y
|
|
85
|
+
.command(frictionCommand)
|
|
86
|
+
.command(briefCommand)
|
|
87
|
+
.command(exportCommand)
|
|
88
|
+
.command(subscribeCommand)
|
|
89
|
+
.demandCommand(1, 'Specify a subcommand. Run "velaro executive-insights --help" for options.'),
|
|
90
|
+
handler: () => {},
|
|
91
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { messagingGet } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
// -- velaro keyword-intelligence ----------------------------------------------------
|
|
5
|
+
// Top customer keywords/phrases and conversation drill-down
|
|
6
|
+
// (velaro-messaging /api/keyword-intelligence/*). Site comes from auth.
|
|
7
|
+
|
|
8
|
+
const print = (data) => console.log(JSON.stringify(data, null, 2));
|
|
9
|
+
|
|
10
|
+
const top = {
|
|
11
|
+
command: 'top',
|
|
12
|
+
describe: 'Top keywords/phrases in a date range',
|
|
13
|
+
builder: (y) => y
|
|
14
|
+
.option('from', { type: 'string', demandOption: true, describe: 'ISO start date' })
|
|
15
|
+
.option('to', { type: 'string', demandOption: true, describe: 'ISO end date' })
|
|
16
|
+
.option('group-by', { type: 'string', default: 'none' })
|
|
17
|
+
.option('ngram', { type: 'number', default: 3 })
|
|
18
|
+
.option('limit', { type: 'number', default: 50 })
|
|
19
|
+
.option('team', { type: 'number' })
|
|
20
|
+
.option('agent', { type: 'number' })
|
|
21
|
+
.option('website', { type: 'string' }),
|
|
22
|
+
handler: runCommand(async (a) => {
|
|
23
|
+
const qs = new URLSearchParams({ from: a.from, to: a.to, groupBy: a.groupBy, ngram: String(a.ngram), limit: String(a.limit) });
|
|
24
|
+
if (a.team != null) qs.set('teamId', String(a.team));
|
|
25
|
+
if (a.agent != null) qs.set('agentId', String(a.agent));
|
|
26
|
+
if (a.website) qs.set('website', a.website);
|
|
27
|
+
print(await messagingGet(`/api/keyword-intelligence/top?${qs}`));
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const conversations = {
|
|
32
|
+
command: 'conversations',
|
|
33
|
+
describe: 'Conversations containing a keyword',
|
|
34
|
+
builder: (y) => y
|
|
35
|
+
.option('keyword', { type: 'string', demandOption: true })
|
|
36
|
+
.option('from', { type: 'string', demandOption: true })
|
|
37
|
+
.option('to', { type: 'string', demandOption: true })
|
|
38
|
+
.option('team', { type: 'number' })
|
|
39
|
+
.option('agent', { type: 'number' }),
|
|
40
|
+
handler: runCommand(async (a) => {
|
|
41
|
+
const qs = new URLSearchParams({ keyword: a.keyword, from: a.from, to: a.to });
|
|
42
|
+
if (a.team != null) qs.set('teamId', String(a.team));
|
|
43
|
+
if (a.agent != null) qs.set('agentId', String(a.agent));
|
|
44
|
+
print(await messagingGet(`/api/keyword-intelligence/conversations?${qs}`));
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const keywordIntelligenceCommand = {
|
|
49
|
+
command: 'keyword-intelligence',
|
|
50
|
+
describe: 'Keyword Intelligence: what customers talk about, with drill-down',
|
|
51
|
+
builder: (y) => y.command(top).command(conversations).demandCommand(1, 'Specify a subcommand. Run "velaro keyword-intelligence --help".'),
|
|
52
|
+
handler: () => {},
|
|
53
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { messagingGet, messagingPost, messagingPut, messagingDel } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
// -- velaro sentiment | quality | chat-intelligence ---------------------------------
|
|
5
|
+
// Report CLI with the optional Website (deploymentId) and team filters, plus QA
|
|
6
|
+
// rubric/alert commands. Endpoints live in velaro-messaging; site comes from auth and
|
|
7
|
+
// deploymentId/teamId are verified server-side against that site.
|
|
8
|
+
|
|
9
|
+
const print = (data) => console.log(JSON.stringify(data, null, 2));
|
|
10
|
+
const scope = (y) => y
|
|
11
|
+
.option('deployment', { type: 'number', describe: 'Website (deployment row id)' })
|
|
12
|
+
.option('team', { type: 'number', describe: 'Team id' });
|
|
13
|
+
|
|
14
|
+
const SENTIMENT_ROUTES = {
|
|
15
|
+
overview: 'sentiment-overview',
|
|
16
|
+
breakdown: 'sentiment-breakdown',
|
|
17
|
+
'conversation-overview': 'conversation-sentiment-overview',
|
|
18
|
+
'conversation-breakdown': 'conversation-sentiment-breakdown',
|
|
19
|
+
'contact-overview': 'contact-sentiment-overview',
|
|
20
|
+
'agent-breakdown': 'agent-sentiment-breakdown',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const sentimentCommand = {
|
|
24
|
+
command: 'sentiment [kind]',
|
|
25
|
+
describe: 'Sentiment report, optionally per website/team',
|
|
26
|
+
builder: (y) => scope(y
|
|
27
|
+
.positional('kind', { choices: Object.keys(SENTIMENT_ROUTES), default: 'overview' })
|
|
28
|
+
.option('from', { type: 'string' })
|
|
29
|
+
.option('to', { type: 'string' })
|
|
30
|
+
.option('group-by', { type: 'string', default: 'day' })),
|
|
31
|
+
handler: runCommand(async (a) => {
|
|
32
|
+
const end = a.to ? new Date(a.to) : new Date();
|
|
33
|
+
const start = a.from ? new Date(a.from) : new Date(end.getTime() - 30 * 86400000);
|
|
34
|
+
const body = { Start: start.toISOString(), End: end.toISOString(), GroupBy: a.groupBy };
|
|
35
|
+
if (a.deployment != null) body.DeploymentId = a.deployment;
|
|
36
|
+
if (a.team != null) body.TeamId = a.team;
|
|
37
|
+
print(await messagingPost(`/Reports/${SENTIMENT_ROUTES[a.kind]}`, body));
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const agentPerformance = {
|
|
42
|
+
command: 'agent-performance',
|
|
43
|
+
describe: 'AI quality scores per agent, optionally per website/team',
|
|
44
|
+
builder: (y) => scope(y.option('days', { type: 'number', default: 30 })),
|
|
45
|
+
handler: runCommand(async (a) => {
|
|
46
|
+
const qs = new URLSearchParams({ days: String(a.days) });
|
|
47
|
+
if (a.deployment != null) qs.set('deploymentId', String(a.deployment));
|
|
48
|
+
if (a.team != null) qs.set('teamId', String(a.team));
|
|
49
|
+
print(await messagingGet(`/api/admin/Reports/AgentPerformance?${qs}`));
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const lowScores = {
|
|
54
|
+
command: 'low-scores',
|
|
55
|
+
describe: 'Conversations below the QA threshold',
|
|
56
|
+
builder: (y) => y.option('threshold', { type: 'number' }).option('days', { type: 'number', default: 30 }).option('agent', { type: 'number' }).option('take', { type: 'number', default: 50 }),
|
|
57
|
+
handler: runCommand(async (a) => {
|
|
58
|
+
const qs = new URLSearchParams({ days: String(a.days), take: String(a.take) });
|
|
59
|
+
if (a.threshold != null) qs.set('threshold', String(a.threshold));
|
|
60
|
+
if (a.agent != null) qs.set('agentId', String(a.agent));
|
|
61
|
+
print(await messagingGet(`/api/admin/QualityQa/low-scores?${qs}`));
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const calibration = {
|
|
66
|
+
command: 'calibration',
|
|
67
|
+
describe: 'AI score vs human review calibration',
|
|
68
|
+
builder: (y) => y.option('days', { type: 'number', default: 90 }),
|
|
69
|
+
handler: runCommand(async (a) => print(await messagingGet(`/api/admin/QualityQa/calibration?days=${a.days}`))),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const rubric = {
|
|
73
|
+
command: 'rubric <action>',
|
|
74
|
+
describe: 'QA rubrics: list | get | default | create | update | delete (--json for the rubric body)',
|
|
75
|
+
builder: (y) => y.positional('action', { choices: ['list', 'get', 'default', 'create', 'update', 'delete'] })
|
|
76
|
+
.option('id', { type: 'number' }).option('json', { type: 'string', describe: 'Rubric JSON body' }),
|
|
77
|
+
handler: runCommand(async (a) => {
|
|
78
|
+
const base = '/api/admin/QualityQa/rubrics';
|
|
79
|
+
const body = a.json ? JSON.parse(a.json) : {};
|
|
80
|
+
if (a.action === 'list') return print(await messagingGet(base));
|
|
81
|
+
if (a.action === 'default') return print(await messagingGet(`${base}/default`));
|
|
82
|
+
if (a.action === 'create') return print(await messagingPost(base, body));
|
|
83
|
+
if (a.id == null) throw new Error('--id is required');
|
|
84
|
+
if (a.action === 'get') return print(await messagingGet(`${base}/${a.id}`));
|
|
85
|
+
if (a.action === 'update') return print(await messagingPut(`${base}/${a.id}`, body));
|
|
86
|
+
print(await messagingDel(`${base}/${a.id}`));
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const alerts = {
|
|
91
|
+
command: 'alerts',
|
|
92
|
+
describe: 'Quality alert settings (read; pass --json to update)',
|
|
93
|
+
builder: (y) => y.option('json', { type: 'string' }),
|
|
94
|
+
handler: runCommand(async (a) => {
|
|
95
|
+
const path = '/api/admin/QualityQa/alert-settings';
|
|
96
|
+
print(a.json ? await messagingPut(path, JSON.parse(a.json)) : await messagingGet(path));
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const qualityCommand = {
|
|
101
|
+
command: 'quality',
|
|
102
|
+
describe: 'Conversation Quality: agent performance, QA rubrics, low scores, calibration, alerts',
|
|
103
|
+
builder: (y) => y.command(agentPerformance).command(lowScores).command(calibration).command(rubric).command(alerts)
|
|
104
|
+
.demandCommand(1, 'Specify a subcommand. Run "velaro quality --help".'),
|
|
105
|
+
handler: () => {},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const chatIntelligenceThemesCommand = {
|
|
109
|
+
command: 'chat-intelligence',
|
|
110
|
+
describe: 'Chat Intelligence themes, optionally per website/team',
|
|
111
|
+
builder: (y) => scope(y.option('from', { type: 'string' }).option('to', { type: 'string' }).option('group-by', { type: 'string', default: 'month' })),
|
|
112
|
+
handler: runCommand(async (a) => {
|
|
113
|
+
const qs = new URLSearchParams({ groupBy: a.groupBy });
|
|
114
|
+
if (a.from) qs.set('startDate', a.from);
|
|
115
|
+
if (a.to) qs.set('endDate', a.to);
|
|
116
|
+
if (a.deployment != null) qs.set('deploymentId', String(a.deployment));
|
|
117
|
+
if (a.team != null) qs.set('teamId', String(a.team));
|
|
118
|
+
print(await messagingGet(`/Reports/chat-intelligence/themes?${qs}`));
|
|
119
|
+
}),
|
|
120
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { messagingGet, messagingPost, messagingPut, messagingDel } from '../api.js';
|
|
2
|
+
import { runCommand } from '../run.js';
|
|
3
|
+
|
|
4
|
+
// -- velaro skill-routing ---------------------------------------------------------
|
|
5
|
+
// CLI for skill-based routing (velaro-messaging SkillRoutingController, route
|
|
6
|
+
// /SkillRouting). Site comes from the caller's auth, never from CLI args.
|
|
7
|
+
// Gated by the EnableSkillBasedRouting entitlement (403 when not entitled).
|
|
8
|
+
// Requirements are disabled by default and PUT is partial (omitted fields unchanged).
|
|
9
|
+
|
|
10
|
+
const print = (data) => console.log(JSON.stringify(data, null, 2));
|
|
11
|
+
const ids = (v) => (v == null ? undefined : String(v).split(',').map((x) => Number(x.trim())).filter((n) => !Number.isNaN(n)));
|
|
12
|
+
const strip = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
|
|
13
|
+
|
|
14
|
+
function crud(resource, noun, addOpts, toBody) {
|
|
15
|
+
return {
|
|
16
|
+
command: `${resource.toLowerCase()} <action>`,
|
|
17
|
+
describe: `${noun}: list | create | update | delete`,
|
|
18
|
+
builder: (y) => addOpts(y.positional('action', { choices: ['list', 'create', 'update', 'delete'] }).option('id', { type: 'number', describe: 'Row id (update/delete)' })),
|
|
19
|
+
handler: runCommand(async (argv) => {
|
|
20
|
+
const base = `/SkillRouting/${resource}`;
|
|
21
|
+
if (argv.action === 'list') return print(await messagingGet(base));
|
|
22
|
+
if (argv.action === 'create') return print(await messagingPost(base, toBody(argv)));
|
|
23
|
+
if (argv.id == null) throw new Error('--id is required for update/delete');
|
|
24
|
+
if (argv.action === 'update') return print(await messagingPut(`${base}/${argv.id}`, toBody(argv)));
|
|
25
|
+
print(await messagingDel(`${base}/${argv.id}`));
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const skills = crud('Skills', 'Skill catalog',
|
|
31
|
+
(y) => y.option('name', { type: 'string' }).option('description', { type: 'string' }),
|
|
32
|
+
(a) => strip({ Name: a.name, Description: a.description }));
|
|
33
|
+
|
|
34
|
+
const assignments = crud('Assignments', 'Agent skill assignments',
|
|
35
|
+
(y) => y.option('agent', { type: 'number', describe: 'workspaceUserId' }).option('skill', { type: 'number', describe: 'agentSkillId' }).option('proficiency', { type: 'number', describe: '1-5' }),
|
|
36
|
+
(a) => strip({ WorkspaceUserId: a.agent, AgentSkillId: a.skill, Proficiency: a.proficiency }));
|
|
37
|
+
|
|
38
|
+
const requirements = crud('Requirements', 'Required/preferred skills per deployment/team',
|
|
39
|
+
(y) => y.option('deployment', { type: 'number' }).option('team', { type: 'number' })
|
|
40
|
+
.option('required', { type: 'string', describe: 'Comma-separated skill ids' })
|
|
41
|
+
.option('preferred', { type: 'string', describe: 'Comma-separated skill ids' })
|
|
42
|
+
.option('overflow-wait', { type: 'number', describe: 'Seconds before falling back to all agents' })
|
|
43
|
+
.option('enabled', { type: 'boolean', describe: 'Turn the requirement on/off (default off)' }),
|
|
44
|
+
(a) => strip({ DeploymentId: a.deployment, TeamId: a.team, RequiredSkillIds: ids(a.required), PreferredSkillIds: ids(a.preferred), OverflowWaitSeconds: a.overflowWait, IsEnabled: a.enabled }));
|
|
45
|
+
|
|
46
|
+
const simulate = {
|
|
47
|
+
command: 'simulate',
|
|
48
|
+
describe: 'Dry-run skill routing for a team (never assigns, never writes the decision log)',
|
|
49
|
+
builder: (y) => y.option('team', { type: 'number' }).option('waited', { type: 'number', describe: 'Seconds the conversation has waited' }),
|
|
50
|
+
handler: runCommand(async (argv) => {
|
|
51
|
+
print(await messagingPost('/SkillRouting/Simulate', { TeamId: argv.team ?? null, WaitedSeconds: argv.waited ?? null }));
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const skillRoutingCommand = {
|
|
56
|
+
command: 'skill-routing',
|
|
57
|
+
describe: 'Skill-based routing: skills, agent assignments, requirements, simulator',
|
|
58
|
+
builder: (y) => y.command(skills).command(assignments).command(requirements).command(simulate)
|
|
59
|
+
.demandCommand(1, 'Specify a subcommand. Run "velaro skill-routing --help".'),
|
|
60
|
+
handler: () => {},
|
|
61
|
+
};
|
package/package.json
CHANGED