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,614 @@
1
+ import json
2
+ import time
3
+ from typing import Any, Optional
4
+
5
+ import typer
6
+
7
+ from applied_cli.commands._parsers import (
8
+ parse_csv_list,
9
+ parse_optional_bool,
10
+ validate_uuid,
11
+ validate_uuid_list,
12
+ )
13
+ from applied_cli.error_reporting import render_api_error
14
+ from applied_cli.http import (
15
+ APIError,
16
+ get_insights_report,
17
+ get_insights_status,
18
+ insights_followup,
19
+ insights_generate,
20
+ list_insights_reports,
21
+ )
22
+ from applied_cli.runtime import resolve_runtime
23
+
24
+ app = typer.Typer(help="Generate and inspect analytics insights reports.")
25
+
26
+
27
+ def _emit(payload: Any, output_json: bool) -> None:
28
+ if output_json:
29
+ typer.echo(json.dumps(payload, indent=2, default=str))
30
+ return
31
+ if isinstance(payload, list):
32
+ if not payload:
33
+ typer.echo("No results.")
34
+ return
35
+ for row in payload:
36
+ if not isinstance(row, dict):
37
+ typer.echo(str(row))
38
+ continue
39
+ items: list[str] = []
40
+ report_id = row.get("reportId") or row.get("id")
41
+ if report_id:
42
+ items.append(f"report_id={report_id}")
43
+ title = row.get("title")
44
+ if title:
45
+ items.append(f"title={title}")
46
+ status = row.get("status")
47
+ if status:
48
+ items.append(f"status={status}")
49
+ findings = row.get("findingsCount")
50
+ if findings is not None:
51
+ items.append(f"findings={findings}")
52
+ conversations = row.get("conversationsAnalyzed")
53
+ if conversations is not None:
54
+ items.append(f"conversations={conversations}")
55
+ typer.echo(" | ".join(items) if items else str(row))
56
+ return
57
+ if isinstance(payload, dict):
58
+ items = []
59
+ report_id = payload.get("reportId")
60
+ if report_id:
61
+ items.append(f"report_id={report_id}")
62
+ status = payload.get("status")
63
+ if status:
64
+ items.append(f"status={status}")
65
+ title = payload.get("title")
66
+ if title:
67
+ items.append(f"title={title}")
68
+ findings = payload.get("findingsCount")
69
+ if findings is not None:
70
+ items.append(f"findings={findings}")
71
+ conversations = payload.get("conversationsAnalyzed")
72
+ if conversations is not None:
73
+ items.append(f"conversations={conversations}")
74
+ if items:
75
+ typer.echo("result=success | " + " | ".join(items))
76
+ return
77
+ typer.echo(str(payload))
78
+
79
+
80
+ def _build_filters(
81
+ *,
82
+ agent_ids: list[str],
83
+ topic_ids: list[str],
84
+ intent_ids: list[str],
85
+ resolutions: list[str],
86
+ conversation_types: list[str],
87
+ user_ids: list[str],
88
+ tags: list[str],
89
+ directions: list[str],
90
+ flags: list[str],
91
+ score_gte: Optional[int],
92
+ score_lte: Optional[int],
93
+ is_test: Optional[bool],
94
+ ) -> dict[str, Any]:
95
+ filters: dict[str, Any] = {}
96
+ if agent_ids:
97
+ filters["agent__in"] = ",".join(agent_ids)
98
+ if topic_ids:
99
+ filters["label__in"] = ",".join(topic_ids)
100
+ if intent_ids:
101
+ filters["sublabel__in"] = ",".join(intent_ids)
102
+ if resolutions:
103
+ filters["resolution__in"] = ",".join(resolutions)
104
+ if conversation_types:
105
+ filters["type__in"] = ",".join(conversation_types)
106
+ if user_ids:
107
+ filters["user__in"] = ",".join(user_ids)
108
+ if tags:
109
+ filters["tags__in"] = ",".join(tags)
110
+ if directions:
111
+ filters["direction__in"] = ",".join(directions)
112
+ if flags:
113
+ filters["flags__in"] = ",".join(flags)
114
+ if score_gte is not None:
115
+ filters["score__gte"] = score_gte
116
+ if score_lte is not None:
117
+ filters["score__lte"] = score_lte
118
+ if is_test is not None:
119
+ filters["is_test"] = is_test
120
+ return filters
121
+
122
+
123
+ def _wait_for_report_completion(
124
+ *,
125
+ base_url: str,
126
+ shop_id: str,
127
+ api_token: str,
128
+ report_id: str,
129
+ poll_seconds: float,
130
+ max_wait_seconds: float,
131
+ ) -> dict[str, Any]:
132
+ deadline = time.monotonic() + max_wait_seconds
133
+ last_status: dict[str, Any] = {}
134
+ while time.monotonic() < deadline:
135
+ try:
136
+ last_status = get_insights_status(
137
+ base_url=base_url,
138
+ shop_id=shop_id,
139
+ api_token=api_token,
140
+ report_id=report_id,
141
+ )
142
+ except APIError as exc:
143
+ typer.echo(render_api_error(exc, action="poll insights status"), err=True)
144
+ raise typer.Exit(code=1) from exc
145
+
146
+ status_value = str(last_status.get("status") or "").lower()
147
+ if status_value in {"completed", "failed"}:
148
+ return last_status
149
+ time.sleep(poll_seconds)
150
+
151
+ return {
152
+ "reportId": report_id,
153
+ "status": "generating",
154
+ "message": "Timed out waiting for completion. Re-run `applied-cli insights status --report-id <uuid>`.",
155
+ "last_status": last_status,
156
+ }
157
+
158
+
159
+ def _wait_and_emit_report(
160
+ *,
161
+ base_url: str,
162
+ shop_id: str,
163
+ api_token: str,
164
+ report_id: str,
165
+ poll_seconds: float,
166
+ max_wait_seconds: float,
167
+ output_json: bool,
168
+ ) -> None:
169
+ status_payload = _wait_for_report_completion(
170
+ base_url=base_url,
171
+ shop_id=shop_id,
172
+ api_token=api_token,
173
+ report_id=report_id,
174
+ poll_seconds=poll_seconds,
175
+ max_wait_seconds=max_wait_seconds,
176
+ )
177
+ status_value = str(status_payload.get("status") or "").lower()
178
+ if status_value == "completed":
179
+ try:
180
+ report = get_insights_report(
181
+ base_url=base_url,
182
+ shop_id=shop_id,
183
+ api_token=api_token,
184
+ report_id=report_id,
185
+ )
186
+ except APIError as exc:
187
+ typer.echo(render_api_error(exc, action="fetch insights report"), err=True)
188
+ raise typer.Exit(code=1) from exc
189
+ _emit(report, output_json)
190
+ return
191
+
192
+ _emit(status_payload, output_json)
193
+ raise typer.Exit(code=1)
194
+
195
+
196
+ @app.command(
197
+ "run",
198
+ help=(
199
+ "Generate an insights report. Example: applied-cli insights run "
200
+ "--query 'top issues in last 30 days' --agent-ids <uuid> --wait"
201
+ ),
202
+ )
203
+ def run_cmd(
204
+ query: Optional[str] = typer.Option(
205
+ None,
206
+ "--query",
207
+ "--instruction",
208
+ help="Optional natural-language analysis question.",
209
+ ),
210
+ date_range: str = typer.Option(
211
+ "relative:-14d,",
212
+ "--date-range",
213
+ help="Date range, e.g. relative:-30d, or ISO start,end.",
214
+ ),
215
+ data_source: str = typer.Option(
216
+ "conversations",
217
+ "--data-source",
218
+ help="Data source: conversations or tickets.",
219
+ ),
220
+ agent_ids_raw: Optional[str] = typer.Option(
221
+ None,
222
+ "--agent-ids",
223
+ help="Comma-separated agent UUIDs.",
224
+ ),
225
+ topic_ids_raw: Optional[str] = typer.Option(
226
+ None,
227
+ "--topic-ids",
228
+ help="Comma-separated topic UUIDs.",
229
+ ),
230
+ intent_ids_raw: Optional[str] = typer.Option(
231
+ None,
232
+ "--intent-ids",
233
+ help="Comma-separated intent UUIDs.",
234
+ ),
235
+ resolutions_raw: Optional[str] = typer.Option(
236
+ None,
237
+ "--resolutions",
238
+ help="Comma-separated values like escalated,human,soft,hard.",
239
+ ),
240
+ conversation_types_raw: Optional[str] = typer.Option(
241
+ None,
242
+ "--types",
243
+ help="Comma-separated conversation types: web_chat,email,sms,phone_call,web_call,comments,form.",
244
+ ),
245
+ user_ids_raw: Optional[str] = typer.Option(
246
+ None,
247
+ "--user-ids",
248
+ help="Comma-separated assignee/user UUIDs.",
249
+ ),
250
+ tags_raw: Optional[str] = typer.Option(
251
+ None,
252
+ "--tags",
253
+ help="Comma-separated tags.",
254
+ ),
255
+ directions_raw: Optional[str] = typer.Option(
256
+ None,
257
+ "--directions",
258
+ help="Comma-separated directions (e.g. inbound,outbound).",
259
+ ),
260
+ flags_raw: Optional[str] = typer.Option(
261
+ None,
262
+ "--flags",
263
+ help="Comma-separated conversation flags.",
264
+ ),
265
+ score_gte: Optional[int] = typer.Option(
266
+ None,
267
+ "--score-gte",
268
+ help="Minimum CSAT score.",
269
+ ),
270
+ score_lte: Optional[int] = typer.Option(
271
+ None,
272
+ "--score-lte",
273
+ help="Maximum CSAT score.",
274
+ ),
275
+ is_test_raw: Optional[str] = typer.Option(
276
+ None,
277
+ "--is-test",
278
+ help="Filter test conversations: true/false.",
279
+ ),
280
+ conversation_ids_raw: Optional[str] = typer.Option(
281
+ None,
282
+ "--conversation-ids",
283
+ help="Optional comma-separated conversation UUIDs to scope report.",
284
+ ),
285
+ wait: bool = typer.Option(
286
+ False,
287
+ "--wait/--no-wait",
288
+ help="Poll until completed/failed.",
289
+ ),
290
+ poll_seconds: float = typer.Option(
291
+ 5.0,
292
+ "--poll-seconds",
293
+ help="Polling interval when --wait is set.",
294
+ ),
295
+ max_wait_seconds: float = typer.Option(
296
+ 600.0,
297
+ "--max-wait-seconds",
298
+ help="Max wait duration for --wait.",
299
+ ),
300
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
301
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
302
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
303
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
304
+ ) -> None:
305
+ if data_source not in {"conversations", "tickets"}:
306
+ raise typer.BadParameter("data-source must be one of: conversations, tickets.")
307
+ if poll_seconds <= 0:
308
+ raise typer.BadParameter("poll-seconds must be > 0.")
309
+ if max_wait_seconds <= 0:
310
+ raise typer.BadParameter("max-wait-seconds must be > 0.")
311
+ if score_gte is not None and score_lte is not None and score_gte > score_lte:
312
+ raise typer.BadParameter("score-gte cannot be greater than score-lte.")
313
+
314
+ agent_ids = parse_csv_list(agent_ids_raw)
315
+ topic_ids = parse_csv_list(topic_ids_raw)
316
+ intent_ids = parse_csv_list(intent_ids_raw)
317
+ user_ids = parse_csv_list(user_ids_raw)
318
+ conversation_ids = parse_csv_list(conversation_ids_raw)
319
+ resolutions = parse_csv_list(resolutions_raw)
320
+ conversation_types = parse_csv_list(conversation_types_raw)
321
+ tags = parse_csv_list(tags_raw)
322
+ directions = parse_csv_list(directions_raw)
323
+ flags = parse_csv_list(flags_raw)
324
+ is_test = parse_optional_bool(is_test_raw, field_name="is-test")
325
+
326
+ validate_uuid_list(
327
+ [*agent_ids, *topic_ids, *intent_ids, *user_ids, *conversation_ids],
328
+ field_name="uuid",
329
+ )
330
+
331
+ filters = _build_filters(
332
+ agent_ids=agent_ids,
333
+ topic_ids=topic_ids,
334
+ intent_ids=intent_ids,
335
+ resolutions=resolutions,
336
+ conversation_types=conversation_types,
337
+ user_ids=user_ids,
338
+ tags=tags,
339
+ directions=directions,
340
+ flags=flags,
341
+ score_gte=score_gte,
342
+ score_lte=score_lte,
343
+ is_test=is_test,
344
+ )
345
+
346
+ try:
347
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
348
+ base_url=base_url,
349
+ shop_id=shop_id,
350
+ api_token=api_token,
351
+ )
352
+ generated = insights_generate(
353
+ base_url=resolved_base_url,
354
+ shop_id=resolved_shop_id,
355
+ api_token=resolved_token,
356
+ instruction=query,
357
+ date_range=date_range,
358
+ filters=filters if filters else None,
359
+ conversation_ids=conversation_ids if conversation_ids else None,
360
+ data_source=data_source,
361
+ )
362
+ except APIError as exc:
363
+ typer.echo(render_api_error(exc, action="run insights report"), err=True)
364
+ raise typer.Exit(code=1) from exc
365
+
366
+ report_id = str(generated.get("reportId") or "")
367
+ if not wait or not report_id:
368
+ _emit(generated, output_json)
369
+ return
370
+
371
+ _wait_and_emit_report(
372
+ base_url=resolved_base_url,
373
+ shop_id=resolved_shop_id,
374
+ api_token=resolved_token,
375
+ report_id=report_id,
376
+ poll_seconds=poll_seconds,
377
+ max_wait_seconds=max_wait_seconds,
378
+ output_json=output_json,
379
+ )
380
+
381
+
382
+ @app.command(
383
+ "followup",
384
+ help=(
385
+ "Run a follow-up insights report on a parent report thread. Example: "
386
+ "applied-cli insights followup --report-id <uuid> --query "
387
+ "'break down refund complaints by intent' --wait"
388
+ ),
389
+ )
390
+ def followup_cmd(
391
+ report_id: str = typer.Option(
392
+ ...,
393
+ "--report-id",
394
+ "--parent-report-id",
395
+ "--id",
396
+ help="Parent insights report UUID.",
397
+ ),
398
+ query: str = typer.Option(
399
+ ...,
400
+ "--query",
401
+ "--instruction",
402
+ help="Follow-up analysis question.",
403
+ ),
404
+ date_range: Optional[str] = typer.Option(
405
+ None,
406
+ "--date-range",
407
+ help="Optional override date range, e.g. relative:-30d, or ISO start,end.",
408
+ ),
409
+ conversation_ids_raw: Optional[str] = typer.Option(
410
+ None,
411
+ "--conversation-ids",
412
+ help="Optional comma-separated conversation UUIDs to scope follow-up.",
413
+ ),
414
+ wait: bool = typer.Option(
415
+ False,
416
+ "--wait/--no-wait",
417
+ help="Poll until completed/failed.",
418
+ ),
419
+ poll_seconds: float = typer.Option(
420
+ 5.0,
421
+ "--poll-seconds",
422
+ help="Polling interval when --wait is set.",
423
+ ),
424
+ max_wait_seconds: float = typer.Option(
425
+ 600.0,
426
+ "--max-wait-seconds",
427
+ help="Max wait duration for --wait.",
428
+ ),
429
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
430
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
431
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
432
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
433
+ ) -> None:
434
+ if not query.strip():
435
+ raise typer.BadParameter("query cannot be empty.")
436
+ if poll_seconds <= 0:
437
+ raise typer.BadParameter("poll-seconds must be > 0.")
438
+ if max_wait_seconds <= 0:
439
+ raise typer.BadParameter("max-wait-seconds must be > 0.")
440
+ validate_uuid(report_id, field_name="report-id")
441
+
442
+ conversation_ids = parse_csv_list(conversation_ids_raw)
443
+ validate_uuid_list(conversation_ids, field_name="conversation-id")
444
+
445
+ try:
446
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
447
+ base_url=base_url,
448
+ shop_id=shop_id,
449
+ api_token=api_token,
450
+ )
451
+ generated = insights_followup(
452
+ base_url=resolved_base_url,
453
+ shop_id=resolved_shop_id,
454
+ api_token=resolved_token,
455
+ instruction=query,
456
+ parent_report_id=report_id,
457
+ date_range=date_range,
458
+ conversation_ids=conversation_ids if conversation_ids else None,
459
+ )
460
+ except APIError as exc:
461
+ typer.echo(render_api_error(exc, action="run insights follow-up"), err=True)
462
+ raise typer.Exit(code=1) from exc
463
+
464
+ followup_report_id = str(generated.get("reportId") or "")
465
+ if not wait or not followup_report_id:
466
+ _emit(generated, output_json)
467
+ return
468
+
469
+ _wait_and_emit_report(
470
+ base_url=resolved_base_url,
471
+ shop_id=resolved_shop_id,
472
+ api_token=resolved_token,
473
+ report_id=followup_report_id,
474
+ poll_seconds=poll_seconds,
475
+ max_wait_seconds=max_wait_seconds,
476
+ output_json=output_json,
477
+ )
478
+
479
+
480
+ @app.command(
481
+ "status",
482
+ help=(
483
+ "Check report generation status. Example: applied-cli insights status "
484
+ "--report-id <uuid>"
485
+ ),
486
+ )
487
+ def status_cmd(
488
+ report_id: str = typer.Option(
489
+ ...,
490
+ "--report-id",
491
+ "--id",
492
+ help="Insights report UUID.",
493
+ ),
494
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
495
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
496
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
497
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
498
+ ) -> None:
499
+ validate_uuid(report_id, field_name="report-id")
500
+ try:
501
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
502
+ base_url=base_url,
503
+ shop_id=shop_id,
504
+ api_token=api_token,
505
+ )
506
+ status_data = get_insights_status(
507
+ base_url=resolved_base_url,
508
+ shop_id=resolved_shop_id,
509
+ api_token=resolved_token,
510
+ report_id=report_id,
511
+ )
512
+ except APIError as exc:
513
+ typer.echo(render_api_error(exc, action="get insights status"), err=True)
514
+ raise typer.Exit(code=1) from exc
515
+ _emit(status_data, output_json)
516
+
517
+
518
+ @app.command(
519
+ "reports",
520
+ help=(
521
+ "List report history. Example: applied-cli insights reports --status completed"
522
+ ),
523
+ )
524
+ def reports_cmd(
525
+ status: Optional[str] = typer.Option(
526
+ None,
527
+ "--status",
528
+ help="Optional status filter: generating, completed, failed.",
529
+ ),
530
+ configuration_id: Optional[str] = typer.Option(
531
+ None,
532
+ "--configuration-id",
533
+ help="Optional configuration UUID filter.",
534
+ ),
535
+ parent_report_id: Optional[str] = typer.Option(
536
+ None,
537
+ "--parent-report-id",
538
+ help="Optional parent report UUID filter.",
539
+ ),
540
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
541
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
542
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
543
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
544
+ ) -> None:
545
+ if status and status not in {"generating", "completed", "failed"}:
546
+ raise typer.BadParameter("status must be one of: generating, completed, failed.")
547
+ if configuration_id:
548
+ validate_uuid(configuration_id, field_name="configuration-id")
549
+ if parent_report_id:
550
+ validate_uuid(parent_report_id, field_name="parent-report-id")
551
+
552
+ try:
553
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
554
+ base_url=base_url,
555
+ shop_id=shop_id,
556
+ api_token=api_token,
557
+ )
558
+ rows = list_insights_reports(
559
+ base_url=resolved_base_url,
560
+ shop_id=resolved_shop_id,
561
+ api_token=resolved_token,
562
+ status=status,
563
+ configuration_id=configuration_id,
564
+ parent_report_id=parent_report_id,
565
+ )
566
+ except APIError as exc:
567
+ typer.echo(render_api_error(exc, action="list insights reports"), err=True)
568
+ raise typer.Exit(code=1) from exc
569
+ _emit(rows, output_json)
570
+
571
+
572
+ @app.command(
573
+ "describe",
574
+ help=(
575
+ "Describe a report including generated findings. Example: applied-cli insights "
576
+ "describe --report-id <uuid>"
577
+ ),
578
+ )
579
+ @app.command(
580
+ "show",
581
+ help=(
582
+ "Show a report including generated findings. Example: applied-cli insights show "
583
+ "--report-id <uuid>"
584
+ ),
585
+ )
586
+ def show_cmd(
587
+ report_id: str = typer.Option(
588
+ ...,
589
+ "--report-id",
590
+ "--id",
591
+ help="Insights report UUID.",
592
+ ),
593
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
594
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
595
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
596
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
597
+ ) -> None:
598
+ validate_uuid(report_id, field_name="report-id")
599
+ try:
600
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
601
+ base_url=base_url,
602
+ shop_id=shop_id,
603
+ api_token=api_token,
604
+ )
605
+ payload = get_insights_report(
606
+ base_url=resolved_base_url,
607
+ shop_id=resolved_shop_id,
608
+ api_token=resolved_token,
609
+ report_id=report_id,
610
+ )
611
+ except APIError as exc:
612
+ typer.echo(render_api_error(exc, action="show insights report"), err=True)
613
+ raise typer.Exit(code=1) from exc
614
+ _emit(payload, output_json)