applied-cli 0.5.73__tar.gz → 0.5.74__tar.gz
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-0.5.73 → applied_cli-0.5.74}/PKG-INFO +14 -1
- {applied_cli-0.5.73 → applied_cli-0.5.74}/README.md +13 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/applied_cli/__init__.py +1 -1
- {applied_cli-0.5.73 → applied_cli-0.5.74}/applied_cli/client.py +312 -12
- {applied_cli-0.5.73 → applied_cli-0.5.74}/applied_cli.egg-info/PKG-INFO +14 -1
- {applied_cli-0.5.73 → applied_cli-0.5.74}/pyproject.toml +1 -1
- {applied_cli-0.5.73 → applied_cli-0.5.74}/applied_cli/agent_scoped_flows.py +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/applied_cli/cli.py +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/applied_cli/conversation_lookup.py +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/applied_cli/conversations.py +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/applied_cli/credentials.py +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/applied_cli/flow_helpers.py +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/applied_cli/formatters.py +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/applied_cli/tools.py +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/applied_cli.egg-info/SOURCES.txt +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/applied_cli.egg-info/requires.txt +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/setup.cfg +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/tests/test_agent_scoped_flows.py +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/tests/test_audit_tools.py +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/tests/test_benchmark_scenario_tools.py +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/tests/test_cli.py +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/tests/test_client.py +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/tests/test_conversation_tools.py +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/tests/test_flow_tools.py +0 -0
- {applied_cli-0.5.73 → applied_cli-0.5.74}/tests/test_knowledge_content_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: applied-cli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.74
|
|
4
4
|
Summary: CLI and shared client library for Applied Labs AI support agents
|
|
5
5
|
Author: Applied Labs
|
|
6
6
|
License-Expression: MIT
|
|
@@ -67,8 +67,18 @@ applied knowledge-unprotect <id>
|
|
|
67
67
|
# Taxonomy
|
|
68
68
|
applied taxonomy --type topics
|
|
69
69
|
applied taxonomy-counts --start 2026-03-01 --end 2026-03-18 --format csv
|
|
70
|
+
|
|
71
|
+
# Analytics
|
|
72
|
+
applied analytics-report --view overview_aggregate_metrics --model conversation --start 2026-04-01 --end 2026-04-30 --format json
|
|
73
|
+
applied analytics --group-by topic --metrics count --start 2026-04-01 --end 2026-04-30 --format json
|
|
74
|
+
applied analytics --group-by intent --metrics count --start 2026-04-01 --end 2026-04-30 --format json
|
|
75
|
+
applied metrics --metric-name conversation.resolve --start 2026-04-01 --end 2026-04-30 --period day --format json
|
|
70
76
|
```
|
|
71
77
|
|
|
78
|
+
`analytics-report` returns the selected report payload, not necessarily a `{ "rows": [...] }`
|
|
79
|
+
object. `analytics` returns grouped rows and currently supports `--metrics count`.
|
|
80
|
+
Raw analytics SQL is not available through the public CLI surface.
|
|
81
|
+
|
|
72
82
|
## Library Usage
|
|
73
83
|
|
|
74
84
|
```python
|
|
@@ -99,6 +109,9 @@ conversations = await tools.conversation_query(
|
|
|
99
109
|
| `knowledge_list` | List knowledge base items |
|
|
100
110
|
| `taxonomy_list` | List topics, intents, and flags |
|
|
101
111
|
| `taxonomy_counts` | Aggregate conversation counts by topic and intent |
|
|
112
|
+
| `analytics_report` | Read standard dashboard/report analytics views |
|
|
113
|
+
| `analytics_query` | Aggregate supported conversation dimensions with count |
|
|
114
|
+
| `metrics_query` | Roll up named metric events |
|
|
102
115
|
|
|
103
116
|
## Examples
|
|
104
117
|
|
|
@@ -42,8 +42,18 @@ applied knowledge-unprotect <id>
|
|
|
42
42
|
# Taxonomy
|
|
43
43
|
applied taxonomy --type topics
|
|
44
44
|
applied taxonomy-counts --start 2026-03-01 --end 2026-03-18 --format csv
|
|
45
|
+
|
|
46
|
+
# Analytics
|
|
47
|
+
applied analytics-report --view overview_aggregate_metrics --model conversation --start 2026-04-01 --end 2026-04-30 --format json
|
|
48
|
+
applied analytics --group-by topic --metrics count --start 2026-04-01 --end 2026-04-30 --format json
|
|
49
|
+
applied analytics --group-by intent --metrics count --start 2026-04-01 --end 2026-04-30 --format json
|
|
50
|
+
applied metrics --metric-name conversation.resolve --start 2026-04-01 --end 2026-04-30 --period day --format json
|
|
45
51
|
```
|
|
46
52
|
|
|
53
|
+
`analytics-report` returns the selected report payload, not necessarily a `{ "rows": [...] }`
|
|
54
|
+
object. `analytics` returns grouped rows and currently supports `--metrics count`.
|
|
55
|
+
Raw analytics SQL is not available through the public CLI surface.
|
|
56
|
+
|
|
47
57
|
## Library Usage
|
|
48
58
|
|
|
49
59
|
```python
|
|
@@ -74,6 +84,9 @@ conversations = await tools.conversation_query(
|
|
|
74
84
|
| `knowledge_list` | List knowledge base items |
|
|
75
85
|
| `taxonomy_list` | List topics, intents, and flags |
|
|
76
86
|
| `taxonomy_counts` | Aggregate conversation counts by topic and intent |
|
|
87
|
+
| `analytics_report` | Read standard dashboard/report analytics views |
|
|
88
|
+
| `analytics_query` | Aggregate supported conversation dimensions with count |
|
|
89
|
+
| `metrics_query` | Roll up named metric events |
|
|
77
90
|
|
|
78
91
|
## Examples
|
|
79
92
|
|
|
@@ -2,11 +2,94 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
5
6
|
import uuid
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
import httpx
|
|
9
10
|
|
|
11
|
+
ACCESS_MODE_ENV_VAR = "APPLIED_ASSISTANT_ACCESS_MODE"
|
|
12
|
+
ASSISTANT_CONVERSATION_ENV_VAR = "APPLIED_ASSISTANT_CONVERSATION_ID"
|
|
13
|
+
FULL_ACCESS_MODE = "full_access"
|
|
14
|
+
DEFAULT_ACCESS_MODE = "default"
|
|
15
|
+
READ_ONLY_ACCESS_MODE = "read_only"
|
|
16
|
+
MUTATING_METHODS = {"POST", "PATCH", "DELETE", "PUT"}
|
|
17
|
+
OBJECT_COLLECTION_LABELS = {
|
|
18
|
+
"agents": "Agent",
|
|
19
|
+
"c": "Conversation",
|
|
20
|
+
"conversations": "Conversation",
|
|
21
|
+
"content": "Content",
|
|
22
|
+
"flows": "Flow",
|
|
23
|
+
"nodes": "Flow node",
|
|
24
|
+
"edges": "Flow edge",
|
|
25
|
+
"products": "Product",
|
|
26
|
+
"responses": "Response",
|
|
27
|
+
"tickets": "Ticket",
|
|
28
|
+
"tools": "Tool",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _normalize_access_mode(access_mode: str | None) -> str:
|
|
33
|
+
if access_mode is None or not access_mode.strip():
|
|
34
|
+
return FULL_ACCESS_MODE
|
|
35
|
+
|
|
36
|
+
normalized = access_mode.strip().lower().replace("-", "_")
|
|
37
|
+
if normalized in {FULL_ACCESS_MODE, DEFAULT_ACCESS_MODE, READ_ONLY_ACCESS_MODE}:
|
|
38
|
+
return normalized
|
|
39
|
+
return DEFAULT_ACCESS_MODE
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _json_safe(value: Any) -> Any:
|
|
43
|
+
try:
|
|
44
|
+
return json.loads(json.dumps(value, default=str, ensure_ascii=True))
|
|
45
|
+
except (TypeError, ValueError):
|
|
46
|
+
return str(value)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _infer_change_action(method: str) -> str:
|
|
50
|
+
method = method.upper()
|
|
51
|
+
if method == "POST":
|
|
52
|
+
return "add"
|
|
53
|
+
if method == "DELETE":
|
|
54
|
+
return "remove"
|
|
55
|
+
return "update"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _infer_object_reference(path: str, body: Any) -> tuple[str, str, str]:
|
|
59
|
+
parts = [part for part in path.split("?", 1)[0].strip("/").split("/") if part]
|
|
60
|
+
if parts and parts[0].startswith("v") and len(parts) > 1:
|
|
61
|
+
parts = parts[1:]
|
|
62
|
+
|
|
63
|
+
object_type = "DB object"
|
|
64
|
+
object_id = ""
|
|
65
|
+
for index, part in enumerate(parts):
|
|
66
|
+
collection = part.replace("-", "_")
|
|
67
|
+
if collection not in OBJECT_COLLECTION_LABELS:
|
|
68
|
+
continue
|
|
69
|
+
object_type = OBJECT_COLLECTION_LABELS[collection]
|
|
70
|
+
if index + 1 < len(parts):
|
|
71
|
+
candidate = parts[index + 1]
|
|
72
|
+
if candidate.replace("-", "_") not in OBJECT_COLLECTION_LABELS:
|
|
73
|
+
object_id = candidate
|
|
74
|
+
|
|
75
|
+
object_name = ""
|
|
76
|
+
if isinstance(body, dict):
|
|
77
|
+
for key in ("name", "title", "label", "display_name", "displayName"):
|
|
78
|
+
value = body.get(key)
|
|
79
|
+
if isinstance(value, str) and value.strip():
|
|
80
|
+
object_name = value.strip()
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
return object_type, object_id, object_name
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _candidate_after(before: Any, body: Any, after: Any) -> Any:
|
|
87
|
+
if after is not None:
|
|
88
|
+
return after
|
|
89
|
+
if isinstance(before, dict) and isinstance(body, dict):
|
|
90
|
+
return {**before, **body}
|
|
91
|
+
return body or {}
|
|
92
|
+
|
|
10
93
|
|
|
11
94
|
class AppliedAPIError(Exception):
|
|
12
95
|
"""API error with details and recovery suggestions."""
|
|
@@ -355,7 +438,10 @@ def _decode_dashboard_group(group: Any, expected_count: int) -> list[Any]:
|
|
|
355
438
|
|
|
356
439
|
def _sort_analytics_rows(rows: list[dict[str, Any]], group_by: list[str]) -> None:
|
|
357
440
|
def sort_key(row: dict[str, Any]) -> tuple[str, ...]:
|
|
358
|
-
return tuple(
|
|
441
|
+
return tuple(
|
|
442
|
+
"" if row.get(dimension) is None else str(row.get(dimension))
|
|
443
|
+
for dimension in group_by
|
|
444
|
+
)
|
|
359
445
|
|
|
360
446
|
rows.sort(key=sort_key)
|
|
361
447
|
|
|
@@ -369,11 +455,163 @@ class AppliedClient:
|
|
|
369
455
|
shop_id: str | None = None,
|
|
370
456
|
base_url: str = "https://api.appliedlabs.ai",
|
|
371
457
|
timeout: float = 30.0,
|
|
458
|
+
access_mode: str | None = None,
|
|
372
459
|
):
|
|
373
460
|
self.token = token
|
|
374
461
|
self.shop_id = shop_id
|
|
375
462
|
self.base_url = base_url.rstrip("/")
|
|
376
463
|
self.timeout = timeout
|
|
464
|
+
self.access_mode = _normalize_access_mode(
|
|
465
|
+
access_mode
|
|
466
|
+
if access_mode is not None
|
|
467
|
+
else os.environ.get(ACCESS_MODE_ENV_VAR)
|
|
468
|
+
)
|
|
469
|
+
self.assistant_conversation_id = os.environ.get(
|
|
470
|
+
ASSISTANT_CONVERSATION_ENV_VAR,
|
|
471
|
+
"",
|
|
472
|
+
).strip()
|
|
473
|
+
|
|
474
|
+
async def _record_access_mode_change(
|
|
475
|
+
self,
|
|
476
|
+
*,
|
|
477
|
+
method: str,
|
|
478
|
+
path: str,
|
|
479
|
+
body: Any = None,
|
|
480
|
+
before: Any = None,
|
|
481
|
+
after: Any = None,
|
|
482
|
+
shop_id: str | None = None,
|
|
483
|
+
status: str,
|
|
484
|
+
) -> None:
|
|
485
|
+
if not self.assistant_conversation_id or not self.token:
|
|
486
|
+
return
|
|
487
|
+
|
|
488
|
+
safe_body = _json_safe(body or {})
|
|
489
|
+
safe_before = _json_safe(before if before is not None else {})
|
|
490
|
+
safe_after = _json_safe(_candidate_after(before, body, after))
|
|
491
|
+
object_type, object_id, object_name = _infer_object_reference(
|
|
492
|
+
path,
|
|
493
|
+
safe_body if safe_body else safe_after,
|
|
494
|
+
)
|
|
495
|
+
resolved_shop_id = shop_id or self.shop_id
|
|
496
|
+
payload = {
|
|
497
|
+
"access_mode": self.access_mode,
|
|
498
|
+
"status": status,
|
|
499
|
+
"object_type": object_type,
|
|
500
|
+
"object_id": object_id,
|
|
501
|
+
"object_name": object_name,
|
|
502
|
+
"action": _infer_change_action(method),
|
|
503
|
+
"before": safe_before,
|
|
504
|
+
"after": safe_after,
|
|
505
|
+
"diff": {"request_body": safe_body},
|
|
506
|
+
"request_method": method.upper(),
|
|
507
|
+
"request_path": path,
|
|
508
|
+
"request_body": safe_body,
|
|
509
|
+
}
|
|
510
|
+
headers = {
|
|
511
|
+
"Authorization": f"Bearer {self.token}",
|
|
512
|
+
"Content-Type": "application/json",
|
|
513
|
+
}
|
|
514
|
+
if resolved_shop_id:
|
|
515
|
+
headers["X-Shop-Id"] = resolved_shop_id
|
|
516
|
+
|
|
517
|
+
timeout = (
|
|
518
|
+
min(self.timeout, 10.0) if isinstance(self.timeout, int | float) else 10.0
|
|
519
|
+
)
|
|
520
|
+
diffs_url = (
|
|
521
|
+
f"{self.base_url}/v1/threads/{self.assistant_conversation_id}/diffs/"
|
|
522
|
+
)
|
|
523
|
+
try:
|
|
524
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
525
|
+
await client.post(
|
|
526
|
+
diffs_url,
|
|
527
|
+
headers=headers,
|
|
528
|
+
json=payload,
|
|
529
|
+
)
|
|
530
|
+
except Exception:
|
|
531
|
+
# Do not let proposal logging mask the access-mode decision.
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
async def _fetch_current_object(
|
|
535
|
+
self,
|
|
536
|
+
*,
|
|
537
|
+
method: str,
|
|
538
|
+
path: str,
|
|
539
|
+
shop_id: str | None = None,
|
|
540
|
+
) -> Any:
|
|
541
|
+
if method.upper() not in {"PATCH", "PUT", "DELETE"}:
|
|
542
|
+
return {}
|
|
543
|
+
|
|
544
|
+
headers = {"Authorization": f"Bearer {self.token}"}
|
|
545
|
+
resolved_shop_id = shop_id or self.shop_id
|
|
546
|
+
if resolved_shop_id:
|
|
547
|
+
headers["X-Shop-Id"] = resolved_shop_id
|
|
548
|
+
|
|
549
|
+
timeout = (
|
|
550
|
+
min(self.timeout, 10.0) if isinstance(self.timeout, int | float) else 10.0
|
|
551
|
+
)
|
|
552
|
+
try:
|
|
553
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
554
|
+
response = await client.get(f"{self.base_url}{path}", headers=headers)
|
|
555
|
+
if response.status_code >= 400:
|
|
556
|
+
return {}
|
|
557
|
+
return response.json()
|
|
558
|
+
except Exception:
|
|
559
|
+
return {}
|
|
560
|
+
|
|
561
|
+
async def _enforce_access_mode(
|
|
562
|
+
self,
|
|
563
|
+
method: str,
|
|
564
|
+
path: str,
|
|
565
|
+
*,
|
|
566
|
+
body: Any = None,
|
|
567
|
+
shop_id: str | None = None,
|
|
568
|
+
) -> None:
|
|
569
|
+
method = method.upper()
|
|
570
|
+
if method not in MUTATING_METHODS or self.access_mode == FULL_ACCESS_MODE:
|
|
571
|
+
return
|
|
572
|
+
|
|
573
|
+
requires_approval = self.access_mode == DEFAULT_ACCESS_MODE
|
|
574
|
+
before = await self._fetch_current_object(
|
|
575
|
+
method=method,
|
|
576
|
+
path=path,
|
|
577
|
+
shop_id=shop_id,
|
|
578
|
+
)
|
|
579
|
+
await self._record_access_mode_change(
|
|
580
|
+
method=method,
|
|
581
|
+
path=path,
|
|
582
|
+
body=body,
|
|
583
|
+
before=before,
|
|
584
|
+
shop_id=shop_id,
|
|
585
|
+
status="proposed" if requires_approval else "blocked",
|
|
586
|
+
)
|
|
587
|
+
error_code = (
|
|
588
|
+
"requires_approval" if requires_approval else "read_only_access_mode"
|
|
589
|
+
)
|
|
590
|
+
message = (
|
|
591
|
+
f"{method} {path} was not sent because Applied Assistant access mode "
|
|
592
|
+
f"is {self.access_mode}."
|
|
593
|
+
)
|
|
594
|
+
detail = {
|
|
595
|
+
"error": error_code,
|
|
596
|
+
"requires_approval": requires_approval,
|
|
597
|
+
"access_mode": self.access_mode,
|
|
598
|
+
"method": method,
|
|
599
|
+
"path": path,
|
|
600
|
+
"message": message,
|
|
601
|
+
}
|
|
602
|
+
suggestion = (
|
|
603
|
+
"Request approval or run with APPLIED_ASSISTANT_ACCESS_MODE=full_access "
|
|
604
|
+
"before retrying this mutation."
|
|
605
|
+
if requires_approval
|
|
606
|
+
else "This environment is read-only. Re-run in full_access mode only when "
|
|
607
|
+
"mutations are intended."
|
|
608
|
+
)
|
|
609
|
+
raise AppliedAPIError(
|
|
610
|
+
message=f"{method} {path} blocked by access mode",
|
|
611
|
+
status_code=403,
|
|
612
|
+
detail=json.dumps(detail, separators=(",", ":"), ensure_ascii=True),
|
|
613
|
+
suggestion=suggestion,
|
|
614
|
+
)
|
|
377
615
|
|
|
378
616
|
async def _request(
|
|
379
617
|
self,
|
|
@@ -384,6 +622,13 @@ class AppliedClient:
|
|
|
384
622
|
shop_id: str | None = None,
|
|
385
623
|
) -> Any:
|
|
386
624
|
"""Make an authenticated API request."""
|
|
625
|
+
method = method.upper()
|
|
626
|
+
await self._enforce_access_mode(
|
|
627
|
+
method,
|
|
628
|
+
path,
|
|
629
|
+
body=body,
|
|
630
|
+
shop_id=shop_id,
|
|
631
|
+
)
|
|
387
632
|
headers = {
|
|
388
633
|
"Authorization": f"Bearer {self.token}",
|
|
389
634
|
"Content-Type": "application/json",
|
|
@@ -392,6 +637,13 @@ class AppliedClient:
|
|
|
392
637
|
if resolved_shop_id:
|
|
393
638
|
headers["X-Shop-Id"] = resolved_shop_id
|
|
394
639
|
url = f"{self.base_url}{path}"
|
|
640
|
+
before = None
|
|
641
|
+
if method in MUTATING_METHODS and self.access_mode == FULL_ACCESS_MODE:
|
|
642
|
+
before = await self._fetch_current_object(
|
|
643
|
+
method=method,
|
|
644
|
+
path=path,
|
|
645
|
+
shop_id=resolved_shop_id,
|
|
646
|
+
)
|
|
395
647
|
|
|
396
648
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
397
649
|
if method == "GET":
|
|
@@ -441,9 +693,19 @@ class AppliedClient:
|
|
|
441
693
|
suggestion=suggestion,
|
|
442
694
|
)
|
|
443
695
|
|
|
444
|
-
if resp.status_code == 204
|
|
445
|
-
|
|
446
|
-
|
|
696
|
+
result = None if resp.status_code == 204 else resp.json()
|
|
697
|
+
|
|
698
|
+
if method in MUTATING_METHODS and self.access_mode == FULL_ACCESS_MODE:
|
|
699
|
+
await self._record_access_mode_change(
|
|
700
|
+
method=method,
|
|
701
|
+
path=path,
|
|
702
|
+
body=body,
|
|
703
|
+
before=before,
|
|
704
|
+
after=result,
|
|
705
|
+
shop_id=shop_id,
|
|
706
|
+
status="applied",
|
|
707
|
+
)
|
|
708
|
+
return result
|
|
447
709
|
|
|
448
710
|
async def _upload_request(
|
|
449
711
|
self,
|
|
@@ -453,12 +715,21 @@ class AppliedClient:
|
|
|
453
715
|
data: dict[str, Any] | None = None,
|
|
454
716
|
) -> Any:
|
|
455
717
|
"""Make an authenticated multipart file upload request."""
|
|
718
|
+
method = method.upper()
|
|
719
|
+
await self._enforce_access_mode(method, path, body=data, shop_id=self.shop_id)
|
|
456
720
|
headers = {
|
|
457
721
|
"Authorization": f"Bearer {self.token}",
|
|
458
722
|
}
|
|
459
723
|
if self.shop_id:
|
|
460
724
|
headers["X-Shop-Id"] = self.shop_id
|
|
461
725
|
url = f"{self.base_url}{path}"
|
|
726
|
+
before = None
|
|
727
|
+
if method in MUTATING_METHODS and self.access_mode == FULL_ACCESS_MODE:
|
|
728
|
+
before = await self._fetch_current_object(
|
|
729
|
+
method=method,
|
|
730
|
+
path=path,
|
|
731
|
+
shop_id=self.shop_id,
|
|
732
|
+
)
|
|
462
733
|
|
|
463
734
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
464
735
|
resp = await client.request(
|
|
@@ -485,9 +756,19 @@ class AppliedClient:
|
|
|
485
756
|
detail=detail,
|
|
486
757
|
)
|
|
487
758
|
|
|
488
|
-
if resp.status_code == 204
|
|
489
|
-
|
|
490
|
-
|
|
759
|
+
result = None if resp.status_code == 204 else resp.json()
|
|
760
|
+
|
|
761
|
+
if method in MUTATING_METHODS and self.access_mode == FULL_ACCESS_MODE:
|
|
762
|
+
await self._record_access_mode_change(
|
|
763
|
+
method=method,
|
|
764
|
+
path=path,
|
|
765
|
+
body=data,
|
|
766
|
+
before=before,
|
|
767
|
+
after=result,
|
|
768
|
+
shop_id=self.shop_id,
|
|
769
|
+
status="applied",
|
|
770
|
+
)
|
|
771
|
+
return result
|
|
491
772
|
|
|
492
773
|
def _require_shop_id(self, shop_id: str | None = None) -> str:
|
|
493
774
|
resolved_shop_id = shop_id or self.shop_id
|
|
@@ -564,9 +845,7 @@ class AppliedClient:
|
|
|
564
845
|
**kwargs: Any,
|
|
565
846
|
) -> dict:
|
|
566
847
|
"""Update an agent's properties."""
|
|
567
|
-
return await self._request(
|
|
568
|
-
"PATCH", f"/v1/agents/{agent_id}/", body=kwargs
|
|
569
|
-
)
|
|
848
|
+
return await self._request("PATCH", f"/v1/agents/{agent_id}/", body=kwargs)
|
|
570
849
|
|
|
571
850
|
async def update_agent_picture(
|
|
572
851
|
self,
|
|
@@ -1886,6 +2165,18 @@ class AppliedClient:
|
|
|
1886
2165
|
Returns:
|
|
1887
2166
|
Dict with conversation_id, response, and status
|
|
1888
2167
|
"""
|
|
2168
|
+
path = f"/v1/agents/{agent_id}/complete/"
|
|
2169
|
+
await self._enforce_access_mode(
|
|
2170
|
+
"POST",
|
|
2171
|
+
path,
|
|
2172
|
+
body={
|
|
2173
|
+
"conversation_id": conversation_id or "",
|
|
2174
|
+
"message": message,
|
|
2175
|
+
"metadata": metadata or {},
|
|
2176
|
+
"flow_id": flow_id or "",
|
|
2177
|
+
},
|
|
2178
|
+
)
|
|
2179
|
+
|
|
1889
2180
|
conversation = await self.ensure_test_conversation(
|
|
1890
2181
|
agent_id=agent_id,
|
|
1891
2182
|
conversation_id=conversation_id,
|
|
@@ -1922,7 +2213,7 @@ class AppliedClient:
|
|
|
1922
2213
|
if runtime_metadata:
|
|
1923
2214
|
body["metadata"] = runtime_metadata
|
|
1924
2215
|
|
|
1925
|
-
url = f"{self.base_url}
|
|
2216
|
+
url = f"{self.base_url}{path}"
|
|
1926
2217
|
|
|
1927
2218
|
response_text = ""
|
|
1928
2219
|
|
|
@@ -1944,11 +2235,20 @@ class AppliedClient:
|
|
|
1944
2235
|
except json.JSONDecodeError:
|
|
1945
2236
|
pass
|
|
1946
2237
|
|
|
1947
|
-
|
|
2238
|
+
result = {
|
|
1948
2239
|
"conversation_id": result_conversation_id,
|
|
1949
2240
|
"response": response_text,
|
|
1950
2241
|
"status": "success" if response_text else "empty",
|
|
1951
2242
|
}
|
|
2243
|
+
if self.access_mode == FULL_ACCESS_MODE:
|
|
2244
|
+
await self._record_access_mode_change(
|
|
2245
|
+
method="POST",
|
|
2246
|
+
path=path,
|
|
2247
|
+
body=body,
|
|
2248
|
+
after=result,
|
|
2249
|
+
status="applied",
|
|
2250
|
+
)
|
|
2251
|
+
return result
|
|
1952
2252
|
|
|
1953
2253
|
# -------------------------------------------------------------------------
|
|
1954
2254
|
# Benchmarks
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: applied-cli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.74
|
|
4
4
|
Summary: CLI and shared client library for Applied Labs AI support agents
|
|
5
5
|
Author: Applied Labs
|
|
6
6
|
License-Expression: MIT
|
|
@@ -67,8 +67,18 @@ applied knowledge-unprotect <id>
|
|
|
67
67
|
# Taxonomy
|
|
68
68
|
applied taxonomy --type topics
|
|
69
69
|
applied taxonomy-counts --start 2026-03-01 --end 2026-03-18 --format csv
|
|
70
|
+
|
|
71
|
+
# Analytics
|
|
72
|
+
applied analytics-report --view overview_aggregate_metrics --model conversation --start 2026-04-01 --end 2026-04-30 --format json
|
|
73
|
+
applied analytics --group-by topic --metrics count --start 2026-04-01 --end 2026-04-30 --format json
|
|
74
|
+
applied analytics --group-by intent --metrics count --start 2026-04-01 --end 2026-04-30 --format json
|
|
75
|
+
applied metrics --metric-name conversation.resolve --start 2026-04-01 --end 2026-04-30 --period day --format json
|
|
70
76
|
```
|
|
71
77
|
|
|
78
|
+
`analytics-report` returns the selected report payload, not necessarily a `{ "rows": [...] }`
|
|
79
|
+
object. `analytics` returns grouped rows and currently supports `--metrics count`.
|
|
80
|
+
Raw analytics SQL is not available through the public CLI surface.
|
|
81
|
+
|
|
72
82
|
## Library Usage
|
|
73
83
|
|
|
74
84
|
```python
|
|
@@ -99,6 +109,9 @@ conversations = await tools.conversation_query(
|
|
|
99
109
|
| `knowledge_list` | List knowledge base items |
|
|
100
110
|
| `taxonomy_list` | List topics, intents, and flags |
|
|
101
111
|
| `taxonomy_counts` | Aggregate conversation counts by topic and intent |
|
|
112
|
+
| `analytics_report` | Read standard dashboard/report analytics views |
|
|
113
|
+
| `analytics_query` | Aggregate supported conversation dimensions with count |
|
|
114
|
+
| `metrics_query` | Roll up named metric events |
|
|
102
115
|
|
|
103
116
|
## Examples
|
|
104
117
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|