applied-cli 0.6.12__tar.gz → 0.7.0__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.6.12 → applied_cli-0.7.0}/PKG-INFO +1 -1
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/__init__.py +1 -1
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/v2/catalog.py +2 -0
- applied_cli-0.7.0/applied_cli/v2/dashboards.py +330 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli.egg-info/PKG-INFO +1 -1
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli.egg-info/SOURCES.txt +2 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/pyproject.toml +1 -1
- applied_cli-0.7.0/tests/test_v2_dashboards.py +142 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/README.md +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/agent_scoped_flows.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/auth.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/cli.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/client.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/conversation_lookup.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/conversations.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/credentials.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/flow_helpers.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/formatters.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/mcp.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/recovery.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/toolkit.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/tools.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/v2/__init__.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/v2/agents.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/v2/articles.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/v2/connectors.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/v2/content.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/v2/conversations.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/v2/domains.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/v2/evals.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/v2/flows.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/v2/knowledge.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/v2/manifest.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/v2/products.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/v2/scenarios.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/v2/taxonomy.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli/v2/tickets.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli.egg-info/requires.txt +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/setup.cfg +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_agent_scoped_flows.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_audit_tools.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_auth_context.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_benchmark_clone.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_benchmark_delete_guardrail.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_benchmark_list_with_results.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_benchmark_results.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_benchmark_scenario_tools.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_cli.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_cli_v2.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_client.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_client_v2.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_conversation_tools.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_flow_tools.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_knowledge_content_tools.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_recovery.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_scenario_bulk_cancel.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_scenario_bulk_run_contact.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_scenario_bulk_run_wait.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_scenario_create_bulk.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_toolkit_contract.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_v2_agents.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_v2_articles.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_v2_catalog_and_mcp.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_v2_connectors.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_v2_content.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_v2_conversations.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_v2_evals.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_v2_flows.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_v2_knowledge.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_v2_products.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_v2_scenarios.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_v2_taxonomy.py +0 -0
- {applied_cli-0.6.12 → applied_cli-0.7.0}/tests/test_v2_tickets.py +0 -0
|
@@ -17,6 +17,7 @@ from applied_cli.v2.articles import article_specs
|
|
|
17
17
|
from applied_cli.v2.connectors import connector_specs
|
|
18
18
|
from applied_cli.v2.content import content_specs
|
|
19
19
|
from applied_cli.v2.conversations import conversation_specs
|
|
20
|
+
from applied_cli.v2.dashboards import dashboard_specs
|
|
20
21
|
from applied_cli.v2.domains import LEGACY_TOOL_RENAMES
|
|
21
22
|
from applied_cli.v2.evals import evals_specs
|
|
22
23
|
from applied_cli.v2.flows import flow_specs
|
|
@@ -228,6 +229,7 @@ def get_tool_catalog(include_debug: bool = False) -> ToolCatalog:
|
|
|
228
229
|
*connector_specs(),
|
|
229
230
|
*content_specs(),
|
|
230
231
|
*conversation_specs(),
|
|
232
|
+
*dashboard_specs(),
|
|
231
233
|
*evals_specs(),
|
|
232
234
|
*flow_specs(),
|
|
233
235
|
*knowledge_specs(),
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Native structured v2 tools for analytics dashboards and save-metric config.
|
|
2
|
+
|
|
3
|
+
These tools let an agent stand up a shop's Save Agent analytics end to end:
|
|
4
|
+
list the built-in dashboard templates, instantiate one (e.g. ``save_agent``),
|
|
5
|
+
and read/update the save-metric vocabulary (cancellation reasons and save
|
|
6
|
+
tactics) that the post-conversation metric extractor classifies against.
|
|
7
|
+
|
|
8
|
+
All endpoints are shop-scoped and merchant-authenticated. Save-metric
|
|
9
|
+
extraction itself is enrolled per shop via the ``save_metric_extraction``
|
|
10
|
+
feature flag (platform admin) — ``save_metrics_config_get`` reports whether
|
|
11
|
+
the current shop is enrolled.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
19
|
+
|
|
20
|
+
from applied_cli.client import AppliedAPIError, AppliedClient
|
|
21
|
+
from applied_cli.toolkit import ToolError, ToolResult, ToolSpec
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StrictInput(BaseModel):
|
|
25
|
+
model_config = ConfigDict(extra="forbid")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ── inputs ──────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DashboardsListInput(StrictInput):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DashboardTemplatesListInput(StrictInput):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DashboardsCreateInput(StrictInput):
|
|
40
|
+
template_key: str | None = Field(
|
|
41
|
+
default=None,
|
|
42
|
+
description=(
|
|
43
|
+
"Key of a built-in dashboard template to instantiate (see "
|
|
44
|
+
"dashboards_templates_list; e.g. 'save_agent'). Omit to create an "
|
|
45
|
+
"empty dashboard."
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
name: str | None = Field(
|
|
49
|
+
default=None,
|
|
50
|
+
description="Dashboard name. Defaults to the template's name.",
|
|
51
|
+
)
|
|
52
|
+
description: str | None = Field(
|
|
53
|
+
default=None,
|
|
54
|
+
description="Dashboard description. Defaults to the template's.",
|
|
55
|
+
)
|
|
56
|
+
is_public: bool = Field(
|
|
57
|
+
default=True,
|
|
58
|
+
description=(
|
|
59
|
+
"Whether the dashboard is visible to the whole workspace (true) "
|
|
60
|
+
"or private to the creator (false)."
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SaveMetricsConfigGetInput(StrictInput):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SaveMetricsConfigUpdateInput(StrictInput):
|
|
70
|
+
cancellation_reasons: list[str] | None = Field(
|
|
71
|
+
default=None,
|
|
72
|
+
description=(
|
|
73
|
+
"Canonical cancellation_reason values the extractor classifies "
|
|
74
|
+
"against. Replaces the current list; pass [] to clear the override "
|
|
75
|
+
"and fall back to values observed in the shop's metric events. "
|
|
76
|
+
"Omit to leave unchanged."
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
save_tactics: list[str] | None = Field(
|
|
80
|
+
default=None,
|
|
81
|
+
description=(
|
|
82
|
+
"Canonical save_tactic values the extractor classifies against. "
|
|
83
|
+
"Replaces the current list; pass [] to clear the override and fall "
|
|
84
|
+
"back to observed values. Omit to leave unchanged."
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ── helpers ─────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _api_error_result(exc: AppliedAPIError) -> ToolResult[Any]:
|
|
93
|
+
return ToolResult(
|
|
94
|
+
ok=False,
|
|
95
|
+
error=ToolError(
|
|
96
|
+
code=f"api.{exc.status_code}",
|
|
97
|
+
message=str(exc),
|
|
98
|
+
status_code=exc.status_code,
|
|
99
|
+
retryable=exc.status_code in {408, 429, 500, 502, 503, 504},
|
|
100
|
+
recovery_hint=exc.suggestion,
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _rows(data: Any) -> list[dict[str, Any]]:
|
|
106
|
+
if isinstance(data, list):
|
|
107
|
+
return [row for row in data if isinstance(row, dict)]
|
|
108
|
+
if isinstance(data, dict):
|
|
109
|
+
for key in ("results", "items", "data"):
|
|
110
|
+
value = data.get(key)
|
|
111
|
+
if isinstance(value, list):
|
|
112
|
+
return [row for row in value if isinstance(row, dict)]
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _project_dashboard(item: dict[str, Any]) -> dict[str, Any]:
|
|
117
|
+
revision = item.get("latest_revision") or item.get("revision") or {}
|
|
118
|
+
if not isinstance(revision, dict):
|
|
119
|
+
revision = {}
|
|
120
|
+
return {
|
|
121
|
+
"id": item.get("id"),
|
|
122
|
+
"name": item.get("name") or revision.get("name"),
|
|
123
|
+
"description": item.get("description") or revision.get("description"),
|
|
124
|
+
"is_public": item.get("is_public"),
|
|
125
|
+
"created_at": item.get("created_at"),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ── handlers ────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def dashboards_list_handler(
|
|
133
|
+
client: AppliedClient,
|
|
134
|
+
params: DashboardsListInput,
|
|
135
|
+
) -> ToolResult[Any]:
|
|
136
|
+
try:
|
|
137
|
+
data = await client.request("GET", "/v1/dashboards/")
|
|
138
|
+
except AppliedAPIError as exc:
|
|
139
|
+
return _api_error_result(exc)
|
|
140
|
+
|
|
141
|
+
items = [_project_dashboard(row) for row in _rows(data)]
|
|
142
|
+
suffix = "" if len(items) == 1 else "s"
|
|
143
|
+
return ToolResult(
|
|
144
|
+
data={"count": len(items), "dashboards": items},
|
|
145
|
+
summary=f"Found {len(items)} dashboard{suffix}.",
|
|
146
|
+
next_actions=[
|
|
147
|
+
"Use dashboards_create with a template_key to add a templated dashboard.",
|
|
148
|
+
],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def dashboards_templates_list_handler(
|
|
153
|
+
client: AppliedClient,
|
|
154
|
+
params: DashboardTemplatesListInput,
|
|
155
|
+
) -> ToolResult[Any]:
|
|
156
|
+
try:
|
|
157
|
+
data = await client.request("GET", "/v1/dashboards/templates/")
|
|
158
|
+
except AppliedAPIError as exc:
|
|
159
|
+
return _api_error_result(exc)
|
|
160
|
+
|
|
161
|
+
templates = data.get("templates", []) if isinstance(data, dict) else []
|
|
162
|
+
suffix = "" if len(templates) == 1 else "s"
|
|
163
|
+
return ToolResult(
|
|
164
|
+
data={"count": len(templates), "templates": templates},
|
|
165
|
+
summary=f"Found {len(templates)} dashboard template{suffix}.",
|
|
166
|
+
next_actions=[
|
|
167
|
+
"Use dashboards_create with template_key to instantiate a template "
|
|
168
|
+
"(e.g. 'save_agent' for the Save Agent dashboard).",
|
|
169
|
+
],
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def dashboards_create_handler(
|
|
174
|
+
client: AppliedClient,
|
|
175
|
+
params: DashboardsCreateInput,
|
|
176
|
+
) -> ToolResult[Any]:
|
|
177
|
+
body: dict[str, Any] = {"is_public": params.is_public}
|
|
178
|
+
if params.template_key:
|
|
179
|
+
body["template_key"] = params.template_key
|
|
180
|
+
if params.name:
|
|
181
|
+
body["name"] = params.name
|
|
182
|
+
if params.description:
|
|
183
|
+
body["description"] = params.description
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
data = await client.request("POST", "/v1/dashboards/", body=body)
|
|
187
|
+
except AppliedAPIError as exc:
|
|
188
|
+
return _api_error_result(exc)
|
|
189
|
+
|
|
190
|
+
dashboard = _project_dashboard(data if isinstance(data, dict) else {})
|
|
191
|
+
label = params.template_key or "empty"
|
|
192
|
+
return ToolResult(
|
|
193
|
+
data={"dashboard": dashboard, "raw": data},
|
|
194
|
+
summary=f"Created dashboard {dashboard.get('id')} from template '{label}'.",
|
|
195
|
+
next_actions=[
|
|
196
|
+
"Use save_metrics_config_get to check the save-metric vocabulary "
|
|
197
|
+
"and enrollment if this is a Save Agent dashboard.",
|
|
198
|
+
],
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
async def save_metrics_config_get_handler(
|
|
203
|
+
client: AppliedClient,
|
|
204
|
+
params: SaveMetricsConfigGetInput,
|
|
205
|
+
) -> ToolResult[Any]:
|
|
206
|
+
try:
|
|
207
|
+
data = await client.request("GET", "/v1/save-metrics/config/")
|
|
208
|
+
except AppliedAPIError as exc:
|
|
209
|
+
return _api_error_result(exc)
|
|
210
|
+
|
|
211
|
+
enrolled = bool(data.get("enrolled")) if isinstance(data, dict) else False
|
|
212
|
+
return ToolResult(
|
|
213
|
+
data=data,
|
|
214
|
+
summary=(
|
|
215
|
+
"Save-metric extraction is "
|
|
216
|
+
+ ("enrolled" if enrolled else "NOT enrolled")
|
|
217
|
+
+ " for this shop."
|
|
218
|
+
),
|
|
219
|
+
next_actions=[
|
|
220
|
+
"Use save_metrics_config_update to set cancellation_reasons or save_tactics.",
|
|
221
|
+
"Enrollment needs the save_metric_extraction feature flag (platform "
|
|
222
|
+
"admin) plus a dashboard charting the save metrics.",
|
|
223
|
+
],
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
async def save_metrics_config_update_handler(
|
|
228
|
+
client: AppliedClient,
|
|
229
|
+
params: SaveMetricsConfigUpdateInput,
|
|
230
|
+
) -> ToolResult[Any]:
|
|
231
|
+
body: dict[str, Any] = {}
|
|
232
|
+
if params.cancellation_reasons is not None:
|
|
233
|
+
body["cancellation_reasons"] = params.cancellation_reasons
|
|
234
|
+
if params.save_tactics is not None:
|
|
235
|
+
body["save_tactics"] = params.save_tactics
|
|
236
|
+
if not body:
|
|
237
|
+
return ToolResult(
|
|
238
|
+
ok=False,
|
|
239
|
+
error=ToolError(
|
|
240
|
+
code="input.invalid",
|
|
241
|
+
message="Provide cancellation_reasons and/or save_tactics.",
|
|
242
|
+
retryable=False,
|
|
243
|
+
recovery_hint=(
|
|
244
|
+
"Pass the full replacement list for at least one of "
|
|
245
|
+
"cancellation_reasons or save_tactics."
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
data = await client.request("PUT", "/v1/save-metrics/config/", body=body)
|
|
252
|
+
except AppliedAPIError as exc:
|
|
253
|
+
return _api_error_result(exc)
|
|
254
|
+
|
|
255
|
+
return ToolResult(
|
|
256
|
+
data=data,
|
|
257
|
+
summary=f"Updated save-metric vocabulary ({', '.join(sorted(body))}).",
|
|
258
|
+
next_actions=[
|
|
259
|
+
"Use save_metrics_config_get to verify the effective taxonomy.",
|
|
260
|
+
],
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def dashboard_specs() -> list[ToolSpec]:
|
|
265
|
+
return [
|
|
266
|
+
ToolSpec(
|
|
267
|
+
name="dashboards_list",
|
|
268
|
+
namespace="dashboards",
|
|
269
|
+
description="List the shop's analytics dashboards.",
|
|
270
|
+
input_model=DashboardsListInput,
|
|
271
|
+
output_model=None,
|
|
272
|
+
handler=dashboards_list_handler,
|
|
273
|
+
read_write_mode="read",
|
|
274
|
+
tags=["native"],
|
|
275
|
+
),
|
|
276
|
+
ToolSpec(
|
|
277
|
+
name="dashboards_templates_list",
|
|
278
|
+
namespace="dashboards",
|
|
279
|
+
description=(
|
|
280
|
+
"List built-in dashboard templates (e.g. save_agent) usable "
|
|
281
|
+
"with dashboards_create."
|
|
282
|
+
),
|
|
283
|
+
input_model=DashboardTemplatesListInput,
|
|
284
|
+
output_model=None,
|
|
285
|
+
handler=dashboards_templates_list_handler,
|
|
286
|
+
read_write_mode="read",
|
|
287
|
+
tags=["native"],
|
|
288
|
+
),
|
|
289
|
+
ToolSpec(
|
|
290
|
+
name="dashboards_create",
|
|
291
|
+
namespace="dashboards",
|
|
292
|
+
description=(
|
|
293
|
+
"Create an analytics dashboard, optionally from a built-in "
|
|
294
|
+
"template such as 'save_agent'."
|
|
295
|
+
),
|
|
296
|
+
input_model=DashboardsCreateInput,
|
|
297
|
+
output_model=None,
|
|
298
|
+
handler=dashboards_create_handler,
|
|
299
|
+
read_write_mode="write",
|
|
300
|
+
tags=["native"],
|
|
301
|
+
),
|
|
302
|
+
ToolSpec(
|
|
303
|
+
name="save_metrics_config_get",
|
|
304
|
+
namespace="save_metrics",
|
|
305
|
+
description=(
|
|
306
|
+
"Read the shop's save-metric vocabulary (cancellation reasons "
|
|
307
|
+
"and save tactics), the observed values, and whether the shop "
|
|
308
|
+
"is enrolled in post-conversation save-metric extraction."
|
|
309
|
+
),
|
|
310
|
+
input_model=SaveMetricsConfigGetInput,
|
|
311
|
+
output_model=None,
|
|
312
|
+
handler=save_metrics_config_get_handler,
|
|
313
|
+
read_write_mode="read",
|
|
314
|
+
tags=["native"],
|
|
315
|
+
),
|
|
316
|
+
ToolSpec(
|
|
317
|
+
name="save_metrics_config_update",
|
|
318
|
+
namespace="save_metrics",
|
|
319
|
+
description=(
|
|
320
|
+
"Update the shop's canonical save tactics and/or cancellation "
|
|
321
|
+
"reasons used by save-metric extraction and the Save Agent "
|
|
322
|
+
"dashboard group-bys."
|
|
323
|
+
),
|
|
324
|
+
input_model=SaveMetricsConfigUpdateInput,
|
|
325
|
+
output_model=None,
|
|
326
|
+
handler=save_metrics_config_update_handler,
|
|
327
|
+
read_write_mode="write",
|
|
328
|
+
tags=["native"],
|
|
329
|
+
),
|
|
330
|
+
]
|
|
@@ -27,6 +27,7 @@ applied_cli/v2/catalog.py
|
|
|
27
27
|
applied_cli/v2/connectors.py
|
|
28
28
|
applied_cli/v2/content.py
|
|
29
29
|
applied_cli/v2/conversations.py
|
|
30
|
+
applied_cli/v2/dashboards.py
|
|
30
31
|
applied_cli/v2/domains.py
|
|
31
32
|
applied_cli/v2/evals.py
|
|
32
33
|
applied_cli/v2/flows.py
|
|
@@ -63,6 +64,7 @@ tests/test_v2_catalog_and_mcp.py
|
|
|
63
64
|
tests/test_v2_connectors.py
|
|
64
65
|
tests/test_v2_content.py
|
|
65
66
|
tests/test_v2_conversations.py
|
|
67
|
+
tests/test_v2_dashboards.py
|
|
66
68
|
tests/test_v2_evals.py
|
|
67
69
|
tests/test_v2_flows.py
|
|
68
70
|
tests/test_v2_knowledge.py
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from applied_cli.v2.catalog import get_tool_catalog
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FakeDashboardsClient:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.access_mode = "full_access"
|
|
9
|
+
self.requests = []
|
|
10
|
+
self.responses = {}
|
|
11
|
+
|
|
12
|
+
async def request(self, method, path, params=None, body=None, shop_id=None):
|
|
13
|
+
self.requests.append(
|
|
14
|
+
{"method": method, "path": path, "params": params, "body": body}
|
|
15
|
+
)
|
|
16
|
+
return self.responses.get((method, path), {})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_catalog_exposes_dashboard_and_save_metrics_tools():
|
|
20
|
+
catalog = get_tool_catalog()
|
|
21
|
+
expected = {
|
|
22
|
+
"dashboards_list": "read",
|
|
23
|
+
"dashboards_templates_list": "read",
|
|
24
|
+
"dashboards_create": "write",
|
|
25
|
+
"save_metrics_config_get": "read",
|
|
26
|
+
"save_metrics_config_update": "write",
|
|
27
|
+
}
|
|
28
|
+
for name, mode in expected.items():
|
|
29
|
+
spec = catalog[name]
|
|
30
|
+
assert spec.handler.__module__ == "applied_cli.v2.dashboards"
|
|
31
|
+
assert spec.read_write_mode == mode
|
|
32
|
+
assert "native" in spec.tags
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.asyncio
|
|
36
|
+
async def test_dashboards_templates_list_returns_templates():
|
|
37
|
+
client = FakeDashboardsClient()
|
|
38
|
+
client.responses[("GET", "/v1/dashboards/templates/")] = {
|
|
39
|
+
"templates": [
|
|
40
|
+
{
|
|
41
|
+
"key": "save_agent",
|
|
42
|
+
"name": "Save Agent Dashboard",
|
|
43
|
+
"description": "Track save performance.",
|
|
44
|
+
"chart_count": 10,
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
spec = get_tool_catalog()["dashboards_templates_list"]
|
|
49
|
+
|
|
50
|
+
result = await spec.run(client, {})
|
|
51
|
+
|
|
52
|
+
assert result.ok is True
|
|
53
|
+
assert result.data["count"] == 1
|
|
54
|
+
assert result.data["templates"][0]["key"] == "save_agent"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.asyncio
|
|
58
|
+
async def test_dashboards_create_posts_template_key():
|
|
59
|
+
client = FakeDashboardsClient()
|
|
60
|
+
client.responses[("POST", "/v1/dashboards/")] = {
|
|
61
|
+
"id": "dash-1",
|
|
62
|
+
"name": "Save Agent Dashboard",
|
|
63
|
+
"is_public": True,
|
|
64
|
+
}
|
|
65
|
+
spec = get_tool_catalog()["dashboards_create"]
|
|
66
|
+
|
|
67
|
+
result = await spec.run(client, {"template_key": "save_agent"})
|
|
68
|
+
|
|
69
|
+
assert result.ok is True
|
|
70
|
+
assert result.data["dashboard"]["id"] == "dash-1"
|
|
71
|
+
assert client.requests[-1]["body"] == {
|
|
72
|
+
"is_public": True,
|
|
73
|
+
"template_key": "save_agent",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.asyncio
|
|
78
|
+
async def test_dashboards_list_projects_rows():
|
|
79
|
+
client = FakeDashboardsClient()
|
|
80
|
+
client.responses[("GET", "/v1/dashboards/")] = {
|
|
81
|
+
"results": [
|
|
82
|
+
{"id": "dash-1", "name": "Save Agent Dashboard", "is_public": True},
|
|
83
|
+
{"id": "dash-2", "name": "Support Trends", "is_public": False},
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
spec = get_tool_catalog()["dashboards_list"]
|
|
87
|
+
|
|
88
|
+
result = await spec.run(client, {})
|
|
89
|
+
|
|
90
|
+
assert result.ok is True
|
|
91
|
+
assert result.data["count"] == 2
|
|
92
|
+
assert result.data["dashboards"][0]["id"] == "dash-1"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.mark.asyncio
|
|
96
|
+
async def test_save_metrics_config_get_reports_enrollment():
|
|
97
|
+
client = FakeDashboardsClient()
|
|
98
|
+
client.responses[("GET", "/v1/save-metrics/config/")] = {
|
|
99
|
+
"configured": {"save_tactics": ["Offer a discount"]},
|
|
100
|
+
"observed": {"cancellation_reasons": ["Price"], "save_tactics": []},
|
|
101
|
+
"effective": {
|
|
102
|
+
"cancellation_reasons": ["Price"],
|
|
103
|
+
"save_tactics": ["Offer a discount"],
|
|
104
|
+
},
|
|
105
|
+
"enrolled": True,
|
|
106
|
+
"flag_name": "save_metric_extraction",
|
|
107
|
+
}
|
|
108
|
+
spec = get_tool_catalog()["save_metrics_config_get"]
|
|
109
|
+
|
|
110
|
+
result = await spec.run(client, {})
|
|
111
|
+
|
|
112
|
+
assert result.ok is True
|
|
113
|
+
assert "enrolled" in result.summary
|
|
114
|
+
assert result.data["effective"]["save_tactics"] == ["Offer a discount"]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@pytest.mark.asyncio
|
|
118
|
+
async def test_save_metrics_config_update_puts_only_provided_lists():
|
|
119
|
+
client = FakeDashboardsClient()
|
|
120
|
+
client.responses[("PUT", "/v1/save-metrics/config/")] = {
|
|
121
|
+
"configured": {"save_tactics": ["Recipe Swap"]},
|
|
122
|
+
"effective": {"cancellation_reasons": [], "save_tactics": ["Recipe Swap"]},
|
|
123
|
+
"enrolled": False,
|
|
124
|
+
}
|
|
125
|
+
spec = get_tool_catalog()["save_metrics_config_update"]
|
|
126
|
+
|
|
127
|
+
result = await spec.run(client, {"save_tactics": ["Recipe Swap"]})
|
|
128
|
+
|
|
129
|
+
assert result.ok is True
|
|
130
|
+
assert client.requests[-1]["body"] == {"save_tactics": ["Recipe Swap"]}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@pytest.mark.asyncio
|
|
134
|
+
async def test_save_metrics_config_update_requires_a_field():
|
|
135
|
+
client = FakeDashboardsClient()
|
|
136
|
+
spec = get_tool_catalog()["save_metrics_config_update"]
|
|
137
|
+
|
|
138
|
+
result = await spec.run(client, {})
|
|
139
|
+
|
|
140
|
+
assert result.ok is False
|
|
141
|
+
assert result.error.code == "input.invalid"
|
|
142
|
+
assert client.requests == []
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|