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.
applied_cli/http.py ADDED
@@ -0,0 +1,1614 @@
1
+ from typing import Any, Optional
2
+ from urllib.parse import urlencode
3
+
4
+ import httpx
5
+
6
+
7
+ class APIError(Exception):
8
+ def __init__(
9
+ self,
10
+ message: str,
11
+ status_code: int | None = None,
12
+ *,
13
+ code: str | None = None,
14
+ hint: str | None = None,
15
+ retryable: bool | None = None,
16
+ method: str | None = None,
17
+ url: str | None = None,
18
+ detail: Any = None,
19
+ suggestions: list[str] | None = None,
20
+ ) -> None:
21
+ super().__init__(message)
22
+ self.status_code = status_code
23
+ self.code = code
24
+ self.hint = hint
25
+ self.retryable = retryable
26
+ self.method = method
27
+ self.url = url
28
+ self.detail = detail
29
+ self.suggestions = suggestions or []
30
+
31
+
32
+ def _auth_headers(*, shop_id: str, api_token: str) -> dict[str, str]:
33
+ return {
34
+ "Authorization": f"Bearer {api_token}",
35
+ "X-Shop-Id": shop_id,
36
+ "Content-Type": "application/json",
37
+ }
38
+
39
+
40
+ def _token_headers(*, api_token: str) -> dict[str, str]:
41
+ return {
42
+ "Authorization": f"Bearer {api_token}",
43
+ "Content-Type": "application/json",
44
+ }
45
+
46
+
47
+ def _error_detail(response: httpx.Response) -> Any:
48
+ try:
49
+ return response.json()
50
+ except Exception:
51
+ return response.text
52
+
53
+
54
+ def _build_api_error(
55
+ *,
56
+ method: str,
57
+ url: str,
58
+ status_code: int,
59
+ detail: Any,
60
+ ) -> APIError:
61
+ code = "API_REQUEST_FAILED"
62
+ hint = "Retry after checking inputs and credentials."
63
+ retryable = status_code >= 500
64
+ suggestions: list[str] = []
65
+
66
+ if status_code == 401:
67
+ code = "AUTH_INVALID_TOKEN"
68
+ hint = "Token rejected by server. Run `applied-cli auth login` again."
69
+ retryable = False
70
+ suggestions.extend(
71
+ [
72
+ "Verify APPLIED_API_TOKEN or stored token value.",
73
+ "Run `applied-cli auth status` to validate token and shop scope.",
74
+ ]
75
+ )
76
+ elif status_code == 403:
77
+ code = "SHOP_SCOPE_FORBIDDEN"
78
+ hint = "Credentials lack permission for this shop or resource."
79
+ retryable = False
80
+ suggestions.extend(
81
+ [
82
+ "Confirm `--shop-id` matches the shop your token can access.",
83
+ "If using admin JWT/API token, ensure X-Shop-Id is set correctly.",
84
+ ]
85
+ )
86
+ elif status_code == 404:
87
+ code = "RESOURCE_NOT_FOUND"
88
+ hint = "Requested resource was not found in the selected shop scope."
89
+ retryable = False
90
+ if "/agents/" in url:
91
+ suggestions.append(
92
+ "Verify `--agent-id` belongs to the target shop and exists."
93
+ )
94
+ elif "/conversations/" in url:
95
+ suggestions.append(
96
+ "Verify `--conversation-id` belongs to the target shop and exists."
97
+ )
98
+ elif status_code == 400:
99
+ code = "INVALID_REQUEST"
100
+ hint = "Request payload is invalid for this endpoint."
101
+ retryable = False
102
+ suggestions.append("Check required fields and allowed enum values.")
103
+
104
+ detail_text = str(detail).lower()
105
+ if "/v1/conversation-benchmarks/" in url and "agent_id" in detail_text:
106
+ code = "BENCHMARK_MISSING_AGENT"
107
+ hint = "Benchmark creation requires an agent id in payload."
108
+ retryable = False
109
+ suggestions.append(
110
+ "Pass the target agent UUID when creating benchmark records."
111
+ )
112
+
113
+ return APIError(
114
+ f"Applied API request failed ({status_code}) {method} {url}",
115
+ status_code=status_code,
116
+ code=code,
117
+ hint=hint,
118
+ retryable=retryable,
119
+ method=method,
120
+ url=url,
121
+ detail=detail,
122
+ suggestions=suggestions,
123
+ )
124
+
125
+
126
+ def _request_json(
127
+ *,
128
+ method: str,
129
+ url: str,
130
+ shop_id: str,
131
+ api_token: str,
132
+ timeout_seconds: float = 20.0,
133
+ payload: dict[str, Any] | None = None,
134
+ ) -> Any:
135
+ try:
136
+ response = httpx.request(
137
+ method=method,
138
+ url=url,
139
+ headers=_auth_headers(shop_id=shop_id, api_token=api_token),
140
+ timeout=timeout_seconds,
141
+ json=payload,
142
+ )
143
+ except httpx.HTTPError as exc:
144
+ raise APIError(
145
+ f"Failed to reach Applied API: {exc}",
146
+ code="NETWORK_ERROR",
147
+ hint=(
148
+ "Check APPLIED_BASE_URL and confirm the server is reachable "
149
+ "(for local dev: `http://localhost:8000`)."
150
+ ),
151
+ retryable=True,
152
+ method=method,
153
+ url=url,
154
+ ) from exc
155
+
156
+ if response.status_code >= 400:
157
+ detail = _error_detail(response)
158
+ raise _build_api_error(
159
+ method=method,
160
+ url=url,
161
+ status_code=response.status_code,
162
+ detail=detail,
163
+ )
164
+ try:
165
+ return response.json()
166
+ except Exception as exc:
167
+ raise APIError(
168
+ f"Applied API returned non-JSON response for {method} {url}.",
169
+ code="NON_JSON_RESPONSE",
170
+ hint="Server returned an unexpected response format.",
171
+ retryable=False,
172
+ method=method,
173
+ url=url,
174
+ ) from exc
175
+
176
+
177
+ def _request_no_content(
178
+ *,
179
+ method: str,
180
+ url: str,
181
+ shop_id: str,
182
+ api_token: str,
183
+ timeout_seconds: float = 20.0,
184
+ ) -> None:
185
+ try:
186
+ response = httpx.request(
187
+ method=method,
188
+ url=url,
189
+ headers=_auth_headers(shop_id=shop_id, api_token=api_token),
190
+ timeout=timeout_seconds,
191
+ )
192
+ except httpx.HTTPError as exc:
193
+ raise APIError(
194
+ f"Failed to reach Applied API: {exc}",
195
+ code="NETWORK_ERROR",
196
+ hint=(
197
+ "Check APPLIED_BASE_URL and confirm the server is reachable "
198
+ "(for local dev: `http://localhost:8000`)."
199
+ ),
200
+ retryable=True,
201
+ method=method,
202
+ url=url,
203
+ ) from exc
204
+
205
+ if response.status_code >= 400:
206
+ detail = _error_detail(response)
207
+ raise _build_api_error(
208
+ method=method,
209
+ url=url,
210
+ status_code=response.status_code,
211
+ detail=detail,
212
+ )
213
+
214
+
215
+ def _results_list(data: Any) -> list[dict[str, Any]]:
216
+ if isinstance(data, list):
217
+ return [item for item in data if isinstance(item, dict)]
218
+ if isinstance(data, dict):
219
+ results = data.get("results")
220
+ if isinstance(results, list):
221
+ return [item for item in results if isinstance(item, dict)]
222
+ return []
223
+
224
+
225
+ def validate_api_token(
226
+ *,
227
+ base_url: str,
228
+ shop_id: str,
229
+ api_token: str,
230
+ timeout_seconds: float = 10.0,
231
+ ) -> dict[str, Any]:
232
+ url = f"{base_url}/v1/tokens/?type=api_token"
233
+ try:
234
+ response = httpx.get(
235
+ url,
236
+ headers=_auth_headers(shop_id=shop_id, api_token=api_token),
237
+ timeout=timeout_seconds,
238
+ )
239
+ except httpx.HTTPError as exc:
240
+ raise APIError(
241
+ f"Failed to reach Applied API: {exc}",
242
+ code="NETWORK_ERROR",
243
+ hint=(
244
+ "Check APPLIED_BASE_URL and confirm the server is reachable "
245
+ "(for local dev: `http://localhost:8000`)."
246
+ ),
247
+ retryable=True,
248
+ method="GET",
249
+ url=url,
250
+ ) from exc
251
+
252
+ if response.status_code >= 400:
253
+ detail = _error_detail(response)
254
+ raise APIError(
255
+ f"Token validation failed ({response.status_code}).",
256
+ status_code=response.status_code,
257
+ code="AUTH_TOKEN_VALIDATION_FAILED",
258
+ hint="Token or shop scope is invalid for this request.",
259
+ retryable=False,
260
+ method="GET",
261
+ url=url,
262
+ detail=detail,
263
+ suggestions=[
264
+ "Run `applied-cli auth login` and paste a fresh API token.",
265
+ "Verify APPLIED_SHOP_ID / --shop-id is the intended shop.",
266
+ ],
267
+ )
268
+
269
+ try:
270
+ return response.json()
271
+ except Exception as exc:
272
+ raise APIError(
273
+ "Applied API returned non-JSON validation response.",
274
+ code="NON_JSON_RESPONSE",
275
+ hint="Server returned an unexpected response format.",
276
+ retryable=False,
277
+ method="GET",
278
+ url=url,
279
+ ) from exc
280
+
281
+
282
+ def list_accessible_shops(
283
+ *,
284
+ base_url: str,
285
+ api_token: str,
286
+ timeout_seconds: float = 10.0,
287
+ ) -> list[dict[str, Any]]:
288
+ candidate_urls = [
289
+ f"{base_url}/v1/shops/accessible/",
290
+ f"{base_url}/v1/shops/",
291
+ ]
292
+ last_error: APIError | None = None
293
+ for url in candidate_urls:
294
+ try:
295
+ response = httpx.get(
296
+ url,
297
+ headers=_token_headers(api_token=api_token),
298
+ timeout=timeout_seconds,
299
+ )
300
+ except httpx.HTTPError as exc:
301
+ last_error = APIError(
302
+ f"Failed to reach Applied API: {exc}",
303
+ code="NETWORK_ERROR",
304
+ hint=(
305
+ "Check APPLIED_BASE_URL and confirm the server is reachable "
306
+ "(for local dev: `http://localhost:8000`)."
307
+ ),
308
+ retryable=True,
309
+ method="GET",
310
+ url=url,
311
+ )
312
+ continue
313
+
314
+ if response.status_code >= 400:
315
+ detail = _error_detail(response)
316
+ last_error = _build_api_error(
317
+ method="GET",
318
+ url=url,
319
+ status_code=response.status_code,
320
+ detail=detail,
321
+ )
322
+ continue
323
+
324
+ try:
325
+ payload = response.json()
326
+ except Exception as exc:
327
+ raise APIError(
328
+ "Shops listing returned non-JSON response.",
329
+ code="NON_JSON_RESPONSE",
330
+ hint="Server returned an unexpected response format.",
331
+ retryable=False,
332
+ method="GET",
333
+ url=url,
334
+ ) from exc
335
+ shops = _results_list(payload)
336
+ if shops:
337
+ return shops
338
+
339
+ if last_error is not None:
340
+ raise last_error
341
+ return []
342
+
343
+
344
+ def start_cli_device_login(
345
+ *,
346
+ base_url: str,
347
+ timeout_seconds: float = 10.0,
348
+ ) -> dict[str, Any]:
349
+ url = f"{base_url}/v1/cli/device/start/"
350
+ try:
351
+ response = httpx.post(url, timeout=timeout_seconds, json={})
352
+ except httpx.HTTPError as exc:
353
+ raise APIError(
354
+ f"Failed to reach Applied API: {exc}",
355
+ code="NETWORK_ERROR",
356
+ hint=(
357
+ "Check APPLIED_BASE_URL and confirm the server is reachable "
358
+ "(for local dev: `http://localhost:8000`)."
359
+ ),
360
+ retryable=True,
361
+ method="POST",
362
+ url=url,
363
+ ) from exc
364
+ if response.status_code >= 400:
365
+ raise _build_api_error(
366
+ method="POST",
367
+ url=url,
368
+ status_code=response.status_code,
369
+ detail=_error_detail(response),
370
+ )
371
+ try:
372
+ payload = response.json()
373
+ except Exception as exc:
374
+ raise APIError(
375
+ "Device login start returned non-JSON response.",
376
+ code="NON_JSON_RESPONSE",
377
+ hint="Server returned an unexpected response format.",
378
+ retryable=False,
379
+ method="POST",
380
+ url=url,
381
+ ) from exc
382
+ if not isinstance(payload, dict):
383
+ raise APIError("Device login start response is not an object.")
384
+ return payload
385
+
386
+
387
+ def poll_cli_device_login(
388
+ *,
389
+ base_url: str,
390
+ device_code: str,
391
+ timeout_seconds: float = 10.0,
392
+ ) -> dict[str, Any]:
393
+ url = f"{base_url}/v1/cli/device/token/"
394
+ try:
395
+ response = httpx.post(
396
+ url,
397
+ json={"device_code": device_code},
398
+ timeout=timeout_seconds,
399
+ headers={"Content-Type": "application/json"},
400
+ )
401
+ except httpx.HTTPError as exc:
402
+ raise APIError(
403
+ f"Failed to reach Applied API: {exc}",
404
+ code="NETWORK_ERROR",
405
+ hint=(
406
+ "Check APPLIED_BASE_URL and confirm the server is reachable "
407
+ "(for local dev: `http://localhost:8000`)."
408
+ ),
409
+ retryable=True,
410
+ method="POST",
411
+ url=url,
412
+ ) from exc
413
+ if response.status_code >= 400:
414
+ raise _build_api_error(
415
+ method="POST",
416
+ url=url,
417
+ status_code=response.status_code,
418
+ detail=_error_detail(response),
419
+ )
420
+ try:
421
+ payload = response.json()
422
+ except Exception as exc:
423
+ raise APIError(
424
+ "Device login poll returned non-JSON response.",
425
+ code="NON_JSON_RESPONSE",
426
+ hint="Server returned an unexpected response format.",
427
+ retryable=False,
428
+ method="POST",
429
+ url=url,
430
+ ) from exc
431
+ if not isinstance(payload, dict):
432
+ raise APIError("Device login poll response is not an object.")
433
+ return payload
434
+
435
+
436
+ def get_conversation(
437
+ *,
438
+ base_url: str,
439
+ shop_id: str,
440
+ api_token: str,
441
+ conversation_id: str,
442
+ timeout_seconds: float = 20.0,
443
+ ) -> dict[str, Any]:
444
+ data = _request_json(
445
+ method="GET",
446
+ url=f"{base_url}/v1/conversations/{conversation_id}/",
447
+ shop_id=shop_id,
448
+ api_token=api_token,
449
+ timeout_seconds=timeout_seconds,
450
+ )
451
+ if not isinstance(data, dict):
452
+ raise APIError("Conversation response is not an object.")
453
+ return data
454
+
455
+
456
+ def delete_conversation(
457
+ *,
458
+ base_url: str,
459
+ shop_id: str,
460
+ api_token: str,
461
+ conversation_id: str,
462
+ timeout_seconds: float = 20.0,
463
+ ) -> None:
464
+ _request_no_content(
465
+ method="DELETE",
466
+ url=f"{base_url}/v1/conversations/{conversation_id}/",
467
+ shop_id=shop_id,
468
+ api_token=api_token,
469
+ timeout_seconds=timeout_seconds,
470
+ )
471
+
472
+
473
+ def import_conversations_bulk(
474
+ *,
475
+ base_url: str,
476
+ shop_id: str,
477
+ api_token: str,
478
+ agent_id: str,
479
+ file_path: Optional[str] = None,
480
+ url: Optional[str] = None,
481
+ process_labels: bool = False,
482
+ timeout_seconds: float = 120.0,
483
+ ) -> dict[str, Any]:
484
+ if bool(file_path) == bool(url):
485
+ raise APIError(
486
+ "Provide exactly one source: file_path or url.",
487
+ code="INVALID_IMPORT_SOURCE",
488
+ hint="Use --file-path for local CSV or --url for remote CSV.",
489
+ retryable=False,
490
+ )
491
+
492
+ request_url = f"{base_url}/v1/conversations/bulk-upload/"
493
+ headers = {
494
+ "Authorization": f"Bearer {api_token}",
495
+ "X-Shop-Id": shop_id,
496
+ }
497
+ data = {
498
+ "agent_id": agent_id,
499
+ "process_labels": "true" if process_labels else "false",
500
+ }
501
+ files = None
502
+ stream = None
503
+ if file_path:
504
+ stream = open(file_path, "rb")
505
+ files = {"file": (file_path.split("/")[-1], stream, "text/csv")}
506
+ else:
507
+ data["url"] = str(url)
508
+
509
+ try:
510
+ response = httpx.post(
511
+ request_url,
512
+ headers=headers,
513
+ data=data,
514
+ files=files,
515
+ timeout=timeout_seconds,
516
+ )
517
+ except httpx.HTTPError as exc:
518
+ raise APIError(
519
+ f"Failed to reach Applied API: {exc}",
520
+ code="NETWORK_ERROR",
521
+ hint=(
522
+ "Check APPLIED_BASE_URL and confirm the server is reachable "
523
+ "(for local dev: `http://localhost:8000`)."
524
+ ),
525
+ retryable=True,
526
+ method="POST",
527
+ url=request_url,
528
+ suggestions=[
529
+ "Verify file path or URL is accessible from your environment.",
530
+ ],
531
+ ) from exc
532
+ finally:
533
+ if stream is not None:
534
+ stream.close()
535
+
536
+ if response.status_code >= 400:
537
+ detail = _error_detail(response)
538
+ exc = _build_api_error(
539
+ method="POST",
540
+ url=request_url,
541
+ status_code=response.status_code,
542
+ detail=detail,
543
+ )
544
+ detail_text = str(detail).lower()
545
+ if "phone" in detail_text:
546
+ exc.suggestions.extend(
547
+ [
548
+ "CONTACT_PHONE can be omitted; importer now fills safe placeholders.",
549
+ "If your CSV has malformed phones, keep them short (<=16 chars) or E.164-like (for example +15551234567).",
550
+ ]
551
+ )
552
+ exc.suggestions.extend(
553
+ [
554
+ "Expected CSV headers: ID, DATE_CREATED, SENDER, TYPE, MESSAGE_DATE, BODY, TITLE, TOPIC, INTENT, SENTIMENT, SENTIMENT_SCORE, CSAT_SCORE.",
555
+ "Optional headers: CONTACT_NAME, CONTACT_EMAIL, CONTACT_PHONE, RESOLUTION_STATUS.",
556
+ ]
557
+ )
558
+ raise exc
559
+
560
+ try:
561
+ payload = response.json()
562
+ except Exception as exc:
563
+ raise APIError(
564
+ "Bulk import returned non-JSON response.",
565
+ code="NON_JSON_RESPONSE",
566
+ hint="Server returned an unexpected response format.",
567
+ retryable=False,
568
+ method="POST",
569
+ url=request_url,
570
+ ) from exc
571
+ if not isinstance(payload, dict):
572
+ raise APIError("Bulk import response is not an object.")
573
+ return payload
574
+
575
+
576
+ def list_conversations(
577
+ *,
578
+ base_url: str,
579
+ shop_id: str,
580
+ api_token: str,
581
+ agent_id: str | None = None,
582
+ conversation_type: str | None = None,
583
+ response_type: str | None = None,
584
+ is_test: bool | None = None,
585
+ limit: int = 20,
586
+ offset: int = 0,
587
+ ordering: str = "-created_at",
588
+ timeout_seconds: float = 20.0,
589
+ ) -> list[dict[str, Any]]:
590
+ params: dict[str, str] = {
591
+ "limit": str(limit),
592
+ "offset": str(offset),
593
+ "ordering": ordering,
594
+ }
595
+ if agent_id:
596
+ params["agent_id"] = agent_id
597
+ if conversation_type:
598
+ params["type"] = conversation_type
599
+ if response_type:
600
+ params["response_type"] = response_type
601
+ if is_test is not None:
602
+ params["is_test"] = "true" if is_test else "false"
603
+ query = urlencode(params)
604
+ data = _request_json(
605
+ method="GET",
606
+ url=f"{base_url}/v1/conversations/?{query}",
607
+ shop_id=shop_id,
608
+ api_token=api_token,
609
+ timeout_seconds=timeout_seconds,
610
+ )
611
+ return _results_list(data)
612
+
613
+
614
+ def list_property_choices(
615
+ *,
616
+ base_url: str,
617
+ shop_id: str,
618
+ api_token: str,
619
+ ordering: str = "name",
620
+ timeout_seconds: float = 20.0,
621
+ ) -> list[dict[str, Any]]:
622
+ query = urlencode({"ordering": ordering})
623
+ data = _request_json(
624
+ method="GET",
625
+ url=f"{base_url}/v1/property-choices/?{query}",
626
+ shop_id=shop_id,
627
+ api_token=api_token,
628
+ timeout_seconds=timeout_seconds,
629
+ )
630
+ return _results_list(data)
631
+
632
+
633
+ def list_conversation_messages(
634
+ *,
635
+ base_url: str,
636
+ shop_id: str,
637
+ api_token: str,
638
+ conversation_id: str,
639
+ timeout_seconds: float = 20.0,
640
+ ) -> list[dict[str, Any]]:
641
+ data = _request_json(
642
+ method="GET",
643
+ url=f"{base_url}/v1/messages/?conversation_id={conversation_id}",
644
+ shop_id=shop_id,
645
+ api_token=api_token,
646
+ timeout_seconds=timeout_seconds,
647
+ )
648
+ return _results_list(data)
649
+
650
+
651
+ def list_conversation_references(
652
+ *,
653
+ base_url: str,
654
+ shop_id: str,
655
+ api_token: str,
656
+ conversation_id: str,
657
+ timeout_seconds: float = 20.0,
658
+ ) -> list[dict[str, Any]]:
659
+ data = _request_json(
660
+ method="GET",
661
+ url=f"{base_url}/v1/conversations/{conversation_id}/references/",
662
+ shop_id=shop_id,
663
+ api_token=api_token,
664
+ timeout_seconds=timeout_seconds,
665
+ )
666
+ return _results_list(data)
667
+
668
+
669
+ def list_conversation_benchmarks(
670
+ *,
671
+ base_url: str,
672
+ shop_id: str,
673
+ api_token: str,
674
+ agent_id: str | None = None,
675
+ limit: int = 50,
676
+ ordering: str = "-created_at",
677
+ timeout_seconds: float = 20.0,
678
+ ) -> list[dict[str, Any]]:
679
+ params: dict[str, str] = {
680
+ "limit": str(limit),
681
+ "ordering": ordering,
682
+ }
683
+ if agent_id:
684
+ params["agent"] = agent_id
685
+ query = urlencode(params)
686
+ data = _request_json(
687
+ method="GET",
688
+ url=f"{base_url}/v1/conversation-benchmarks/?{query}",
689
+ shop_id=shop_id,
690
+ api_token=api_token,
691
+ timeout_seconds=timeout_seconds,
692
+ )
693
+ return _results_list(data)
694
+
695
+
696
+ def get_conversation_benchmark(
697
+ *,
698
+ base_url: str,
699
+ shop_id: str,
700
+ api_token: str,
701
+ benchmark_id: str,
702
+ timeout_seconds: float = 20.0,
703
+ ) -> dict[str, Any]:
704
+ data = _request_json(
705
+ method="GET",
706
+ url=f"{base_url}/v1/conversation-benchmarks/{benchmark_id}/",
707
+ shop_id=shop_id,
708
+ api_token=api_token,
709
+ timeout_seconds=timeout_seconds,
710
+ )
711
+ if not isinstance(data, dict):
712
+ raise APIError("Benchmark response is not an object.")
713
+ return data
714
+
715
+
716
+ def create_conversation_benchmark(
717
+ *,
718
+ base_url: str,
719
+ shop_id: str,
720
+ api_token: str,
721
+ agent_id: str,
722
+ name: str,
723
+ description: str,
724
+ timeout_seconds: float = 20.0,
725
+ ) -> dict[str, Any]:
726
+ data = _request_json(
727
+ method="POST",
728
+ url=f"{base_url}/v1/conversation-benchmarks/",
729
+ shop_id=shop_id,
730
+ api_token=api_token,
731
+ timeout_seconds=timeout_seconds,
732
+ payload={
733
+ "agent": agent_id,
734
+ "name": name,
735
+ "description": description,
736
+ },
737
+ )
738
+ if not isinstance(data, dict):
739
+ raise APIError("Create benchmark response is not an object.")
740
+ return data
741
+
742
+
743
+ def delete_conversation_benchmark(
744
+ *,
745
+ base_url: str,
746
+ shop_id: str,
747
+ api_token: str,
748
+ benchmark_id: str,
749
+ timeout_seconds: float = 20.0,
750
+ ) -> None:
751
+ _request_no_content(
752
+ method="DELETE",
753
+ url=f"{base_url}/v1/conversation-benchmarks/{benchmark_id}/",
754
+ shop_id=shop_id,
755
+ api_token=api_token,
756
+ timeout_seconds=timeout_seconds,
757
+ )
758
+
759
+
760
+ def list_conversation_scenarios(
761
+ *,
762
+ base_url: str,
763
+ shop_id: str,
764
+ api_token: str,
765
+ agent_id: str | None = None,
766
+ name: str | None = None,
767
+ benchmark_id: str | None = None,
768
+ pass_status: str | None = None,
769
+ limit: int = 50,
770
+ ordering: str = "-created_at",
771
+ timeout_seconds: float = 20.0,
772
+ ) -> list[dict[str, Any]]:
773
+ params: dict[str, str] = {
774
+ "limit": str(limit),
775
+ "ordering": ordering,
776
+ }
777
+ if agent_id:
778
+ params["agent"] = agent_id
779
+ if name:
780
+ params["name"] = name
781
+ if benchmark_id:
782
+ params["benchmark"] = benchmark_id
783
+ if pass_status:
784
+ params["pass_status"] = pass_status
785
+ query = urlencode(params)
786
+ data = _request_json(
787
+ method="GET",
788
+ url=f"{base_url}/v1/conversation-scenarios/?{query}",
789
+ shop_id=shop_id,
790
+ api_token=api_token,
791
+ timeout_seconds=timeout_seconds,
792
+ )
793
+ return _results_list(data)
794
+
795
+
796
+ def create_conversation_scenario(
797
+ *,
798
+ base_url: str,
799
+ shop_id: str,
800
+ api_token: str,
801
+ agent_id: str,
802
+ benchmark_id: str | None,
803
+ name: str,
804
+ input_conversation_id: str,
805
+ timeout_seconds: float = 20.0,
806
+ ) -> dict[str, Any]:
807
+ payload: dict[str, Any] = {
808
+ "agent_id": agent_id,
809
+ "name": name,
810
+ "input_conversation_id": input_conversation_id,
811
+ }
812
+ if benchmark_id:
813
+ payload["benchmark_ids"] = [benchmark_id]
814
+ data = _request_json(
815
+ method="POST",
816
+ url=f"{base_url}/v1/conversation-scenarios/",
817
+ shop_id=shop_id,
818
+ api_token=api_token,
819
+ timeout_seconds=timeout_seconds,
820
+ payload=payload,
821
+ )
822
+ if not isinstance(data, dict):
823
+ raise APIError("Create scenario response is not an object.")
824
+ return data
825
+
826
+
827
+ def patch_conversation_scenario(
828
+ *,
829
+ base_url: str,
830
+ shop_id: str,
831
+ api_token: str,
832
+ scenario_id: str,
833
+ payload: dict[str, Any],
834
+ timeout_seconds: float = 20.0,
835
+ ) -> dict[str, Any]:
836
+ data = _request_json(
837
+ method="PATCH",
838
+ url=f"{base_url}/v1/conversation-scenarios/{scenario_id}/",
839
+ shop_id=shop_id,
840
+ api_token=api_token,
841
+ timeout_seconds=timeout_seconds,
842
+ payload=payload,
843
+ )
844
+ if not isinstance(data, dict):
845
+ raise APIError("Patch scenario response is not an object.")
846
+ return data
847
+
848
+
849
+ def delete_conversation_scenario(
850
+ *,
851
+ base_url: str,
852
+ shop_id: str,
853
+ api_token: str,
854
+ scenario_id: str,
855
+ timeout_seconds: float = 20.0,
856
+ ) -> None:
857
+ _request_no_content(
858
+ method="DELETE",
859
+ url=f"{base_url}/v1/conversation-scenarios/{scenario_id}/",
860
+ shop_id=shop_id,
861
+ api_token=api_token,
862
+ timeout_seconds=timeout_seconds,
863
+ )
864
+
865
+
866
+ def get_conversation_scenario(
867
+ *,
868
+ base_url: str,
869
+ shop_id: str,
870
+ api_token: str,
871
+ scenario_id: str,
872
+ timeout_seconds: float = 20.0,
873
+ ) -> dict[str, Any]:
874
+ data = _request_json(
875
+ method="GET",
876
+ url=f"{base_url}/v1/conversation-scenarios/{scenario_id}/",
877
+ shop_id=shop_id,
878
+ api_token=api_token,
879
+ timeout_seconds=timeout_seconds,
880
+ )
881
+ if not isinstance(data, dict):
882
+ raise APIError("Get scenario response is not an object.")
883
+ return data
884
+
885
+
886
+ def list_scenario_runs(
887
+ *,
888
+ base_url: str,
889
+ shop_id: str,
890
+ api_token: str,
891
+ scenario_id: str,
892
+ latest_only: bool = True,
893
+ timeout_seconds: float = 20.0,
894
+ ) -> list[dict[str, Any]]:
895
+ latest_value = "true" if latest_only else "false"
896
+ data = _request_json(
897
+ method="GET",
898
+ url=f"{base_url}/v1/scenario-runs/?scenario={scenario_id}&latest={latest_value}",
899
+ shop_id=shop_id,
900
+ api_token=api_token,
901
+ timeout_seconds=timeout_seconds,
902
+ )
903
+ return _results_list(data)
904
+
905
+
906
+ def create_scenario_run(
907
+ *,
908
+ base_url: str,
909
+ shop_id: str,
910
+ api_token: str,
911
+ scenario_id: str,
912
+ output_conversation_id: str,
913
+ timeout_seconds: float = 20.0,
914
+ ) -> dict[str, Any]:
915
+ data = _request_json(
916
+ method="POST",
917
+ url=f"{base_url}/v1/scenario-runs/",
918
+ shop_id=shop_id,
919
+ api_token=api_token,
920
+ timeout_seconds=timeout_seconds,
921
+ payload={
922
+ "scenario": scenario_id,
923
+ "output_conversation_id": output_conversation_id,
924
+ },
925
+ )
926
+ if not isinstance(data, dict):
927
+ raise APIError("Create scenario run response is not an object.")
928
+ return data
929
+
930
+
931
+ def patch_scenario_run(
932
+ *,
933
+ base_url: str,
934
+ shop_id: str,
935
+ api_token: str,
936
+ run_id: str,
937
+ payload: dict[str, Any],
938
+ timeout_seconds: float = 20.0,
939
+ ) -> dict[str, Any]:
940
+ data = _request_json(
941
+ method="PATCH",
942
+ url=f"{base_url}/v1/scenario-runs/{run_id}/",
943
+ shop_id=shop_id,
944
+ api_token=api_token,
945
+ timeout_seconds=timeout_seconds,
946
+ payload=payload,
947
+ )
948
+ if not isinstance(data, dict):
949
+ raise APIError("Patch scenario run response is not an object.")
950
+ return data
951
+
952
+
953
+ def get_scenario_run(
954
+ *,
955
+ base_url: str,
956
+ shop_id: str,
957
+ api_token: str,
958
+ run_id: str,
959
+ timeout_seconds: float = 20.0,
960
+ ) -> dict[str, Any]:
961
+ data = _request_json(
962
+ method="GET",
963
+ url=f"{base_url}/v1/scenario-runs/{run_id}/",
964
+ shop_id=shop_id,
965
+ api_token=api_token,
966
+ timeout_seconds=timeout_seconds,
967
+ )
968
+ if not isinstance(data, dict):
969
+ raise APIError("Get scenario run response is not an object.")
970
+ return data
971
+
972
+
973
+ def create_agent(
974
+ *,
975
+ base_url: str,
976
+ shop_id: str,
977
+ api_token: str,
978
+ payload: dict[str, Any],
979
+ timeout_seconds: float = 20.0,
980
+ ) -> dict[str, Any]:
981
+ data = _request_json(
982
+ method="POST",
983
+ url=f"{base_url}/v1/agents/",
984
+ shop_id=shop_id,
985
+ api_token=api_token,
986
+ timeout_seconds=timeout_seconds,
987
+ payload=payload,
988
+ )
989
+ if not isinstance(data, dict):
990
+ raise APIError("Create agent response is not an object.")
991
+ return data
992
+
993
+
994
+ def get_agent(
995
+ *,
996
+ base_url: str,
997
+ shop_id: str,
998
+ api_token: str,
999
+ agent_id: str,
1000
+ timeout_seconds: float = 20.0,
1001
+ ) -> dict[str, Any]:
1002
+ data = _request_json(
1003
+ method="GET",
1004
+ url=f"{base_url}/v1/agents/{agent_id}/",
1005
+ shop_id=shop_id,
1006
+ api_token=api_token,
1007
+ timeout_seconds=timeout_seconds,
1008
+ )
1009
+ if not isinstance(data, dict):
1010
+ raise APIError("Get agent response is not an object.")
1011
+ return data
1012
+
1013
+
1014
+ def list_agents(
1015
+ *,
1016
+ base_url: str,
1017
+ shop_id: str,
1018
+ api_token: str,
1019
+ limit: int = 100,
1020
+ ordering: str = "-created_at",
1021
+ timeout_seconds: float = 20.0,
1022
+ ) -> list[dict[str, Any]]:
1023
+ params = urlencode({"limit": str(limit), "ordering": ordering})
1024
+ data = _request_json(
1025
+ method="GET",
1026
+ url=f"{base_url}/v1/agents/?{params}",
1027
+ shop_id=shop_id,
1028
+ api_token=api_token,
1029
+ timeout_seconds=timeout_seconds,
1030
+ )
1031
+ return _results_list(data)
1032
+
1033
+
1034
+ def update_agent(
1035
+ *,
1036
+ base_url: str,
1037
+ shop_id: str,
1038
+ api_token: str,
1039
+ agent_id: str,
1040
+ payload: dict[str, Any],
1041
+ timeout_seconds: float = 20.0,
1042
+ ) -> dict[str, Any]:
1043
+ data = _request_json(
1044
+ method="PATCH",
1045
+ url=f"{base_url}/v1/agents/{agent_id}/",
1046
+ shop_id=shop_id,
1047
+ api_token=api_token,
1048
+ timeout_seconds=timeout_seconds,
1049
+ payload=payload,
1050
+ )
1051
+ if not isinstance(data, dict):
1052
+ raise APIError("Update agent response is not an object.")
1053
+ return data
1054
+
1055
+
1056
+ def list_responses(
1057
+ *,
1058
+ base_url: str,
1059
+ shop_id: str,
1060
+ api_token: str,
1061
+ agent_id: str | None = None,
1062
+ response_type: str | None = None,
1063
+ active: bool | None = None,
1064
+ limit: int = 100,
1065
+ ordering: str = "-updated_at",
1066
+ timeout_seconds: float = 20.0,
1067
+ ) -> list[dict[str, Any]]:
1068
+ params: dict[str, str] = {
1069
+ "limit": str(limit),
1070
+ "ordering": ordering,
1071
+ }
1072
+ if agent_id:
1073
+ params["agent_id"] = agent_id
1074
+ if response_type:
1075
+ params["type"] = response_type
1076
+ if active is not None:
1077
+ params["active"] = "true" if active else "false"
1078
+ query = urlencode(params)
1079
+ data = _request_json(
1080
+ method="GET",
1081
+ url=f"{base_url}/v1/responses/?{query}",
1082
+ shop_id=shop_id,
1083
+ api_token=api_token,
1084
+ timeout_seconds=timeout_seconds,
1085
+ )
1086
+ return _results_list(data)
1087
+
1088
+
1089
+ def get_response(
1090
+ *,
1091
+ base_url: str,
1092
+ shop_id: str,
1093
+ api_token: str,
1094
+ response_id: str,
1095
+ timeout_seconds: float = 20.0,
1096
+ ) -> dict[str, Any]:
1097
+ data = _request_json(
1098
+ method="GET",
1099
+ url=f"{base_url}/v1/responses/{response_id}/",
1100
+ shop_id=shop_id,
1101
+ api_token=api_token,
1102
+ timeout_seconds=timeout_seconds,
1103
+ )
1104
+ if not isinstance(data, dict):
1105
+ raise APIError("Get response response is not an object.")
1106
+ return data
1107
+
1108
+
1109
+ def create_response(
1110
+ *,
1111
+ base_url: str,
1112
+ shop_id: str,
1113
+ api_token: str,
1114
+ payload: dict[str, Any],
1115
+ timeout_seconds: float = 20.0,
1116
+ ) -> dict[str, Any]:
1117
+ data = _request_json(
1118
+ method="POST",
1119
+ url=f"{base_url}/v1/responses/",
1120
+ shop_id=shop_id,
1121
+ api_token=api_token,
1122
+ timeout_seconds=timeout_seconds,
1123
+ payload=payload,
1124
+ )
1125
+ if not isinstance(data, dict):
1126
+ raise APIError("Create response response is not an object.")
1127
+ return data
1128
+
1129
+
1130
+ def patch_response(
1131
+ *,
1132
+ base_url: str,
1133
+ shop_id: str,
1134
+ api_token: str,
1135
+ response_id: str,
1136
+ payload: dict[str, Any],
1137
+ timeout_seconds: float = 20.0,
1138
+ ) -> dict[str, Any]:
1139
+ data = _request_json(
1140
+ method="PATCH",
1141
+ url=f"{base_url}/v1/responses/{response_id}/",
1142
+ shop_id=shop_id,
1143
+ api_token=api_token,
1144
+ timeout_seconds=timeout_seconds,
1145
+ payload=payload,
1146
+ )
1147
+ if not isinstance(data, dict):
1148
+ raise APIError("Patch response response is not an object.")
1149
+ return data
1150
+
1151
+
1152
+ def simulate_agent_conversation(
1153
+ *,
1154
+ base_url: str,
1155
+ shop_id: str,
1156
+ api_token: str,
1157
+ agent_id: str,
1158
+ payload: dict[str, Any],
1159
+ timeout_seconds: float = 60.0,
1160
+ ) -> dict[str, Any]:
1161
+ data = _request_json(
1162
+ method="POST",
1163
+ url=f"{base_url}/v2/agents/{agent_id}/simulate/",
1164
+ shop_id=shop_id,
1165
+ api_token=api_token,
1166
+ timeout_seconds=timeout_seconds,
1167
+ payload=payload,
1168
+ )
1169
+ if not isinstance(data, dict):
1170
+ raise APIError("Simulate response is not an object.")
1171
+ return data
1172
+
1173
+
1174
+ def insights_generate(
1175
+ *,
1176
+ base_url: str,
1177
+ shop_id: str,
1178
+ api_token: str,
1179
+ instruction: str | None = None,
1180
+ date_range: str | None = None,
1181
+ filters: dict[str, Any] | None = None,
1182
+ conversation_ids: list[str] | None = None,
1183
+ data_source: str | None = None,
1184
+ timeout_seconds: float = 30.0,
1185
+ ) -> dict[str, Any]:
1186
+ payload: dict[str, Any] = {}
1187
+ if instruction is not None:
1188
+ payload["instruction"] = instruction
1189
+ if date_range is not None:
1190
+ payload["date_range"] = date_range
1191
+ if filters is not None:
1192
+ payload["filters"] = filters
1193
+ if conversation_ids is not None:
1194
+ payload["conversation_ids"] = conversation_ids
1195
+ if data_source is not None:
1196
+ payload["data_source"] = data_source
1197
+
1198
+ data = _request_json(
1199
+ method="POST",
1200
+ url=f"{base_url}/v1/conversations/insights-generate/",
1201
+ shop_id=shop_id,
1202
+ api_token=api_token,
1203
+ timeout_seconds=timeout_seconds,
1204
+ payload=payload,
1205
+ )
1206
+ if not isinstance(data, dict):
1207
+ raise APIError("Insights generate response is not an object.")
1208
+ return data
1209
+
1210
+
1211
+ def insights_followup(
1212
+ *,
1213
+ base_url: str,
1214
+ shop_id: str,
1215
+ api_token: str,
1216
+ instruction: str,
1217
+ parent_report_id: str,
1218
+ date_range: str | None = None,
1219
+ conversation_ids: list[str] | None = None,
1220
+ timeout_seconds: float = 30.0,
1221
+ ) -> dict[str, Any]:
1222
+ payload: dict[str, Any] = {
1223
+ "instruction": instruction,
1224
+ "parent_report_id": parent_report_id,
1225
+ }
1226
+ if date_range is not None:
1227
+ payload["date_range"] = date_range
1228
+ if conversation_ids is not None:
1229
+ payload["conversation_ids"] = conversation_ids
1230
+
1231
+ data = _request_json(
1232
+ method="POST",
1233
+ url=f"{base_url}/v1/conversations/insights-followup/",
1234
+ shop_id=shop_id,
1235
+ api_token=api_token,
1236
+ timeout_seconds=timeout_seconds,
1237
+ payload=payload,
1238
+ )
1239
+ if not isinstance(data, dict):
1240
+ raise APIError("Insights followup response is not an object.")
1241
+ return data
1242
+
1243
+
1244
+ def get_insights_status(
1245
+ *,
1246
+ base_url: str,
1247
+ shop_id: str,
1248
+ api_token: str,
1249
+ report_id: str,
1250
+ timeout_seconds: float = 20.0,
1251
+ ) -> dict[str, Any]:
1252
+ data = _request_json(
1253
+ method="GET",
1254
+ url=f"{base_url}/v1/conversations/insights-status/{report_id}/",
1255
+ shop_id=shop_id,
1256
+ api_token=api_token,
1257
+ timeout_seconds=timeout_seconds,
1258
+ )
1259
+ if not isinstance(data, dict):
1260
+ raise APIError("Insights status response is not an object.")
1261
+ return data
1262
+
1263
+
1264
+ def list_insights_reports(
1265
+ *,
1266
+ base_url: str,
1267
+ shop_id: str,
1268
+ api_token: str,
1269
+ status: str | None = None,
1270
+ configuration_id: str | None = None,
1271
+ parent_report_id: str | None = None,
1272
+ timeout_seconds: float = 20.0,
1273
+ ) -> list[dict[str, Any]]:
1274
+ params: dict[str, str] = {}
1275
+ if status:
1276
+ params["status"] = status
1277
+ if configuration_id:
1278
+ params["configuration_id"] = configuration_id
1279
+ if parent_report_id:
1280
+ params["parent_report_id"] = parent_report_id
1281
+
1282
+ query = f"?{urlencode(params)}" if params else ""
1283
+ data = _request_json(
1284
+ method="GET",
1285
+ url=f"{base_url}/v1/conversations/insights-reports/{query}",
1286
+ shop_id=shop_id,
1287
+ api_token=api_token,
1288
+ timeout_seconds=timeout_seconds,
1289
+ )
1290
+ if not isinstance(data, dict):
1291
+ raise APIError("Insights reports response is not an object.")
1292
+ return _results_list(data.get("reports"))
1293
+
1294
+
1295
+ def get_insights_report(
1296
+ *,
1297
+ base_url: str,
1298
+ shop_id: str,
1299
+ api_token: str,
1300
+ report_id: str,
1301
+ timeout_seconds: float = 20.0,
1302
+ ) -> dict[str, Any]:
1303
+ data = _request_json(
1304
+ method="GET",
1305
+ url=f"{base_url}/v1/conversations/insights-reports/{report_id}/",
1306
+ shop_id=shop_id,
1307
+ api_token=api_token,
1308
+ timeout_seconds=timeout_seconds,
1309
+ )
1310
+ if not isinstance(data, dict):
1311
+ raise APIError("Insights report response is not an object.")
1312
+ return data
1313
+
1314
+
1315
+ # ---------------------------------------------------------------------------
1316
+ # Shop management
1317
+ # ---------------------------------------------------------------------------
1318
+
1319
+
1320
+ def create_shop(
1321
+ *,
1322
+ base_url: str,
1323
+ shop_id: str,
1324
+ api_token: str,
1325
+ name: str,
1326
+ timeout_seconds: float = 30.0,
1327
+ ) -> dict[str, Any]:
1328
+ """Create a new shop.
1329
+
1330
+ ``shop_id`` is the *admin* shop's UUID used in the ``X-Shop-Id`` header for
1331
+ authentication. The backend restricts this endpoint to Applied's main/demo
1332
+ team IDs and auto-mints a ``setup_token`` for the new shop which is returned
1333
+ in the response (only returned at creation time).
1334
+ """
1335
+ data = _request_json(
1336
+ method="POST",
1337
+ url=f"{base_url}/v1/shops/",
1338
+ shop_id=shop_id,
1339
+ api_token=api_token,
1340
+ timeout_seconds=timeout_seconds,
1341
+ payload={"name": name},
1342
+ )
1343
+ if not isinstance(data, dict):
1344
+ raise APIError("Create shop response is not an object.")
1345
+ return data
1346
+
1347
+
1348
+ def create_content_source(
1349
+ *,
1350
+ base_url: str,
1351
+ shop_id: str,
1352
+ api_token: str,
1353
+ url: str,
1354
+ title: Optional[str] = None,
1355
+ timeout_seconds: float = 30.0,
1356
+ ) -> dict[str, Any]:
1357
+ """Create a website-type knowledge-base content source for a shop."""
1358
+ payload: dict[str, Any] = {
1359
+ "type": "website",
1360
+ "source": url,
1361
+ }
1362
+ if title:
1363
+ payload["title"] = title
1364
+ data = _request_json(
1365
+ method="POST",
1366
+ url=f"{base_url}/v1/content-source/",
1367
+ shop_id=shop_id,
1368
+ api_token=api_token,
1369
+ timeout_seconds=timeout_seconds,
1370
+ payload=payload,
1371
+ )
1372
+ if not isinstance(data, dict):
1373
+ raise APIError("Create content source response is not an object.")
1374
+ return data
1375
+
1376
+
1377
+ def create_property_choice(
1378
+ *,
1379
+ base_url: str,
1380
+ shop_id: str,
1381
+ api_token: str,
1382
+ name: str,
1383
+ description: str = "",
1384
+ parent_choice_id: Optional[str] = None,
1385
+ timeout_seconds: float = 15.0,
1386
+ ) -> dict[str, Any]:
1387
+ """Create a topic (no parent) or intent (with parent) in the shop taxonomy.
1388
+
1389
+ Topics and intents are both ``PropertyChoice`` objects. A topic has no
1390
+ ``parent_choice``; an intent has ``parent_choice`` set to its topic's ID.
1391
+ """
1392
+ payload: dict[str, Any] = {
1393
+ "name": name,
1394
+ "color": "blue",
1395
+ "field_id": None,
1396
+ }
1397
+ if description:
1398
+ payload["description"] = description
1399
+ if parent_choice_id:
1400
+ payload["parent_choice_id"] = parent_choice_id
1401
+ data = _request_json(
1402
+ method="POST",
1403
+ url=f"{base_url}/v1/property-choices/",
1404
+ shop_id=shop_id,
1405
+ api_token=api_token,
1406
+ timeout_seconds=timeout_seconds,
1407
+ payload=payload,
1408
+ )
1409
+ if not isinstance(data, dict):
1410
+ raise APIError("Create property choice response is not an object.")
1411
+ return data
1412
+
1413
+
1414
+ def check_superuser(
1415
+ *,
1416
+ base_url: str,
1417
+ shop_id: str,
1418
+ api_token: str,
1419
+ timeout_seconds: float = 10.0,
1420
+ ) -> bool:
1421
+ """Return True if the current API token belongs to a superuser."""
1422
+ data = _request_json(
1423
+ method="GET",
1424
+ url=f"{base_url}/v1/users/check-superuser/",
1425
+ shop_id=shop_id,
1426
+ api_token=api_token,
1427
+ timeout_seconds=timeout_seconds,
1428
+ )
1429
+ return bool(data.get("is_superuser"))
1430
+
1431
+
1432
+ def populate_demo_shop(
1433
+ *,
1434
+ base_url: str,
1435
+ shop_id: str,
1436
+ api_token: str,
1437
+ target_shop_id: str,
1438
+ distribution: list[dict[str, Any]],
1439
+ date_from: str,
1440
+ date_to: str,
1441
+ num_conversations: int = 20,
1442
+ delete_previous: bool = False,
1443
+ max_turns: int = 3,
1444
+ timeout_seconds: float = 60.0,
1445
+ ) -> dict[str, Any]:
1446
+ """Enqueue demo simulated conversations for a shop.
1447
+
1448
+ Requires a superuser API token or JWT passed as ``api_token``.
1449
+ ``shop_id`` is the *admin* shop ID used in the ``X-Shop-Id`` header.
1450
+ ``target_shop_id`` is the shop to populate and is sent in the request body.
1451
+ """
1452
+ payload: dict[str, Any] = {
1453
+ "shop_id": target_shop_id,
1454
+ "distribution": distribution,
1455
+ "date_from": date_from,
1456
+ "date_to": date_to,
1457
+ "num_conversations": num_conversations,
1458
+ "delete_previous": delete_previous,
1459
+ "max_turns": max_turns,
1460
+ }
1461
+ data = _request_json(
1462
+ method="POST",
1463
+ url=f"{base_url}/v1/users/populate-demo-shop/",
1464
+ shop_id=shop_id,
1465
+ api_token=api_token,
1466
+ timeout_seconds=timeout_seconds,
1467
+ payload=payload,
1468
+ )
1469
+ if not isinstance(data, dict):
1470
+ raise APIError("Populate demo shop response is not an object.")
1471
+ return data
1472
+
1473
+
1474
+ _ESCALATION_FLOW_TEMPLATE: dict[str, Any] = {
1475
+ "name": "escalate_conversation",
1476
+ "description": "",
1477
+ "prompt": "",
1478
+ "trigger": "llm.call",
1479
+ "sync_enabled": False,
1480
+ "resync_schedule": "",
1481
+ "type": "operational",
1482
+ "status": "Active",
1483
+ "nodes": [
1484
+ {
1485
+ "id": "node-trigger",
1486
+ "name": "trigger",
1487
+ "description": "",
1488
+ "prompt": "",
1489
+ "metadata": {
1490
+ "position": {"x": 0, "y": 0},
1491
+ "input_schema": [
1492
+ {
1493
+ "list": False,
1494
+ "name": "name",
1495
+ "type": "str",
1496
+ "required": True,
1497
+ "description": "user's name",
1498
+ },
1499
+ {
1500
+ "list": False,
1501
+ "name": "email",
1502
+ "type": "str",
1503
+ "required": True,
1504
+ "description": "user's email",
1505
+ },
1506
+ ],
1507
+ "metric_properties": [],
1508
+ },
1509
+ },
1510
+ {
1511
+ "id": "node-escalate",
1512
+ "name": "_escalate_conversation",
1513
+ "description": "",
1514
+ "prompt": "",
1515
+ "metadata": {
1516
+ "name": "{trigger.name}",
1517
+ "tags": [],
1518
+ "email": "{trigger.email}",
1519
+ "position": {"x": 546.47, "y": 23.69},
1520
+ "ticket_name": "{trigger.conversation.title}",
1521
+ "escalation_mode": "default",
1522
+ "ticket_priority": None,
1523
+ "should_yield_default_response": False,
1524
+ "append_summary_to_ticket_description": False,
1525
+ "append_transcript_to_ticket_description": False,
1526
+ },
1527
+ },
1528
+ ],
1529
+ "edges": [
1530
+ {
1531
+ "id": "edge-1",
1532
+ "source_node_id": "node-trigger",
1533
+ "target_node_id": "node-escalate",
1534
+ "source_result": None,
1535
+ "target_argument": None,
1536
+ "label": None,
1537
+ }
1538
+ ],
1539
+ }
1540
+
1541
+
1542
+ def create_escalation_flow(
1543
+ *,
1544
+ base_url: str,
1545
+ shop_id: str,
1546
+ api_token: str,
1547
+ agent_id: str,
1548
+ timeout_seconds: float = 30.0,
1549
+ ) -> dict[str, Any]:
1550
+ """Import the silent escalation flow for an email agent and activate it.
1551
+
1552
+ The flow triggers on LLM call and silently escalates the conversation
1553
+ (should_yield_default_response=False) without sending a reply to the customer.
1554
+ """
1555
+ import io
1556
+ import json as _json
1557
+ import uuid as _uuid
1558
+
1559
+ flow_json = _json.dumps(_ESCALATION_FLOW_TEMPLATE).encode("utf-8")
1560
+ filename = f"escalate_conversation_{_uuid.uuid4().hex[:8]}.json"
1561
+
1562
+ # Do NOT set Content-Type — httpx sets it automatically for multipart uploads
1563
+ headers = {
1564
+ "Authorization": f"Bearer {api_token}",
1565
+ "X-Shop-Id": shop_id,
1566
+ }
1567
+ url = f"{base_url}/v1/flows/import/"
1568
+
1569
+ try:
1570
+ resp = httpx.post(
1571
+ url,
1572
+ headers=headers,
1573
+ files={"file": (filename, io.BytesIO(flow_json), "application/json")},
1574
+ data={"agent_id": agent_id},
1575
+ timeout=timeout_seconds,
1576
+ )
1577
+ except httpx.HTTPError as exc:
1578
+ raise APIError(
1579
+ f"Network error creating escalation flow: {exc}",
1580
+ code="NETWORK_ERROR",
1581
+ retryable=True,
1582
+ method="POST",
1583
+ url=url,
1584
+ ) from exc
1585
+
1586
+ if resp.status_code >= 400:
1587
+ detail = _error_detail(resp)
1588
+ raise _build_api_error(method="POST", url=url, status_code=resp.status_code, detail=detail)
1589
+
1590
+ flow_data = resp.json()
1591
+ if not isinstance(flow_data, dict):
1592
+ raise APIError("Escalation flow import response is not an object.")
1593
+
1594
+ flow_id = flow_data.get("id") or flow_data.get("flowId")
1595
+ if not flow_id:
1596
+ raise APIError("Escalation flow import response missing id.")
1597
+
1598
+ # The template embeds "status": "Active" so the flow is usually already active
1599
+ # after import. Attempt activation in case the backend overrides to DRAFT, but
1600
+ # treat any error as non-fatal.
1601
+ try:
1602
+ activate_data = _request_json(
1603
+ method="PATCH",
1604
+ url=f"{base_url}/v1/flows/{flow_id}/",
1605
+ shop_id=shop_id,
1606
+ api_token=api_token,
1607
+ timeout_seconds=timeout_seconds,
1608
+ payload={"status": "active"},
1609
+ )
1610
+ return activate_data if isinstance(activate_data, dict) else flow_data
1611
+ except APIError:
1612
+ # Flow was imported; activation PATCH may fail if already active or
1613
+ # the endpoint requires additional fields. Return import data as-is.
1614
+ return flow_data