applied-cli 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,1006 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Any, Optional
4
+
5
+ import typer
6
+
7
+ from applied_cli.commands._parsers import (
8
+ parse_optional_bool,
9
+ validate_uuid,
10
+ )
11
+ from applied_cli.error_reporting import render_api_error
12
+ from applied_cli.http import (
13
+ APIError,
14
+ create_conversation_benchmark,
15
+ create_conversation_scenario,
16
+ create_scenario_run,
17
+ delete_conversation,
18
+ delete_conversation_benchmark,
19
+ delete_conversation_scenario,
20
+ get_conversation,
21
+ import_conversations_bulk,
22
+ get_conversation_benchmark,
23
+ get_conversation_scenario,
24
+ get_scenario_run,
25
+ list_conversation_benchmarks,
26
+ list_conversation_messages,
27
+ list_conversation_references,
28
+ list_conversation_scenarios,
29
+ list_conversations,
30
+ list_scenario_runs,
31
+ patch_conversation_scenario,
32
+ patch_scenario_run,
33
+ )
34
+ from applied_cli.runtime import resolve_runtime
35
+
36
+ conversations_app = typer.Typer(help="List and inspect conversations.")
37
+ benchmarks_app = typer.Typer(help="List and inspect test coverage benchmarks.")
38
+ scenarios_app = typer.Typer(help="Manage test scenarios.")
39
+ runs_app = typer.Typer(help="Manage scenario runs.")
40
+
41
+
42
+ def _emit(items: Any, as_json: bool) -> None:
43
+ if as_json:
44
+ typer.echo(json.dumps(items, indent=2, default=str))
45
+ return
46
+ if isinstance(items, list):
47
+ if not items:
48
+ typer.echo("No results.")
49
+ return
50
+ for item in items:
51
+ if isinstance(item, dict):
52
+ row: list[str] = []
53
+ identifier = item.get("id")
54
+ if identifier is not None:
55
+ row.append(f"id={identifier}")
56
+ name = item.get("name")
57
+ if name:
58
+ row.append(f"name={name}")
59
+ item_type = item.get("type") or item.get("target_type")
60
+ if item_type:
61
+ row.append(f"type={item_type}")
62
+ pass_status = item.get("pass_status")
63
+ if pass_status:
64
+ row.append(f"pass_status={pass_status}")
65
+ csat_score = item.get("csat_score")
66
+ if csat_score is not None:
67
+ row.append(f"csat_score={csat_score}")
68
+ created_at = item.get("created_at")
69
+ if created_at:
70
+ row.append(f"created_at={created_at}")
71
+ typer.echo(" | ".join(row))
72
+ else:
73
+ typer.echo(str(item))
74
+ return
75
+ typer.echo(json.dumps(items, indent=2, default=str))
76
+
77
+
78
+ def _require_option(value: Optional[str], *, option: str, example: str) -> str:
79
+ if value:
80
+ return value
81
+ raise typer.BadParameter(
82
+ f"{option} is required. Example: {example}",
83
+ param_hint=option,
84
+ )
85
+
86
+
87
+ def _parse_optional_pass_status(raw: Optional[str]) -> Optional[str]:
88
+ if raw is None:
89
+ return None
90
+ value = raw.strip().lower()
91
+ if value not in {"pass", "fail"}:
92
+ raise typer.BadParameter("pass-status must be one of: pass, fail.")
93
+ return value
94
+
95
+
96
+ def _is_test_conversation(row: dict[str, Any]) -> bool:
97
+ is_test = row.get("is_test")
98
+ if isinstance(is_test, bool):
99
+ return is_test
100
+ metadata = row.get("metadata")
101
+ if isinstance(metadata, dict) and isinstance(metadata.get("is_test"), bool):
102
+ return metadata["is_test"]
103
+ return False
104
+
105
+
106
+ @conversations_app.command(
107
+ "list",
108
+ help=(
109
+ "List conversations. Example: applied-cli conversations list --agent-id <uuid> "
110
+ "--is-test true --limit 20"
111
+ ),
112
+ )
113
+ def conversations_list(
114
+ agent_id: Optional[str] = typer.Option(
115
+ None, "--agent-id", "--agent", help="Filter by agent UUID."
116
+ ),
117
+ is_test: Optional[str] = typer.Option(
118
+ None, help="Filter by test flag: true/false."
119
+ ),
120
+ limit: int = typer.Option(20, help="Maximum number of results."),
121
+ ordering: str = typer.Option("-created_at", help="Ordering, e.g. -created_at."),
122
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
123
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
124
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
125
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
126
+ ) -> None:
127
+ if agent_id:
128
+ validate_uuid(agent_id, field_name="agent-id")
129
+ parsed_is_test = parse_optional_bool(is_test, field_name="is-test")
130
+ try:
131
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
132
+ base_url=base_url,
133
+ shop_id=shop_id,
134
+ api_token=api_token,
135
+ )
136
+ rows = list_conversations(
137
+ base_url=resolved_base_url,
138
+ shop_id=resolved_shop_id,
139
+ api_token=resolved_token,
140
+ agent_id=agent_id,
141
+ is_test=parsed_is_test,
142
+ limit=limit,
143
+ ordering=ordering,
144
+ )
145
+ except APIError as exc:
146
+ typer.echo(render_api_error(exc, action="list conversations"), err=True)
147
+ raise typer.Exit(code=1) from exc
148
+ _emit(rows, output_json)
149
+
150
+
151
+ @conversations_app.command(
152
+ "describe",
153
+ help=(
154
+ "Describe one conversation and optional transcript. Example: applied-cli conversations "
155
+ "describe --conversation-id <uuid> --include-references"
156
+ ),
157
+ )
158
+ @conversations_app.command(
159
+ "show",
160
+ help=(
161
+ "Show one conversation and optional transcript. Example: applied-cli conversations show "
162
+ "--conversation-id <uuid> --include-references"
163
+ ),
164
+ )
165
+ def conversations_show(
166
+ conversation_id: Optional[str] = typer.Option(
167
+ None, "--conversation-id", "--conversation", "--id", help="Conversation UUID."
168
+ ),
169
+ include_messages: bool = typer.Option(
170
+ True, "--include-messages/--no-include-messages", help="Include transcript."
171
+ ),
172
+ include_references: bool = typer.Option(
173
+ False,
174
+ "--include-references/--no-include-references",
175
+ help="Include message references.",
176
+ ),
177
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
178
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
179
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
180
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
181
+ ) -> None:
182
+ conversation_id = _require_option(
183
+ conversation_id,
184
+ option="--conversation-id",
185
+ example="applied-cli conversations show --conversation-id <uuid>",
186
+ )
187
+ validate_uuid(conversation_id, field_name="conversation-id")
188
+ try:
189
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
190
+ base_url=base_url,
191
+ shop_id=shop_id,
192
+ api_token=api_token,
193
+ )
194
+ conversation = get_conversation(
195
+ base_url=resolved_base_url,
196
+ shop_id=resolved_shop_id,
197
+ api_token=resolved_token,
198
+ conversation_id=conversation_id,
199
+ )
200
+ messages: list[dict[str, Any]] = []
201
+ references: list[dict[str, Any]] = []
202
+ if include_messages:
203
+ messages = list_conversation_messages(
204
+ base_url=resolved_base_url,
205
+ shop_id=resolved_shop_id,
206
+ api_token=resolved_token,
207
+ conversation_id=conversation_id,
208
+ )
209
+ if include_references:
210
+ references = list_conversation_references(
211
+ base_url=resolved_base_url,
212
+ shop_id=resolved_shop_id,
213
+ api_token=resolved_token,
214
+ conversation_id=conversation_id,
215
+ )
216
+ except APIError as exc:
217
+ typer.echo(render_api_error(exc, action="show conversation"), err=True)
218
+ raise typer.Exit(code=1) from exc
219
+
220
+ payload: dict[str, Any] = {"conversation": conversation}
221
+ if include_messages:
222
+ payload["messages"] = messages
223
+ if include_references:
224
+ payload["references"] = references
225
+ _emit(payload, output_json)
226
+
227
+
228
+ @conversations_app.command(
229
+ "references",
230
+ help=(
231
+ "List message references for a conversation. Example: applied-cli conversations references "
232
+ "--conversation-id <uuid>"
233
+ ),
234
+ )
235
+ def conversations_references(
236
+ conversation_id: Optional[str] = typer.Option(
237
+ None, "--conversation-id", "--conversation", "--id", help="Conversation UUID."
238
+ ),
239
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
240
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
241
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
242
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
243
+ ) -> None:
244
+ conversation_id = _require_option(
245
+ conversation_id,
246
+ option="--conversation-id",
247
+ example="applied-cli conversations references --conversation-id <uuid>",
248
+ )
249
+ validate_uuid(conversation_id, field_name="conversation-id")
250
+ try:
251
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
252
+ base_url=base_url,
253
+ shop_id=shop_id,
254
+ api_token=api_token,
255
+ )
256
+ references = list_conversation_references(
257
+ base_url=resolved_base_url,
258
+ shop_id=resolved_shop_id,
259
+ api_token=resolved_token,
260
+ conversation_id=conversation_id,
261
+ )
262
+ except APIError as exc:
263
+ typer.echo(render_api_error(exc, action="list conversation references"), err=True)
264
+ raise typer.Exit(code=1) from exc
265
+ _emit(references, output_json)
266
+
267
+
268
+ @conversations_app.command(
269
+ "delete",
270
+ help=(
271
+ "Delete a test conversation only. Example: applied-cli conversations delete "
272
+ "--conversation-id <uuid> --yes"
273
+ ),
274
+ )
275
+ def conversations_delete(
276
+ conversation_id: Optional[str] = typer.Option(
277
+ None, "--conversation-id", "--conversation", "--id", help="Conversation UUID."
278
+ ),
279
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
280
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
281
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
282
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
283
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
284
+ ) -> None:
285
+ conversation_id = _require_option(
286
+ conversation_id,
287
+ option="--conversation-id",
288
+ example="applied-cli conversations delete --conversation-id <uuid> --yes",
289
+ )
290
+ validate_uuid(conversation_id, field_name="conversation-id")
291
+ try:
292
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
293
+ base_url=base_url,
294
+ shop_id=shop_id,
295
+ api_token=api_token,
296
+ )
297
+ row = get_conversation(
298
+ base_url=resolved_base_url,
299
+ shop_id=resolved_shop_id,
300
+ api_token=resolved_token,
301
+ conversation_id=conversation_id,
302
+ )
303
+ if not _is_test_conversation(row):
304
+ raise typer.BadParameter(
305
+ "Only test conversations can be deleted. This conversation is not marked is_test=true."
306
+ )
307
+ if not yes and not typer.confirm(
308
+ f"Delete test conversation {conversation_id} in shop {resolved_shop_id}?"
309
+ ):
310
+ raise typer.Exit(code=1)
311
+ delete_conversation(
312
+ base_url=resolved_base_url,
313
+ shop_id=resolved_shop_id,
314
+ api_token=resolved_token,
315
+ conversation_id=conversation_id,
316
+ )
317
+ except APIError as exc:
318
+ typer.echo(render_api_error(exc, action="delete test conversation"), err=True)
319
+ raise typer.Exit(code=1) from exc
320
+ payload = {"deleted": True, "resource": "conversation", "id": conversation_id}
321
+ if output_json:
322
+ typer.echo(json.dumps(payload, indent=2))
323
+ else:
324
+ typer.echo(f"result=success | deleted=true | resource=conversation | id={conversation_id}")
325
+
326
+
327
+ @conversations_app.command(
328
+ "import",
329
+ help=(
330
+ "Import historical conversations from CSV file/URL. Example: applied-cli conversations "
331
+ "import --agent-id <uuid> --file-path ./historical.csv --process-labels. "
332
+ "Expected CSV headers: ID, DATE_CREATED, SENDER, TYPE, MESSAGE_DATE, BODY, TITLE, TOPIC, "
333
+ "INTENT, SENTIMENT, SENTIMENT_SCORE. CONTACT_PHONE is optional; importer now normalizes or fills placeholders."
334
+ ),
335
+ )
336
+ def conversations_import(
337
+ agent_id: str = typer.Option(..., "--agent-id", "--agent", "--id", help="Target agent UUID."),
338
+ file_path: Optional[str] = typer.Option(
339
+ None,
340
+ "--file-path",
341
+ help="Local CSV path. Use either --file-path or --url.",
342
+ ),
343
+ source_url: Optional[str] = typer.Option(
344
+ None,
345
+ "--url",
346
+ help="Remote CSV URL (s3://, http://, https://). Use either --url or --file-path.",
347
+ ),
348
+ process_labels: bool = typer.Option(
349
+ False,
350
+ "--process-labels/--no-process-labels",
351
+ help="Apply topic/intent labels during import.",
352
+ ),
353
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
354
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
355
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
356
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
357
+ ) -> None:
358
+ validate_uuid(agent_id, field_name="agent-id")
359
+ if bool(file_path) == bool(source_url):
360
+ raise typer.BadParameter(
361
+ "Provide exactly one source: --file-path or --url. Example: applied-cli conversations import --agent-id <uuid> --file-path ./historical.csv"
362
+ )
363
+ if file_path:
364
+ path = Path(file_path)
365
+ if not path.exists() or not path.is_file():
366
+ raise typer.BadParameter(f"file-path not found: {file_path}")
367
+
368
+ try:
369
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
370
+ base_url=base_url,
371
+ shop_id=shop_id,
372
+ api_token=api_token,
373
+ )
374
+ result = import_conversations_bulk(
375
+ base_url=resolved_base_url,
376
+ shop_id=resolved_shop_id,
377
+ api_token=resolved_token,
378
+ agent_id=agent_id,
379
+ file_path=file_path,
380
+ url=source_url,
381
+ process_labels=process_labels,
382
+ )
383
+ except APIError as exc:
384
+ typer.echo(render_api_error(exc, action="import historical conversations"), err=True)
385
+ typer.echo(
386
+ "CSV format reminder: ID, DATE_CREATED, SENDER, TYPE, MESSAGE_DATE, BODY, TITLE, TOPIC, INTENT, SENTIMENT, SENTIMENT_SCORE, CSAT_SCORE",
387
+ err=True,
388
+ )
389
+ typer.echo(
390
+ "CONTACT_PHONE is optional. Import now normalizes long phones and generates safe placeholders when missing.",
391
+ err=True,
392
+ )
393
+ raise typer.Exit(code=1) from exc
394
+
395
+ payload = {
396
+ "result": "success",
397
+ "agent_id": agent_id,
398
+ "source": file_path or source_url,
399
+ "process_labels": process_labels,
400
+ "summary": result,
401
+ }
402
+ _emit(payload, output_json)
403
+
404
+
405
+ @benchmarks_app.command(
406
+ "list",
407
+ help=(
408
+ "List conversation benchmarks. Example: applied-cli test benchmarks list --agent-id <uuid>"
409
+ ),
410
+ )
411
+ def benchmarks_list(
412
+ agent_id: Optional[str] = typer.Option(
413
+ None, "--agent-id", "--agent", help="Filter by agent UUID."
414
+ ),
415
+ limit: int = typer.Option(50, help="Maximum number of results."),
416
+ ordering: str = typer.Option("-created_at", help="Ordering, e.g. -created_at."),
417
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
418
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
419
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
420
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
421
+ ) -> None:
422
+ if agent_id:
423
+ validate_uuid(agent_id, field_name="agent-id")
424
+ try:
425
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
426
+ base_url=base_url,
427
+ shop_id=shop_id,
428
+ api_token=api_token,
429
+ )
430
+ rows = list_conversation_benchmarks(
431
+ base_url=resolved_base_url,
432
+ shop_id=resolved_shop_id,
433
+ api_token=resolved_token,
434
+ agent_id=agent_id,
435
+ limit=limit,
436
+ ordering=ordering,
437
+ )
438
+ except APIError as exc:
439
+ typer.echo(render_api_error(exc, action="list benchmarks"), err=True)
440
+ raise typer.Exit(code=1) from exc
441
+ _emit(rows, output_json)
442
+
443
+
444
+ @benchmarks_app.command(
445
+ "describe",
446
+ help=(
447
+ "Describe one benchmark. Example: applied-cli test benchmarks describe --benchmark-id <uuid>"
448
+ ),
449
+ )
450
+ @benchmarks_app.command(
451
+ "show",
452
+ help="Show one benchmark. Example: applied-cli test benchmarks show --benchmark-id <uuid>",
453
+ )
454
+ def benchmarks_show(
455
+ benchmark_id: Optional[str] = typer.Option(
456
+ None, "--benchmark-id", "--benchmark", "--id", help="Benchmark UUID."
457
+ ),
458
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
459
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
460
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
461
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
462
+ ) -> None:
463
+ benchmark_id = _require_option(
464
+ benchmark_id,
465
+ option="--benchmark-id",
466
+ example="applied-cli test benchmarks show --benchmark-id <uuid>",
467
+ )
468
+ validate_uuid(benchmark_id, field_name="benchmark-id")
469
+ try:
470
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
471
+ base_url=base_url,
472
+ shop_id=shop_id,
473
+ api_token=api_token,
474
+ )
475
+ row = get_conversation_benchmark(
476
+ base_url=resolved_base_url,
477
+ shop_id=resolved_shop_id,
478
+ api_token=resolved_token,
479
+ benchmark_id=benchmark_id,
480
+ )
481
+ except APIError as exc:
482
+ typer.echo(render_api_error(exc, action="show benchmark"), err=True)
483
+ raise typer.Exit(code=1) from exc
484
+ _emit(row, output_json)
485
+
486
+
487
+ @benchmarks_app.command(
488
+ "create",
489
+ help=(
490
+ "Create one benchmark. Example: applied-cli test benchmarks create --agent-id <uuid> "
491
+ "--name 'CLI Self-Rated Conversations'"
492
+ ),
493
+ )
494
+ def benchmarks_create(
495
+ agent_id: str = typer.Option(..., "--agent-id", "--agent", help="Target agent UUID."),
496
+ name: str = typer.Option(..., "--name", help="Benchmark name."),
497
+ description: str = typer.Option(
498
+ "Benchmark collection created by applied-cli.",
499
+ "--description",
500
+ help="Benchmark description.",
501
+ ),
502
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
503
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
504
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
505
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
506
+ ) -> None:
507
+ validate_uuid(agent_id, field_name="agent-id")
508
+ try:
509
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
510
+ base_url=base_url,
511
+ shop_id=shop_id,
512
+ api_token=api_token,
513
+ )
514
+ row = create_conversation_benchmark(
515
+ base_url=resolved_base_url,
516
+ shop_id=resolved_shop_id,
517
+ api_token=resolved_token,
518
+ agent_id=agent_id,
519
+ name=name.strip(),
520
+ description=description.strip(),
521
+ )
522
+ except APIError as exc:
523
+ typer.echo(render_api_error(exc, action="create benchmark"), err=True)
524
+ raise typer.Exit(code=1) from exc
525
+ _emit(row, output_json)
526
+
527
+
528
+ @benchmarks_app.command(
529
+ "delete",
530
+ help="Delete one benchmark. Example: applied-cli test benchmarks delete --benchmark-id <uuid> --yes",
531
+ )
532
+ def benchmarks_delete(
533
+ benchmark_id: Optional[str] = typer.Option(
534
+ None, "--benchmark-id", "--benchmark", "--id", help="Benchmark UUID."
535
+ ),
536
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
537
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
538
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
539
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
540
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
541
+ ) -> None:
542
+ benchmark_id = _require_option(
543
+ benchmark_id,
544
+ option="--benchmark-id",
545
+ example="applied-cli test benchmarks delete --benchmark-id <uuid> --yes",
546
+ )
547
+ validate_uuid(benchmark_id, field_name="benchmark-id")
548
+ try:
549
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
550
+ base_url=base_url,
551
+ shop_id=shop_id,
552
+ api_token=api_token,
553
+ )
554
+ if not yes and not typer.confirm(
555
+ f"Delete benchmark {benchmark_id} in shop {resolved_shop_id}?"
556
+ ):
557
+ raise typer.Exit(code=1)
558
+ delete_conversation_benchmark(
559
+ base_url=resolved_base_url,
560
+ shop_id=resolved_shop_id,
561
+ api_token=resolved_token,
562
+ benchmark_id=benchmark_id,
563
+ )
564
+ except APIError as exc:
565
+ typer.echo(render_api_error(exc, action="delete benchmark"), err=True)
566
+ raise typer.Exit(code=1) from exc
567
+ payload = {"deleted": True, "resource": "benchmark", "id": benchmark_id}
568
+ if output_json:
569
+ typer.echo(json.dumps(payload, indent=2))
570
+ else:
571
+ typer.echo(f"result=success | deleted=true | resource=benchmark | id={benchmark_id}")
572
+
573
+
574
+ @scenarios_app.command(
575
+ "list",
576
+ help=(
577
+ "List scenarios by benchmark/agent. Example: applied-cli test scenarios list "
578
+ "--benchmark-id <uuid> --agent-id <uuid> --pass-status fail"
579
+ ),
580
+ )
581
+ def scenarios_list(
582
+ benchmark_id: Optional[str] = typer.Option(
583
+ None, "--benchmark-id", "--benchmark", help="Filter by benchmark UUID."
584
+ ),
585
+ agent_id: Optional[str] = typer.Option(
586
+ None, "--agent-id", "--agent", help="Filter by agent UUID."
587
+ ),
588
+ name: Optional[str] = typer.Option(None, help="Filter by scenario name."),
589
+ pass_status: Optional[str] = typer.Option(
590
+ None, "--pass-status", help="Filter by pass status: pass/fail."
591
+ ),
592
+ limit: int = typer.Option(50, help="Maximum number of results."),
593
+ ordering: str = typer.Option("-created_at", help="Ordering, e.g. -created_at."),
594
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
595
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
596
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
597
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
598
+ ) -> None:
599
+ if benchmark_id:
600
+ validate_uuid(benchmark_id, field_name="benchmark-id")
601
+ if agent_id:
602
+ validate_uuid(agent_id, field_name="agent-id")
603
+ parsed_pass_status = _parse_optional_pass_status(pass_status)
604
+ try:
605
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
606
+ base_url=base_url,
607
+ shop_id=shop_id,
608
+ api_token=api_token,
609
+ )
610
+ rows = list_conversation_scenarios(
611
+ base_url=resolved_base_url,
612
+ shop_id=resolved_shop_id,
613
+ api_token=resolved_token,
614
+ agent_id=agent_id,
615
+ benchmark_id=benchmark_id,
616
+ name=name,
617
+ pass_status=parsed_pass_status,
618
+ limit=limit,
619
+ ordering=ordering,
620
+ )
621
+ except APIError as exc:
622
+ typer.echo(render_api_error(exc, action="list scenarios"), err=True)
623
+ raise typer.Exit(code=1) from exc
624
+ _emit(rows, output_json)
625
+
626
+
627
+ @scenarios_app.command(
628
+ "describe",
629
+ help="Describe one scenario. Example: applied-cli test scenarios describe --scenario-id <uuid>",
630
+ )
631
+ @scenarios_app.command(
632
+ "show",
633
+ help="Show one scenario. Example: applied-cli test scenarios show --scenario-id <uuid>",
634
+ )
635
+ def scenarios_show(
636
+ scenario_id: Optional[str] = typer.Option(
637
+ None, "--scenario-id", "--scenario", "--id", help="Scenario UUID."
638
+ ),
639
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
640
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
641
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
642
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
643
+ ) -> None:
644
+ scenario_id = _require_option(
645
+ scenario_id,
646
+ option="--scenario-id",
647
+ example="applied-cli test scenarios show --scenario-id <uuid>",
648
+ )
649
+ validate_uuid(scenario_id, field_name="scenario-id")
650
+ try:
651
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
652
+ base_url=base_url,
653
+ shop_id=shop_id,
654
+ api_token=api_token,
655
+ )
656
+ row = get_conversation_scenario(
657
+ base_url=resolved_base_url,
658
+ shop_id=resolved_shop_id,
659
+ api_token=resolved_token,
660
+ scenario_id=scenario_id,
661
+ )
662
+ except APIError as exc:
663
+ typer.echo(render_api_error(exc, action="show scenario"), err=True)
664
+ raise typer.Exit(code=1) from exc
665
+ _emit(row, output_json)
666
+
667
+
668
+ @scenarios_app.command(
669
+ "create",
670
+ help=(
671
+ "Create one scenario. Example: applied-cli test scenarios create --agent-id <uuid> "
672
+ "--name 'Escalation test' --input-conversation-id <uuid>"
673
+ ),
674
+ )
675
+ def scenarios_create(
676
+ agent_id: str = typer.Option(..., "--agent-id", "--agent", help="Target agent UUID."),
677
+ name: str = typer.Option(..., "--name", help="Scenario name."),
678
+ input_conversation_id: str = typer.Option(
679
+ ...,
680
+ "--input-conversation-id",
681
+ "--conversation-id",
682
+ help="Input conversation UUID.",
683
+ ),
684
+ benchmark_id: Optional[str] = typer.Option(
685
+ None, "--benchmark-id", "--benchmark", help="Optional benchmark UUID."
686
+ ),
687
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
688
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
689
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
690
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
691
+ ) -> None:
692
+ validate_uuid(agent_id, field_name="agent-id")
693
+ validate_uuid(input_conversation_id, field_name="input-conversation-id")
694
+ if benchmark_id:
695
+ validate_uuid(benchmark_id, field_name="benchmark-id")
696
+ try:
697
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
698
+ base_url=base_url,
699
+ shop_id=shop_id,
700
+ api_token=api_token,
701
+ )
702
+ row = create_conversation_scenario(
703
+ base_url=resolved_base_url,
704
+ shop_id=resolved_shop_id,
705
+ api_token=resolved_token,
706
+ agent_id=agent_id,
707
+ benchmark_id=benchmark_id,
708
+ name=name.strip(),
709
+ input_conversation_id=input_conversation_id,
710
+ )
711
+ except APIError as exc:
712
+ typer.echo(render_api_error(exc, action="create scenario"), err=True)
713
+ raise typer.Exit(code=1) from exc
714
+ _emit(row, output_json)
715
+
716
+
717
+ @scenarios_app.command(
718
+ "update",
719
+ help=(
720
+ "Update one scenario. Example: applied-cli test scenarios update --scenario-id <uuid> "
721
+ "--pass-status pass --csat-score 5"
722
+ ),
723
+ )
724
+ def scenarios_update(
725
+ scenario_id: str = typer.Option(..., "--scenario-id", "--scenario", "--id"),
726
+ name: Optional[str] = typer.Option(None, "--name", help="Scenario name."),
727
+ benchmark_id: Optional[str] = typer.Option(
728
+ None, "--benchmark-id", "--benchmark", help="Single benchmark UUID to attach."
729
+ ),
730
+ pass_status: Optional[str] = typer.Option(
731
+ None, "--pass-status", help="pass/fail aggregate status."
732
+ ),
733
+ csat_score: Optional[float] = typer.Option(None, "--csat-score"),
734
+ feedback: Optional[str] = typer.Option(None, "--feedback"),
735
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
736
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
737
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
738
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
739
+ ) -> None:
740
+ validate_uuid(scenario_id, field_name="scenario-id")
741
+ if benchmark_id:
742
+ validate_uuid(benchmark_id, field_name="benchmark-id")
743
+ parsed_pass_status = _parse_optional_pass_status(pass_status)
744
+ payload: dict[str, Any] = {}
745
+ if name is not None:
746
+ payload["name"] = name.strip()
747
+ if benchmark_id is not None:
748
+ payload["benchmark_ids"] = [benchmark_id]
749
+ if parsed_pass_status is not None:
750
+ payload["pass_status"] = parsed_pass_status
751
+ if csat_score is not None:
752
+ payload["csat_score"] = float(csat_score)
753
+ if feedback is not None:
754
+ payload["feedback"] = feedback
755
+ if not payload:
756
+ raise typer.BadParameter("No fields provided. Pass at least one update option.")
757
+ try:
758
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
759
+ base_url=base_url,
760
+ shop_id=shop_id,
761
+ api_token=api_token,
762
+ )
763
+ row = patch_conversation_scenario(
764
+ base_url=resolved_base_url,
765
+ shop_id=resolved_shop_id,
766
+ api_token=resolved_token,
767
+ scenario_id=scenario_id,
768
+ payload=payload,
769
+ )
770
+ except APIError as exc:
771
+ typer.echo(render_api_error(exc, action="update scenario"), err=True)
772
+ raise typer.Exit(code=1) from exc
773
+ _emit(row, output_json)
774
+
775
+
776
+ @scenarios_app.command(
777
+ "delete",
778
+ help="Delete one scenario. Example: applied-cli test scenarios delete --scenario-id <uuid> --yes",
779
+ )
780
+ def scenarios_delete(
781
+ scenario_id: Optional[str] = typer.Option(
782
+ None, "--scenario-id", "--scenario", "--id", help="Scenario UUID."
783
+ ),
784
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
785
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
786
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
787
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
788
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
789
+ ) -> None:
790
+ scenario_id = _require_option(
791
+ scenario_id,
792
+ option="--scenario-id",
793
+ example="applied-cli test scenarios delete --scenario-id <uuid> --yes",
794
+ )
795
+ validate_uuid(scenario_id, field_name="scenario-id")
796
+ try:
797
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
798
+ base_url=base_url,
799
+ shop_id=shop_id,
800
+ api_token=api_token,
801
+ )
802
+ if not yes and not typer.confirm(
803
+ f"Delete scenario {scenario_id} in shop {resolved_shop_id}?"
804
+ ):
805
+ raise typer.Exit(code=1)
806
+ delete_conversation_scenario(
807
+ base_url=resolved_base_url,
808
+ shop_id=resolved_shop_id,
809
+ api_token=resolved_token,
810
+ scenario_id=scenario_id,
811
+ )
812
+ except APIError as exc:
813
+ typer.echo(render_api_error(exc, action="delete scenario"), err=True)
814
+ raise typer.Exit(code=1) from exc
815
+ payload = {"deleted": True, "resource": "scenario", "id": scenario_id}
816
+ if output_json:
817
+ typer.echo(json.dumps(payload, indent=2))
818
+ else:
819
+ typer.echo(f"result=success | deleted=true | resource=scenario | id={scenario_id}")
820
+
821
+
822
+ @runs_app.command(
823
+ "list",
824
+ help="List scenario runs. Example: applied-cli test runs list --scenario-id <uuid> --latest",
825
+ )
826
+ def runs_list(
827
+ scenario_id: Optional[str] = typer.Option(
828
+ None, "--scenario-id", "--scenario", "--id", help="Scenario UUID."
829
+ ),
830
+ latest: bool = typer.Option(
831
+ False, "--latest/--all", help="Return only latest run per scenario."
832
+ ),
833
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
834
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
835
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
836
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
837
+ ) -> None:
838
+ scenario_id = _require_option(
839
+ scenario_id,
840
+ option="--scenario-id",
841
+ example="applied-cli test runs list --scenario-id <uuid>",
842
+ )
843
+ validate_uuid(scenario_id, field_name="scenario-id")
844
+ try:
845
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
846
+ base_url=base_url,
847
+ shop_id=shop_id,
848
+ api_token=api_token,
849
+ )
850
+ rows = list_scenario_runs(
851
+ base_url=resolved_base_url,
852
+ shop_id=resolved_shop_id,
853
+ api_token=resolved_token,
854
+ scenario_id=scenario_id,
855
+ latest_only=latest,
856
+ )
857
+ except APIError as exc:
858
+ typer.echo(render_api_error(exc, action="list runs"), err=True)
859
+ raise typer.Exit(code=1) from exc
860
+ _emit(rows, output_json)
861
+
862
+
863
+ @runs_app.command(
864
+ "describe",
865
+ help="Describe one run. Example: applied-cli test runs describe --run-id <uuid>",
866
+ )
867
+ @runs_app.command(
868
+ "show",
869
+ help="Show one run. Example: applied-cli test runs show --run-id <uuid>",
870
+ )
871
+ def runs_show(
872
+ run_id: Optional[str] = typer.Option(None, "--run-id", "--run", "--id", help="Run UUID."),
873
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
874
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
875
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
876
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
877
+ ) -> None:
878
+ run_id = _require_option(
879
+ run_id, option="--run-id", example="applied-cli test runs show --run-id <uuid>"
880
+ )
881
+ validate_uuid(run_id, field_name="run-id")
882
+ try:
883
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
884
+ base_url=base_url,
885
+ shop_id=shop_id,
886
+ api_token=api_token,
887
+ )
888
+ row = get_scenario_run(
889
+ base_url=resolved_base_url,
890
+ shop_id=resolved_shop_id,
891
+ api_token=resolved_token,
892
+ run_id=run_id,
893
+ )
894
+ except APIError as exc:
895
+ typer.echo(render_api_error(exc, action="show run"), err=True)
896
+ raise typer.Exit(code=1) from exc
897
+ _emit(row, output_json)
898
+
899
+
900
+ @runs_app.command(
901
+ "create",
902
+ help=(
903
+ "Create one run. Example: applied-cli test runs create --scenario-id <uuid> "
904
+ "--output-conversation-id <uuid>"
905
+ ),
906
+ )
907
+ def runs_create(
908
+ scenario_id: str = typer.Option(..., "--scenario-id", "--scenario", help="Scenario UUID."),
909
+ output_conversation_id: str = typer.Option(
910
+ ...,
911
+ "--output-conversation-id",
912
+ "--conversation-id",
913
+ help="Output conversation UUID.",
914
+ ),
915
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
916
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
917
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
918
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
919
+ ) -> None:
920
+ validate_uuid(scenario_id, field_name="scenario-id")
921
+ validate_uuid(output_conversation_id, field_name="output-conversation-id")
922
+ try:
923
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
924
+ base_url=base_url,
925
+ shop_id=shop_id,
926
+ api_token=api_token,
927
+ )
928
+ row = create_scenario_run(
929
+ base_url=resolved_base_url,
930
+ shop_id=resolved_shop_id,
931
+ api_token=resolved_token,
932
+ scenario_id=scenario_id,
933
+ output_conversation_id=output_conversation_id,
934
+ )
935
+ except APIError as exc:
936
+ typer.echo(render_api_error(exc, action="create run"), err=True)
937
+ raise typer.Exit(code=1) from exc
938
+ _emit(row, output_json)
939
+
940
+
941
+ @runs_app.command(
942
+ "update",
943
+ help=(
944
+ "Update one run. Example: applied-cli test runs update --run-id <uuid> "
945
+ "--pass-status pass --csat-score 5"
946
+ ),
947
+ )
948
+ def runs_update(
949
+ run_id: str = typer.Option(..., "--run-id", "--run", "--id", help="Run UUID."),
950
+ pass_status: Optional[str] = typer.Option(None, "--pass-status", help="pass/fail result."),
951
+ csat_score: Optional[float] = typer.Option(None, "--csat-score"),
952
+ feedback: Optional[str] = typer.Option(None, "--feedback"),
953
+ reference_score: Optional[float] = typer.Option(None, "--reference-score"),
954
+ reference_notes: Optional[str] = typer.Option(None, "--reference-notes"),
955
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
956
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
957
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
958
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
959
+ ) -> None:
960
+ validate_uuid(run_id, field_name="run-id")
961
+ parsed_pass_status = _parse_optional_pass_status(pass_status)
962
+ payload: dict[str, Any] = {}
963
+ if parsed_pass_status is not None:
964
+ payload["pass_status"] = parsed_pass_status
965
+ if csat_score is not None:
966
+ payload["csat_score"] = float(csat_score)
967
+ if feedback is not None:
968
+ payload["feedback"] = feedback
969
+ if reference_score is not None:
970
+ payload["reference_score"] = float(reference_score)
971
+ if reference_notes is not None:
972
+ payload["reference_notes"] = reference_notes
973
+ if not payload:
974
+ raise typer.BadParameter("No fields provided. Pass at least one update option.")
975
+ try:
976
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
977
+ base_url=base_url,
978
+ shop_id=shop_id,
979
+ api_token=api_token,
980
+ )
981
+ row = patch_scenario_run(
982
+ base_url=resolved_base_url,
983
+ shop_id=resolved_shop_id,
984
+ api_token=resolved_token,
985
+ run_id=run_id,
986
+ payload=payload,
987
+ )
988
+ except APIError as exc:
989
+ typer.echo(render_api_error(exc, action="update run"), err=True)
990
+ raise typer.Exit(code=1) from exc
991
+ _emit(row, output_json)
992
+
993
+
994
+ # =============================================================================
995
+ # SCENARIOS: RATE (imported from rate module)
996
+ # =============================================================================
997
+
998
+ from applied_cli.commands.rate import conversation as rate_conversation
999
+
1000
+ scenarios_app.command(
1001
+ "rate",
1002
+ help=(
1003
+ "Rate a conversation and create a scenario. "
1004
+ "Example: applied-cli test scenarios rate --conversation-id <uuid>"
1005
+ ),
1006
+ )(rate_conversation)