@chanl/eval-cli 0.4.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/LICENSE +21 -0
- package/dist/agent-loader.d.ts +39 -0
- package/dist/agent-loader.d.ts.map +1 -0
- package/dist/agent-loader.js +166 -0
- package/dist/agent-loader.js.map +1 -0
- package/dist/analytics.d.ts +15 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +94 -0
- package/dist/analytics.js.map +1 -0
- package/dist/assertions.d.ts +73 -0
- package/dist/assertions.d.ts.map +1 -0
- package/dist/assertions.js +282 -0
- package/dist/assertions.js.map +1 -0
- package/dist/baseline.d.ts +100 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +327 -0
- package/dist/baseline.js.map +1 -0
- package/dist/bin/chanl.d.ts +3 -0
- package/dist/bin/chanl.d.ts.map +1 -0
- package/dist/bin/chanl.js +11 -0
- package/dist/bin/chanl.js.map +1 -0
- package/dist/client.d.ts +40 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +99 -0
- package/dist/client.js.map +1 -0
- package/dist/commands/analytics.d.ts +3 -0
- package/dist/commands/analytics.d.ts.map +1 -0
- package/dist/commands/analytics.js +44 -0
- package/dist/commands/analytics.js.map +1 -0
- package/dist/commands/compare.d.ts +51 -0
- package/dist/commands/compare.d.ts.map +1 -0
- package/dist/commands/compare.js +429 -0
- package/dist/commands/compare.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +94 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/dataset.d.ts +6 -0
- package/dist/commands/dataset.d.ts.map +1 -0
- package/dist/commands/dataset.js +225 -0
- package/dist/commands/dataset.js.map +1 -0
- package/dist/commands/executions.d.ts +3 -0
- package/dist/commands/executions.d.ts.map +1 -0
- package/dist/commands/executions.js +249 -0
- package/dist/commands/executions.js.map +1 -0
- package/dist/commands/generate.d.ts +3 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +159 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +29 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +545 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +65 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/personas.d.ts +3 -0
- package/dist/commands/personas.d.ts.map +1 -0
- package/dist/commands/personas.js +269 -0
- package/dist/commands/personas.js.map +1 -0
- package/dist/commands/scenarios.d.ts +16 -0
- package/dist/commands/scenarios.d.ts.map +1 -0
- package/dist/commands/scenarios.js +755 -0
- package/dist/commands/scenarios.js.map +1 -0
- package/dist/commands/scorecards.d.ts +3 -0
- package/dist/commands/scorecards.d.ts.map +1 -0
- package/dist/commands/scorecards.js +220 -0
- package/dist/commands/scorecards.js.map +1 -0
- package/dist/commands/server.d.ts +8 -0
- package/dist/commands/server.d.ts.map +1 -0
- package/dist/commands/server.js +357 -0
- package/dist/commands/server.js.map +1 -0
- package/dist/commands/test.d.ts +3 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +410 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/tool-fixtures.d.ts +3 -0
- package/dist/commands/tool-fixtures.d.ts.map +1 -0
- package/dist/commands/tool-fixtures.js +324 -0
- package/dist/commands/tool-fixtures.js.map +1 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +132 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +146 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +30 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +77 -0
- package/dist/output.js.map +1 -0
- package/dist/update-check.d.ts +6 -0
- package/dist/update-check.d.ts.map +1 -0
- package/dist/update-check.js +50 -0
- package/dist/update-check.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.runScenarioAction = runScenarioAction;
|
|
40
|
+
exports.registerScenariosCommand = registerScenariosCommand;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const yaml = __importStar(require("js-yaml"));
|
|
44
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
45
|
+
const ora_1 = __importDefault(require("ora"));
|
|
46
|
+
const client_1 = require("../client");
|
|
47
|
+
const output_1 = require("../output");
|
|
48
|
+
/**
|
|
49
|
+
* Shared run action — used by both `chanl scenarios run` and `chanl run`.
|
|
50
|
+
*/
|
|
51
|
+
async function runScenarioAction(idOrNameOrFile, options, format) {
|
|
52
|
+
// Handle --all: run all active scenarios
|
|
53
|
+
if (options.all || !idOrNameOrFile) {
|
|
54
|
+
const result = await (0, client_1.get)('/scenarios', { status: 'active', limit: '100' });
|
|
55
|
+
const items = result.scenarios || [];
|
|
56
|
+
if (items.length === 0) {
|
|
57
|
+
(0, output_1.printError)('No active scenarios found. Create one with: chanl scenarios create');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
console.log(`Running ${items.length} scenario(s)...\n`);
|
|
61
|
+
for (const scenario of items) {
|
|
62
|
+
const sid = scenario.id || scenario._id;
|
|
63
|
+
console.log(chalk_1.default.bold(`→ ${scenario.name}`));
|
|
64
|
+
try {
|
|
65
|
+
await executeAndPoll(sid, options, format);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
(0, output_1.printError)(` ${(0, client_1.formatError)(err)}`);
|
|
69
|
+
}
|
|
70
|
+
console.log('');
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
let scenarioId = idOrNameOrFile;
|
|
75
|
+
// Check if the argument is a YAML file path
|
|
76
|
+
if (idOrNameOrFile.endsWith('.yaml') || idOrNameOrFile.endsWith('.yml')) {
|
|
77
|
+
const filePath = path.resolve(idOrNameOrFile);
|
|
78
|
+
if (!fs.existsSync(filePath)) {
|
|
79
|
+
(0, output_1.printError)(`File not found: ${filePath}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const spinner = (0, ora_1.default)('Importing scenario from YAML...').start();
|
|
83
|
+
try {
|
|
84
|
+
const yamlContent = fs.readFileSync(filePath, 'utf-8');
|
|
85
|
+
yaml.load(yamlContent);
|
|
86
|
+
const importResult = await (0, client_1.post)('/scenarios/import/yaml', {
|
|
87
|
+
yaml: yamlContent,
|
|
88
|
+
});
|
|
89
|
+
const imported = importResult.scenario;
|
|
90
|
+
scenarioId = imported.id || imported._id;
|
|
91
|
+
spinner.succeed(`Imported scenario: ${imported.name} (${scenarioId})`);
|
|
92
|
+
}
|
|
93
|
+
catch (importErr) {
|
|
94
|
+
spinner.fail('Failed to import YAML');
|
|
95
|
+
(0, output_1.printError)((0, client_1.formatError)(importErr));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (!isObjectId(idOrNameOrFile)) {
|
|
100
|
+
// Try to find scenario by name (slug match)
|
|
101
|
+
const result = await (0, client_1.get)('/scenarios', { limit: '200' });
|
|
102
|
+
const items = result.scenarios || [];
|
|
103
|
+
const match = items.find((s) => slugify(s.name) === slugify(idOrNameOrFile) ||
|
|
104
|
+
s.name.toLowerCase() === idOrNameOrFile.toLowerCase());
|
|
105
|
+
if (match) {
|
|
106
|
+
scenarioId = match.id || match._id;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
(0, output_1.printError)(`Scenario "${idOrNameOrFile}" not found. Run "chanl scenarios list" to see available scenarios.`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
await executeAndPoll(scenarioId, options, format);
|
|
114
|
+
}
|
|
115
|
+
async function executeAndPoll(scenarioId, options, format) {
|
|
116
|
+
const executeDto = {};
|
|
117
|
+
// promptId is required — the Prompt entity + server Settings are the only config source
|
|
118
|
+
if (!options.promptId) {
|
|
119
|
+
const msg = 'Missing --prompt-id. The server resolves adapter config from the Prompt entity.\n' +
|
|
120
|
+
'Create a prompt in the dashboard or via the API, then pass its ID:\n' +
|
|
121
|
+
' chanl scenarios run <scenario> --prompt-id <id>';
|
|
122
|
+
(0, output_1.printError)(msg);
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
throw new Error(msg);
|
|
125
|
+
}
|
|
126
|
+
executeDto.promptId = options.promptId;
|
|
127
|
+
if (options.personaId)
|
|
128
|
+
executeDto.personaId = options.personaId;
|
|
129
|
+
if (options.scorecardId)
|
|
130
|
+
executeDto.scorecardId = options.scorecardId;
|
|
131
|
+
if (options.mode)
|
|
132
|
+
executeDto.mode = options.mode;
|
|
133
|
+
if (options.dryRun)
|
|
134
|
+
executeDto.dryRun = true;
|
|
135
|
+
if (options.tools) {
|
|
136
|
+
executeDto.toolFixtureIds = options.tools
|
|
137
|
+
.split(',')
|
|
138
|
+
.map((id) => id.trim())
|
|
139
|
+
.filter(Boolean);
|
|
140
|
+
}
|
|
141
|
+
console.log(chalk_1.default.dim(`Using prompt: ${options.promptId}`));
|
|
142
|
+
const spinner = (0, ora_1.default)('Starting scenario execution...').start();
|
|
143
|
+
const execResult = await (0, client_1.post)(`/scenarios/${scenarioId}/execute`, executeDto);
|
|
144
|
+
const execution = execResult.execution;
|
|
145
|
+
const executionId = execution.executionId || execution.id || execution._id;
|
|
146
|
+
spinner.succeed(`Execution started: ${executionId}`);
|
|
147
|
+
if (options.wait !== false) {
|
|
148
|
+
await pollExecution(executionId, format);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
console.log(`\nCheck status with: chanl scenarios results ${executionId}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function registerScenariosCommand(program) {
|
|
155
|
+
const scenarios = program
|
|
156
|
+
.command('scenarios')
|
|
157
|
+
.description('Manage evaluation scenarios');
|
|
158
|
+
// --- list ---
|
|
159
|
+
scenarios
|
|
160
|
+
.command('list')
|
|
161
|
+
.description('List all scenarios')
|
|
162
|
+
.option('--status <status>', 'Filter by status')
|
|
163
|
+
.option('--category <category>', 'Filter by category')
|
|
164
|
+
.option('--page <page>', 'Page number', '1')
|
|
165
|
+
.option('--limit <limit>', 'Results per page', '20')
|
|
166
|
+
.action(async (options) => {
|
|
167
|
+
try {
|
|
168
|
+
const params = {
|
|
169
|
+
page: options.page,
|
|
170
|
+
limit: options.limit,
|
|
171
|
+
};
|
|
172
|
+
if (options.status)
|
|
173
|
+
params.status = options.status;
|
|
174
|
+
if (options.category)
|
|
175
|
+
params.category = options.category;
|
|
176
|
+
const result = await (0, client_1.get)('/scenarios', params);
|
|
177
|
+
const items = result.scenarios || [];
|
|
178
|
+
const format = program.opts().format;
|
|
179
|
+
const headers = [
|
|
180
|
+
'ID',
|
|
181
|
+
'Name',
|
|
182
|
+
'Category',
|
|
183
|
+
'Difficulty',
|
|
184
|
+
'Status',
|
|
185
|
+
'Personas',
|
|
186
|
+
];
|
|
187
|
+
const rows = items.map((s) => [
|
|
188
|
+
s.id || s._id || '',
|
|
189
|
+
(0, output_1.truncate)(s.name || '', 30),
|
|
190
|
+
s.category || '',
|
|
191
|
+
s.difficulty || '',
|
|
192
|
+
s.status || '',
|
|
193
|
+
String((s.personaIds || []).length),
|
|
194
|
+
]);
|
|
195
|
+
(0, output_1.printOutput)(format, headers, rows, result);
|
|
196
|
+
if (format !== 'json' && result.pagination) {
|
|
197
|
+
const p = result.pagination;
|
|
198
|
+
console.log(chalk_1.default.dim(`\nShowing page ${p.page}/${p.totalPages} (${result.total} total)`));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
(0, output_1.printError)((0, client_1.formatError)(err));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
// --- run ---
|
|
207
|
+
scenarios
|
|
208
|
+
.command('run [idOrFile]')
|
|
209
|
+
.description('Execute a scenario by ID, name, or from a YAML file')
|
|
210
|
+
.requiredOption('--prompt-id <promptId>', 'Prompt entity ID (defines the agent under test)')
|
|
211
|
+
.option('--persona-id <personaId>', 'Override persona ID')
|
|
212
|
+
.option('--scorecard-id <scorecardId>', 'Override scorecard ID')
|
|
213
|
+
.option('--tools <ids>', 'Comma-separated tool fixture IDs to attach')
|
|
214
|
+
.option('--mode <mode>', 'Execution mode: text or phone', 'text')
|
|
215
|
+
.option('--dry-run', 'Dry run without actually executing')
|
|
216
|
+
.option('--no-wait', 'Do not wait for completion')
|
|
217
|
+
.option('--all', 'Run all active scenarios')
|
|
218
|
+
.action(async (idOrFile, options) => {
|
|
219
|
+
try {
|
|
220
|
+
await runScenarioAction(idOrFile, options, program.opts().format);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
(0, output_1.printError)((0, client_1.formatError)(err));
|
|
224
|
+
process.exitCode = 1;
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
// --- create ---
|
|
229
|
+
scenarios
|
|
230
|
+
.command('create')
|
|
231
|
+
.description('Create a new scenario')
|
|
232
|
+
.requiredOption('--name <name>', 'Scenario name')
|
|
233
|
+
.requiredOption('--prompt <prompt>', 'Opening prompt for the persona')
|
|
234
|
+
.option('--description <description>', 'Description')
|
|
235
|
+
.option('--category <category>', 'Category (support, sales, booking, technical, onboarding, feedback)', 'support')
|
|
236
|
+
.option('--difficulty <difficulty>', 'Difficulty (easy, medium, hard)', 'medium')
|
|
237
|
+
.option('--persona-ids <ids>', 'Comma-separated persona IDs')
|
|
238
|
+
.option('--scorecard-id <id>', 'Scorecard ID')
|
|
239
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
240
|
+
.option('--status <status>', 'Status (draft, active)', 'draft')
|
|
241
|
+
.action(async (options) => {
|
|
242
|
+
try {
|
|
243
|
+
const dto = {
|
|
244
|
+
name: options.name,
|
|
245
|
+
prompt: options.prompt,
|
|
246
|
+
category: options.category,
|
|
247
|
+
difficulty: options.difficulty,
|
|
248
|
+
status: options.status,
|
|
249
|
+
personaIds: options.personaIds
|
|
250
|
+
? options.personaIds.split(',').map((s) => s.trim())
|
|
251
|
+
: [],
|
|
252
|
+
};
|
|
253
|
+
if (options.description)
|
|
254
|
+
dto.description = options.description;
|
|
255
|
+
if (options.scorecardId)
|
|
256
|
+
dto.scorecardId = options.scorecardId;
|
|
257
|
+
if (options.tags)
|
|
258
|
+
dto.tags = options.tags.split(',').map((t) => t.trim());
|
|
259
|
+
const result = await (0, client_1.post)('/scenarios', dto);
|
|
260
|
+
const scenario = result.scenario || result;
|
|
261
|
+
const format = program.opts().format;
|
|
262
|
+
if (format === 'json') {
|
|
263
|
+
console.log(JSON.stringify(result, null, 2));
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
(0, output_1.printSuccess)(`Created scenario: ${scenario.name} (${scenario.id || scenario._id})`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
(0, output_1.printError)((0, client_1.formatError)(err));
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
// --- get ---
|
|
275
|
+
scenarios
|
|
276
|
+
.command('get <id>')
|
|
277
|
+
.description('Get scenario details')
|
|
278
|
+
.action(async (id) => {
|
|
279
|
+
try {
|
|
280
|
+
const result = await (0, client_1.get)(`/scenarios/${id}`);
|
|
281
|
+
const format = program.opts().format;
|
|
282
|
+
if (format === 'json') {
|
|
283
|
+
console.log(JSON.stringify(result, null, 2));
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
const s = result.scenario;
|
|
287
|
+
console.log(`Name: ${s.name}`);
|
|
288
|
+
console.log(`ID: ${s.id || s._id}`);
|
|
289
|
+
console.log(`Category: ${s.category || '-'}`);
|
|
290
|
+
console.log(`Difficulty: ${s.difficulty || '-'}`);
|
|
291
|
+
console.log(`Status: ${s.status || '-'}`);
|
|
292
|
+
console.log(`Personas: ${(s.personaIds || []).length}`);
|
|
293
|
+
if (s.description) {
|
|
294
|
+
console.log(`Description: ${s.description}`);
|
|
295
|
+
}
|
|
296
|
+
if (s.prompt) {
|
|
297
|
+
console.log(`Prompt: ${(0, output_1.truncate)(s.prompt, 80)}`);
|
|
298
|
+
}
|
|
299
|
+
if (s.tags && s.tags.length > 0) {
|
|
300
|
+
console.log(`Tags: ${s.tags.join(', ')}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
(0, output_1.printError)((0, client_1.formatError)(err));
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
// --- update ---
|
|
310
|
+
scenarios
|
|
311
|
+
.command('update <id>')
|
|
312
|
+
.description('Update a scenario')
|
|
313
|
+
.option('--name <name>', 'New name')
|
|
314
|
+
.option('--description <description>', 'New description')
|
|
315
|
+
.option('--prompt <prompt>', 'New opening prompt')
|
|
316
|
+
.option('--category <category>', 'New category')
|
|
317
|
+
.option('--difficulty <difficulty>', 'New difficulty')
|
|
318
|
+
.option('--status <status>', 'New status (draft, active)')
|
|
319
|
+
.option('--persona-ids <ids>', 'Comma-separated persona IDs')
|
|
320
|
+
.option('--scorecard-id <id>', 'New scorecard ID')
|
|
321
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
322
|
+
.action(async (id, options) => {
|
|
323
|
+
try {
|
|
324
|
+
const dto = {};
|
|
325
|
+
if (options.name)
|
|
326
|
+
dto.name = options.name;
|
|
327
|
+
if (options.description)
|
|
328
|
+
dto.description = options.description;
|
|
329
|
+
if (options.prompt)
|
|
330
|
+
dto.prompt = options.prompt;
|
|
331
|
+
if (options.category)
|
|
332
|
+
dto.category = options.category;
|
|
333
|
+
if (options.difficulty)
|
|
334
|
+
dto.difficulty = options.difficulty;
|
|
335
|
+
if (options.status)
|
|
336
|
+
dto.status = options.status;
|
|
337
|
+
if (options.scorecardId)
|
|
338
|
+
dto.scorecardId = options.scorecardId;
|
|
339
|
+
if (options.personaIds) {
|
|
340
|
+
dto.personaIds = options.personaIds.split(',').map((s) => s.trim());
|
|
341
|
+
}
|
|
342
|
+
if (options.tags) {
|
|
343
|
+
dto.tags = options.tags.split(',').map((t) => t.trim());
|
|
344
|
+
}
|
|
345
|
+
const result = await (0, client_1.patch)(`/scenarios/${id}`, dto);
|
|
346
|
+
const scenario = result.scenario || result;
|
|
347
|
+
const format = program.opts().format;
|
|
348
|
+
if (format === 'json') {
|
|
349
|
+
console.log(JSON.stringify(result, null, 2));
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
(0, output_1.printSuccess)(`Updated scenario: ${scenario.name} (${scenario.id || scenario._id})`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
(0, output_1.printError)((0, client_1.formatError)(err));
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
// --- delete ---
|
|
361
|
+
scenarios
|
|
362
|
+
.command('delete <id>')
|
|
363
|
+
.description('Delete a scenario')
|
|
364
|
+
.option('--force', 'Skip confirmation prompt')
|
|
365
|
+
.action(async (id, options) => {
|
|
366
|
+
try {
|
|
367
|
+
if (!options.force) {
|
|
368
|
+
const inquirer = await Promise.resolve().then(() => __importStar(require('inquirer')));
|
|
369
|
+
const answers = await inquirer.default.prompt([
|
|
370
|
+
{
|
|
371
|
+
type: 'confirm',
|
|
372
|
+
name: 'confirm',
|
|
373
|
+
message: `Delete scenario ${id}?`,
|
|
374
|
+
default: false,
|
|
375
|
+
},
|
|
376
|
+
]);
|
|
377
|
+
if (!answers.confirm) {
|
|
378
|
+
console.log('Cancelled.');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
await (0, client_1.del)(`/scenarios/${id}`);
|
|
383
|
+
(0, output_1.printSuccess)(`Deleted scenario: ${id}`);
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
(0, output_1.printError)((0, client_1.formatError)(err));
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
// --- import ---
|
|
391
|
+
scenarios
|
|
392
|
+
.command('import <dirOrFile>')
|
|
393
|
+
.description('Import scenarios from a YAML file or directory')
|
|
394
|
+
.action(async (dirOrFile) => {
|
|
395
|
+
try {
|
|
396
|
+
const resolvedPath = path.resolve(dirOrFile);
|
|
397
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
398
|
+
(0, output_1.printError)(`Path not found: ${resolvedPath}`);
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
const stat = fs.statSync(resolvedPath);
|
|
402
|
+
let files;
|
|
403
|
+
if (stat.isDirectory()) {
|
|
404
|
+
files = fs
|
|
405
|
+
.readdirSync(resolvedPath)
|
|
406
|
+
.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'))
|
|
407
|
+
.map((f) => path.join(resolvedPath, f));
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
files = [resolvedPath];
|
|
411
|
+
}
|
|
412
|
+
if (files.length === 0) {
|
|
413
|
+
(0, output_1.printError)('No YAML files found');
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
416
|
+
console.log(`Importing ${files.length} scenario file(s)...\n`);
|
|
417
|
+
let successCount = 0;
|
|
418
|
+
let failCount = 0;
|
|
419
|
+
for (const file of files) {
|
|
420
|
+
const basename = path.basename(file);
|
|
421
|
+
const spinner = (0, ora_1.default)(`Importing ${basename}...`).start();
|
|
422
|
+
try {
|
|
423
|
+
const yamlContent = fs.readFileSync(file, 'utf-8');
|
|
424
|
+
yaml.load(yamlContent);
|
|
425
|
+
const result = await (0, client_1.post)('/scenarios/import/yaml', {
|
|
426
|
+
yaml: yamlContent,
|
|
427
|
+
});
|
|
428
|
+
const scenario = result.scenario;
|
|
429
|
+
spinner.succeed(`${basename} -> ${scenario.name} (${scenario.id || scenario._id})`);
|
|
430
|
+
successCount++;
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
spinner.fail(`${basename}: ${(0, client_1.formatError)(err)}`);
|
|
434
|
+
failCount++;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
console.log(`\nImported: ${chalk_1.default.green(String(successCount))} | Failed: ${chalk_1.default.red(String(failCount))}`);
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
(0, output_1.printError)((0, client_1.formatError)(err));
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
// --- results ---
|
|
445
|
+
scenarios
|
|
446
|
+
.command('results <executionId>')
|
|
447
|
+
.description('View execution results')
|
|
448
|
+
.action(async (executionId) => {
|
|
449
|
+
try {
|
|
450
|
+
const result = await (0, client_1.get)(`/scenarios/executions/${executionId}`);
|
|
451
|
+
const execution = result.execution;
|
|
452
|
+
const format = program.opts().format;
|
|
453
|
+
if (format === 'json') {
|
|
454
|
+
console.log(JSON.stringify(result, null, 2));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
printExecutionDetails(execution);
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
460
|
+
(0, output_1.printError)((0, client_1.formatError)(err));
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Poll an execution until it reaches a terminal state.
|
|
467
|
+
*/
|
|
468
|
+
async function pollExecution(executionId, format) {
|
|
469
|
+
const TERMINAL_STATES = ['completed', 'failed', 'timeout', 'cancelled'];
|
|
470
|
+
const POLL_INTERVAL = 2000;
|
|
471
|
+
const MAX_POLLS = 150;
|
|
472
|
+
const spinner = (0, ora_1.default)('Waiting for execution to complete...').start();
|
|
473
|
+
for (let i = 0; i < MAX_POLLS; i++) {
|
|
474
|
+
await sleep(POLL_INTERVAL);
|
|
475
|
+
try {
|
|
476
|
+
const result = await (0, client_1.get)(`/scenarios/executions/${executionId}`);
|
|
477
|
+
const execution = result.execution;
|
|
478
|
+
spinner.text = `Status: ${execution.status}`;
|
|
479
|
+
if (execution.metrics) {
|
|
480
|
+
const pct = Math.round(((execution.metrics.completedSteps || 0) /
|
|
481
|
+
(execution.metrics.totalSteps || 1)) *
|
|
482
|
+
100);
|
|
483
|
+
spinner.text = `Status: ${execution.status} (${pct}% steps done)`;
|
|
484
|
+
}
|
|
485
|
+
if (TERMINAL_STATES.includes(execution.status)) {
|
|
486
|
+
if (execution.status === 'completed') {
|
|
487
|
+
spinner.succeed('Execution completed');
|
|
488
|
+
}
|
|
489
|
+
else if (execution.status === 'failed') {
|
|
490
|
+
spinner.fail('Execution failed');
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
spinner.warn(`Execution ${execution.status}`);
|
|
494
|
+
}
|
|
495
|
+
console.log('');
|
|
496
|
+
if (format === 'json') {
|
|
497
|
+
console.log(JSON.stringify(result, null, 2));
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
printExecutionDetails(execution);
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
spinner.text = `Polling... (${(0, client_1.formatError)(err)})`;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
spinner.warn('Polling timeout reached');
|
|
510
|
+
console.log(`Check status with: chanl scenarios results ${executionId}`);
|
|
511
|
+
}
|
|
512
|
+
function printExecutionDetails(execution) {
|
|
513
|
+
const statusColor = execution.status === 'completed'
|
|
514
|
+
? chalk_1.default.green
|
|
515
|
+
: execution.status === 'failed'
|
|
516
|
+
? chalk_1.default.red
|
|
517
|
+
: chalk_1.default.yellow;
|
|
518
|
+
console.log(chalk_1.default.bold('Execution Results'));
|
|
519
|
+
console.log(chalk_1.default.dim('\u2500'.repeat(60)));
|
|
520
|
+
console.log(` ID: ${execution.executionId || execution.id || ''}`);
|
|
521
|
+
console.log(` Status: ${statusColor(execution.status)}`);
|
|
522
|
+
if (execution.duration) {
|
|
523
|
+
console.log(` Duration: ${(execution.duration / 1000).toFixed(1)}s`);
|
|
524
|
+
}
|
|
525
|
+
if (execution.startTime) {
|
|
526
|
+
console.log(` Started: ${new Date(execution.startTime).toLocaleString()}`);
|
|
527
|
+
}
|
|
528
|
+
// -- Transcript --------------------------------------------------------
|
|
529
|
+
if (execution.stepResults && execution.stepResults.length > 0) {
|
|
530
|
+
console.log('');
|
|
531
|
+
console.log(chalk_1.default.bold(' Transcript'));
|
|
532
|
+
console.log(chalk_1.default.dim(' ' + '\u2500'.repeat(52)));
|
|
533
|
+
for (const step of execution.stepResults) {
|
|
534
|
+
const text = step.actualResponse;
|
|
535
|
+
if (!text)
|
|
536
|
+
continue;
|
|
537
|
+
const isAgent = typeof step.stepId === 'string' && step.stepId.includes('agent');
|
|
538
|
+
const label = isAgent
|
|
539
|
+
? chalk_1.default.cyan.bold('Agent')
|
|
540
|
+
: chalk_1.default.magenta.bold('Persona');
|
|
541
|
+
const latencyTag = isAgent && step.duration
|
|
542
|
+
? chalk_1.default.dim(` [${step.duration}ms]`)
|
|
543
|
+
: '';
|
|
544
|
+
console.log(` ${label}${latencyTag}`);
|
|
545
|
+
// Wrap text at ~68 chars, indented
|
|
546
|
+
const lines = wordWrap(text, 68);
|
|
547
|
+
for (const line of lines) {
|
|
548
|
+
console.log(` ${line}`);
|
|
549
|
+
}
|
|
550
|
+
console.log('');
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// -- Score with visual bar ---------------------------------------------
|
|
554
|
+
if (execution.overallScore !== undefined &&
|
|
555
|
+
execution.overallScore !== null) {
|
|
556
|
+
console.log(chalk_1.default.bold(' Score'));
|
|
557
|
+
console.log(chalk_1.default.dim(' ' + '\u2500'.repeat(52)));
|
|
558
|
+
const score = Math.round(execution.overallScore);
|
|
559
|
+
const scoreColor = score >= 80
|
|
560
|
+
? chalk_1.default.green
|
|
561
|
+
: score >= 50
|
|
562
|
+
? chalk_1.default.yellow
|
|
563
|
+
: chalk_1.default.red;
|
|
564
|
+
const bar = buildScoreBar(score, 20);
|
|
565
|
+
console.log(` ${scoreColor(`${score}/100`)} ${bar}`);
|
|
566
|
+
console.log('');
|
|
567
|
+
}
|
|
568
|
+
// -- Scorecard criteria results ----------------------------------------
|
|
569
|
+
const criteriaResults = extractCriteriaResults(execution);
|
|
570
|
+
if (criteriaResults.length > 0) {
|
|
571
|
+
console.log(chalk_1.default.bold(' Scorecard'));
|
|
572
|
+
console.log(chalk_1.default.dim(' ' + '\u2500'.repeat(52)));
|
|
573
|
+
for (const cr of criteriaResults) {
|
|
574
|
+
const icon = cr.passed ? chalk_1.default.green('\u2713') : chalk_1.default.red('\u2717');
|
|
575
|
+
const nameStr = cr.name || cr.key || 'criterion';
|
|
576
|
+
const typeStr = cr.type ? chalk_1.default.dim(` [${cr.type}]`) : '';
|
|
577
|
+
const scoreStr = cr.score !== undefined && cr.score !== null
|
|
578
|
+
? chalk_1.default.dim(` (${cr.score})`)
|
|
579
|
+
: '';
|
|
580
|
+
const reasonStr = cr.reasoning
|
|
581
|
+
? chalk_1.default.dim(` ${(0, output_1.truncate)(cr.reasoning, 60)}`)
|
|
582
|
+
: '';
|
|
583
|
+
console.log(` ${icon} ${nameStr}${typeStr}${scoreStr}`);
|
|
584
|
+
if (reasonStr) {
|
|
585
|
+
console.log(` ${reasonStr}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
console.log('');
|
|
589
|
+
}
|
|
590
|
+
// -- Latency stats -----------------------------------------------------
|
|
591
|
+
const latencyStats = computeLatencyStats(execution);
|
|
592
|
+
if (latencyStats) {
|
|
593
|
+
console.log(chalk_1.default.bold(' Latency'));
|
|
594
|
+
console.log(chalk_1.default.dim(' ' + '\u2500'.repeat(52)));
|
|
595
|
+
console.log(` Avg response: ${chalk_1.default.cyan(latencyStats.avg + 'ms')}`);
|
|
596
|
+
console.log(` Slowest turn: ${chalk_1.default.yellow(latencyStats.max + 'ms')}${latencyStats.slowestTurn ? chalk_1.default.dim(` (turn ${latencyStats.slowestTurn})`) : ''}`);
|
|
597
|
+
if (latencyStats.min !== latencyStats.max) {
|
|
598
|
+
console.log(` Fastest turn: ${chalk_1.default.green(latencyStats.min + 'ms')}`);
|
|
599
|
+
}
|
|
600
|
+
console.log(` Total turns: ${latencyStats.count}`);
|
|
601
|
+
console.log('');
|
|
602
|
+
}
|
|
603
|
+
// -- Metrics -----------------------------------------------------------
|
|
604
|
+
if (execution.metrics) {
|
|
605
|
+
const m = execution.metrics;
|
|
606
|
+
const hasMetrics = m.totalSteps || m.accuracy !== undefined || m.completion !== undefined;
|
|
607
|
+
if (hasMetrics) {
|
|
608
|
+
console.log(chalk_1.default.bold(' Metrics'));
|
|
609
|
+
console.log(chalk_1.default.dim(' ' + '\u2500'.repeat(52)));
|
|
610
|
+
if (m.totalSteps) {
|
|
611
|
+
console.log(` Steps: ${m.completedSteps || 0}/${m.totalSteps} completed, ${m.failedSteps || 0} failed`);
|
|
612
|
+
}
|
|
613
|
+
if (m.accuracy !== undefined) {
|
|
614
|
+
console.log(` Accuracy: ${m.accuracy}%`);
|
|
615
|
+
}
|
|
616
|
+
if (m.completion !== undefined) {
|
|
617
|
+
console.log(` Completion: ${m.completion}%`);
|
|
618
|
+
}
|
|
619
|
+
console.log('');
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// -- Errors ------------------------------------------------------------
|
|
623
|
+
if (execution.errorMessages && execution.errorMessages.length > 0) {
|
|
624
|
+
console.log(chalk_1.default.bold.red(' Errors'));
|
|
625
|
+
console.log(chalk_1.default.dim(' ' + '\u2500'.repeat(52)));
|
|
626
|
+
for (const msg of execution.errorMessages) {
|
|
627
|
+
console.log(` ${chalk_1.default.red('\u2717')} ${msg}`);
|
|
628
|
+
}
|
|
629
|
+
console.log('');
|
|
630
|
+
}
|
|
631
|
+
// -- Cloud teaser (subtle, one line) -----------------------------------
|
|
632
|
+
console.log(chalk_1.default.dim(' \u{1F4A1} chanl cloud \u2192 dashboard, voice testing, trends \u2192 chanl.ai'));
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Build a visual score bar using block characters.
|
|
636
|
+
* Filled blocks for the score portion, light shade for the remainder.
|
|
637
|
+
*/
|
|
638
|
+
function buildScoreBar(score, width) {
|
|
639
|
+
const clamped = Math.max(0, Math.min(100, score));
|
|
640
|
+
const filled = Math.round((clamped / 100) * width);
|
|
641
|
+
const empty = width - filled;
|
|
642
|
+
const filledChar = '\u2588'; // full block
|
|
643
|
+
const emptyChar = '\u2591'; // light shade
|
|
644
|
+
const bar = filledChar.repeat(filled) + emptyChar.repeat(empty);
|
|
645
|
+
if (score >= 80)
|
|
646
|
+
return chalk_1.default.green(bar);
|
|
647
|
+
if (score >= 50)
|
|
648
|
+
return chalk_1.default.yellow(bar);
|
|
649
|
+
return chalk_1.default.red(bar);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Compute latency statistics from agent response times in stepResults.
|
|
653
|
+
*/
|
|
654
|
+
function computeLatencyStats(execution) {
|
|
655
|
+
if (!execution.stepResults || execution.stepResults.length === 0) {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
const agentSteps = execution.stepResults.filter((s) => typeof s.stepId === 'string' &&
|
|
659
|
+
s.stepId.includes('agent') &&
|
|
660
|
+
s.duration > 0);
|
|
661
|
+
if (agentSteps.length === 0)
|
|
662
|
+
return null;
|
|
663
|
+
let total = 0;
|
|
664
|
+
let max = 0;
|
|
665
|
+
let min = Infinity;
|
|
666
|
+
let slowestTurn = null;
|
|
667
|
+
for (const step of agentSteps) {
|
|
668
|
+
const d = step.duration;
|
|
669
|
+
total += d;
|
|
670
|
+
if (d > max) {
|
|
671
|
+
max = d;
|
|
672
|
+
// Extract turn number from stepId like "turn-2-agent"
|
|
673
|
+
const match = step.stepId.match(/turn-(\d+)/);
|
|
674
|
+
slowestTurn = match ? parseInt(match[1], 10) + 1 : null;
|
|
675
|
+
}
|
|
676
|
+
if (d < min) {
|
|
677
|
+
min = d;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
avg: Math.round(total / agentSteps.length),
|
|
682
|
+
max,
|
|
683
|
+
min: min === Infinity ? 0 : min,
|
|
684
|
+
count: agentSteps.length,
|
|
685
|
+
slowestTurn,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Extract scorecard criteria results from execution data.
|
|
690
|
+
* Criteria results may be present at the top level (if the API enriches the response)
|
|
691
|
+
* or nested in execution metadata.
|
|
692
|
+
*/
|
|
693
|
+
function extractCriteriaResults(execution) {
|
|
694
|
+
// Top-level criteriaResults (if API returns enriched data)
|
|
695
|
+
if (execution.criteriaResults && Array.isArray(execution.criteriaResults)) {
|
|
696
|
+
return execution.criteriaResults.map((cr) => ({
|
|
697
|
+
name: cr.criteriaName || cr.criteriaKey || '',
|
|
698
|
+
key: cr.criteriaKey || '',
|
|
699
|
+
type: cr.type,
|
|
700
|
+
score: typeof cr.result === 'number' ? cr.result : undefined,
|
|
701
|
+
passed: !!cr.passed,
|
|
702
|
+
reasoning: cr.reasoning,
|
|
703
|
+
}));
|
|
704
|
+
}
|
|
705
|
+
// Nested in metadata (if execution processor stored them)
|
|
706
|
+
if (execution.metadata?.scorecardResult?.criteriaResults) {
|
|
707
|
+
return execution.metadata.scorecardResult.criteriaResults.map((cr) => ({
|
|
708
|
+
name: cr.criteriaName || cr.criteriaKey || '',
|
|
709
|
+
key: cr.criteriaKey || '',
|
|
710
|
+
type: cr.type,
|
|
711
|
+
score: typeof cr.result === 'number' ? cr.result : undefined,
|
|
712
|
+
passed: !!cr.passed,
|
|
713
|
+
reasoning: cr.reasoning,
|
|
714
|
+
}));
|
|
715
|
+
}
|
|
716
|
+
return [];
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Simple word wrap for transcript display.
|
|
720
|
+
*/
|
|
721
|
+
function wordWrap(text, maxWidth) {
|
|
722
|
+
if (!text)
|
|
723
|
+
return [''];
|
|
724
|
+
const words = text.split(/\s+/);
|
|
725
|
+
const lines = [];
|
|
726
|
+
let currentLine = '';
|
|
727
|
+
for (const word of words) {
|
|
728
|
+
if (currentLine.length + word.length + 1 > maxWidth &&
|
|
729
|
+
currentLine.length > 0) {
|
|
730
|
+
lines.push(currentLine);
|
|
731
|
+
currentLine = word;
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
currentLine = currentLine ? currentLine + ' ' + word : word;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (currentLine)
|
|
738
|
+
lines.push(currentLine);
|
|
739
|
+
return lines.length > 0 ? lines : [''];
|
|
740
|
+
}
|
|
741
|
+
/** Convert a name to a URL-friendly slug. */
|
|
742
|
+
function slugify(name) {
|
|
743
|
+
return name
|
|
744
|
+
.toLowerCase()
|
|
745
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
746
|
+
.replace(/^-+|-+$/g, '');
|
|
747
|
+
}
|
|
748
|
+
/** Check if a string looks like a MongoDB ObjectId. */
|
|
749
|
+
function isObjectId(str) {
|
|
750
|
+
return /^[a-f0-9]{24}$/.test(str);
|
|
751
|
+
}
|
|
752
|
+
function sleep(ms) {
|
|
753
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
754
|
+
}
|
|
755
|
+
//# sourceMappingURL=scenarios.js.map
|