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/__init__.py +2 -0
- applied_cli/auth_store.py +263 -0
- applied_cli/commands/__init__.py +2 -0
- applied_cli/commands/_hints.py +11 -0
- applied_cli/commands/_normalize.py +79 -0
- applied_cli/commands/_parsers.py +58 -0
- applied_cli/commands/_ui.py +33 -0
- applied_cli/commands/agent.py +1231 -0
- applied_cli/commands/auth.py +739 -0
- applied_cli/commands/chat.py +379 -0
- applied_cli/commands/coverage.py +348 -0
- applied_cli/commands/discover.py +1006 -0
- applied_cli/commands/fix.py +1204 -0
- applied_cli/commands/insights.py +614 -0
- applied_cli/commands/intents.py +447 -0
- applied_cli/commands/rate.py +508 -0
- applied_cli/commands/responses.py +604 -0
- applied_cli/commands/shop.py +1757 -0
- applied_cli/commands/simulate.py +330 -0
- applied_cli/commands/spec.py +238 -0
- applied_cli/config.py +50 -0
- applied_cli/error_reporting.py +38 -0
- applied_cli/http.py +1614 -0
- applied_cli/main.py +90 -0
- applied_cli/mcp_server.py +738 -0
- applied_cli/presets/demo.yaml +170 -0
- applied_cli/runtime.py +53 -0
- applied_cli/shop_spec.py +398 -0
- applied_cli/spec_workflow.py +432 -0
- applied_cli-0.1.0.dist-info/METADATA +176 -0
- applied_cli-0.1.0.dist-info/RECORD +34 -0
- applied_cli-0.1.0.dist-info/WHEEL +5 -0
- applied_cli-0.1.0.dist-info/entry_points.txt +3 -0
- applied_cli-0.1.0.dist-info/top_level.txt +1 -0
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
|