applied-cli 0.5.37__tar.gz → 0.5.39__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.37 → applied_cli-0.5.39}/PKG-INFO +1 -1
- {applied_cli-0.5.37 → applied_cli-0.5.39}/applied_cli/cli.py +78 -21
- {applied_cli-0.5.37 → applied_cli-0.5.39}/applied_cli/client.py +54 -0
- {applied_cli-0.5.37 → applied_cli-0.5.39}/applied_cli/tools.py +59 -126
- {applied_cli-0.5.37 → applied_cli-0.5.39}/applied_cli.egg-info/PKG-INFO +1 -1
- {applied_cli-0.5.37 → applied_cli-0.5.39}/pyproject.toml +1 -1
- {applied_cli-0.5.37 → applied_cli-0.5.39}/README.md +0 -0
- {applied_cli-0.5.37 → applied_cli-0.5.39}/applied_cli/__init__.py +0 -0
- {applied_cli-0.5.37 → applied_cli-0.5.39}/applied_cli/credentials.py +0 -0
- {applied_cli-0.5.37 → applied_cli-0.5.39}/applied_cli/formatters.py +0 -0
- {applied_cli-0.5.37 → applied_cli-0.5.39}/applied_cli.egg-info/SOURCES.txt +0 -0
- {applied_cli-0.5.37 → applied_cli-0.5.39}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.5.37 → applied_cli-0.5.39}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.5.37 → applied_cli-0.5.39}/applied_cli.egg-info/requires.txt +0 -0
- {applied_cli-0.5.37 → applied_cli-0.5.39}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.5.37 → applied_cli-0.5.39}/setup.cfg +0 -0
|
@@ -178,40 +178,97 @@ def taxonomy(
|
|
|
178
178
|
|
|
179
179
|
|
|
180
180
|
@app.command()
|
|
181
|
-
def
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
"--
|
|
185
|
-
|
|
181
|
+
def analytics(
|
|
182
|
+
group_by: str = typer.Option(
|
|
183
|
+
...,
|
|
184
|
+
"--group-by",
|
|
185
|
+
"-g",
|
|
186
|
+
help=(
|
|
187
|
+
"Comma-separated dimensions: label_name, sublabel_name, agent_name, "
|
|
188
|
+
"resolution, sentiment, type, user_id, label_id, sublabel_id, agent_id"
|
|
189
|
+
),
|
|
186
190
|
),
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
"--
|
|
190
|
-
|
|
191
|
+
metrics: str = typer.Option(
|
|
192
|
+
"count",
|
|
193
|
+
"--metrics",
|
|
194
|
+
"-m",
|
|
195
|
+
help="Comma-separated metrics: count, avg_score, avg_ttr, resolution_rate",
|
|
191
196
|
),
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
"--
|
|
195
|
-
help=
|
|
197
|
+
filter: list[str] = typer.Option(
|
|
198
|
+
[],
|
|
199
|
+
"--filter",
|
|
200
|
+
help='Filter as "field=value". Repeatable.',
|
|
196
201
|
),
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
202
|
+
start: str = typer.Option(None, "--start", help="Start date, e.g. 2025-01-01"),
|
|
203
|
+
end: str = typer.Option(None, "--end", help="End date, e.g. 2025-01-31"),
|
|
204
|
+
limit: int = typer.Option(1000, "--limit", "-l", help="Max rows to return"),
|
|
205
|
+
format: str = typer.Option(
|
|
206
|
+
"csv", "--format", "-f", help="Output format: csv or json"
|
|
207
|
+
),
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Run a server-side analytics query grouped by dimensions."""
|
|
210
|
+
client = get_client()
|
|
211
|
+
result = asyncio.run(
|
|
212
|
+
tools.analytics_query(
|
|
213
|
+
client,
|
|
214
|
+
group_by=[g.strip() for g in group_by.split(",") if g.strip()],
|
|
215
|
+
metrics=[m.strip() for m in metrics.split(",") if m.strip()],
|
|
216
|
+
filters=list(filter) or None,
|
|
217
|
+
start=start,
|
|
218
|
+
end=end,
|
|
219
|
+
limit=limit if limit != 1000 else None,
|
|
220
|
+
output_format=format,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
typer.echo(result)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@app.command()
|
|
227
|
+
def metrics(
|
|
228
|
+
metric_name: str = typer.Option(
|
|
229
|
+
...,
|
|
230
|
+
"--metric-name",
|
|
231
|
+
"-n",
|
|
232
|
+
help="Metric name, e.g. customer.cancel, customer.save",
|
|
233
|
+
),
|
|
234
|
+
group_by: str = typer.Option(
|
|
235
|
+
"",
|
|
236
|
+
"--group-by",
|
|
237
|
+
"-g",
|
|
238
|
+
help="Property key to group by, e.g. reason",
|
|
239
|
+
),
|
|
240
|
+
period: str = typer.Option(
|
|
241
|
+
"",
|
|
242
|
+
"--period",
|
|
243
|
+
"-p",
|
|
244
|
+
help="Trending period: day, week, month",
|
|
245
|
+
),
|
|
246
|
+
filter: list[str] = typer.Option(
|
|
247
|
+
[],
|
|
248
|
+
"--filter",
|
|
249
|
+
help='Property filter as "key=value". Repeatable.',
|
|
250
|
+
),
|
|
251
|
+
start: str = typer.Option(None, "--start", help="Start date, e.g. 2025-01-01"),
|
|
252
|
+
end: str = typer.Option(None, "--end", help="End date, e.g. 2025-01-31"),
|
|
253
|
+
object_type: str = typer.Option(
|
|
254
|
+
"conversation", "--object-type", help="Object type"
|
|
201
255
|
),
|
|
202
256
|
format: str = typer.Option(
|
|
203
257
|
"csv", "--format", "-f", help="Output format: csv or json"
|
|
204
258
|
),
|
|
205
259
|
) -> None:
|
|
206
|
-
"""
|
|
260
|
+
"""Query metric events with property grouping and trending."""
|
|
207
261
|
client = get_client()
|
|
208
262
|
result = asyncio.run(
|
|
209
|
-
tools.
|
|
263
|
+
tools.analytics_metrics(
|
|
210
264
|
client,
|
|
265
|
+
metric_name=metric_name,
|
|
266
|
+
group_by=group_by or None,
|
|
267
|
+
period=period or None,
|
|
268
|
+
filters=list(filter) or None,
|
|
211
269
|
start=start,
|
|
212
270
|
end=end,
|
|
213
|
-
|
|
214
|
-
include_zero=include_zero,
|
|
271
|
+
object_type=object_type,
|
|
215
272
|
output_format=format,
|
|
216
273
|
)
|
|
217
274
|
)
|
|
@@ -231,6 +231,60 @@ class AppliedClient:
|
|
|
231
231
|
data = await self._request("GET", "/v1/property-choices/", params=params)
|
|
232
232
|
return self._normalize_response(data)
|
|
233
233
|
|
|
234
|
+
# -------------------------------------------------------------------------
|
|
235
|
+
# Analytics
|
|
236
|
+
# -------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
async def analytics_query(
|
|
239
|
+
self,
|
|
240
|
+
group_by: list[str],
|
|
241
|
+
metrics: list[str] | None = None,
|
|
242
|
+
filters: list[str] | None = None,
|
|
243
|
+
start: str | None = None,
|
|
244
|
+
end: str | None = None,
|
|
245
|
+
limit: int | None = None,
|
|
246
|
+
) -> dict:
|
|
247
|
+
"""Run a GROUP BY query against conversation_latest_state."""
|
|
248
|
+
params: dict[str, Any] = {"group_by": ",".join(group_by)}
|
|
249
|
+
if metrics:
|
|
250
|
+
params["metrics"] = ",".join(metrics)
|
|
251
|
+
if filters:
|
|
252
|
+
params["filter"] = filters
|
|
253
|
+
if start:
|
|
254
|
+
params["start_date"] = start
|
|
255
|
+
if end:
|
|
256
|
+
params["end_date"] = end
|
|
257
|
+
if limit:
|
|
258
|
+
params["limit"] = limit
|
|
259
|
+
return await self._request("GET", "/v2/analytics/query/", params=params)
|
|
260
|
+
|
|
261
|
+
async def analytics_metrics(
|
|
262
|
+
self,
|
|
263
|
+
metric_name: str,
|
|
264
|
+
group_by: str | None = None,
|
|
265
|
+
period: str | None = None,
|
|
266
|
+
filters: list[str] | None = None,
|
|
267
|
+
start: str | None = None,
|
|
268
|
+
end: str | None = None,
|
|
269
|
+
object_type: str = "conversation",
|
|
270
|
+
) -> dict:
|
|
271
|
+
"""Query metric_events_hour with optional property grouping and trending."""
|
|
272
|
+
params: dict[str, Any] = {
|
|
273
|
+
"metric_name": metric_name,
|
|
274
|
+
"object_type": object_type,
|
|
275
|
+
}
|
|
276
|
+
if group_by:
|
|
277
|
+
params["group_by"] = group_by
|
|
278
|
+
if period:
|
|
279
|
+
params["period"] = period
|
|
280
|
+
if filters:
|
|
281
|
+
params["filter"] = filters
|
|
282
|
+
if start:
|
|
283
|
+
params["start_date"] = start
|
|
284
|
+
if end:
|
|
285
|
+
params["end_date"] = end
|
|
286
|
+
return await self._request("GET", "/v2/analytics/metrics/", params=params)
|
|
287
|
+
|
|
234
288
|
# -------------------------------------------------------------------------
|
|
235
289
|
# Conversations
|
|
236
290
|
# -------------------------------------------------------------------------
|
|
@@ -4,8 +4,6 @@ These functions wrap the client methods with formatting logic,
|
|
|
4
4
|
suitable for both MCP tools and CLI commands.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import csv
|
|
8
|
-
import io
|
|
9
7
|
import json
|
|
10
8
|
|
|
11
9
|
from applied_cli.client import AppliedAPIError, AppliedClient
|
|
@@ -82,143 +80,78 @@ async def taxonomy_list(
|
|
|
82
80
|
return to_json(items)
|
|
83
81
|
|
|
84
82
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
async def analytics_query(
|
|
84
|
+
client: AppliedClient,
|
|
85
|
+
group_by: list[str],
|
|
86
|
+
metrics: list[str] | None = None,
|
|
87
|
+
filters: list[str] | None = None,
|
|
88
|
+
start: str | None = None,
|
|
89
|
+
end: str | None = None,
|
|
90
|
+
limit: int | None = None,
|
|
91
|
+
output_format: str = "csv",
|
|
89
92
|
) -> str:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
summary_output = io.StringIO()
|
|
93
|
-
summary_writer = csv.writer(summary_output)
|
|
94
|
-
summary_writer.writerow(["field", "value"])
|
|
95
|
-
for key in ["start", "end", "date_field", "total_conversations"]:
|
|
96
|
-
summary_writer.writerow([key, summary.get(key, "")])
|
|
97
|
-
sections.append("# summary")
|
|
98
|
-
sections.append(summary_output.getvalue().strip())
|
|
99
|
-
|
|
100
|
-
sections.append("# topics")
|
|
101
|
-
sections.append(to_csv(topics, ["id", "name", "conversation_count"]).strip())
|
|
102
|
-
|
|
103
|
-
sections.append("# intents")
|
|
104
|
-
sections.append(
|
|
105
|
-
to_csv(
|
|
106
|
-
intents,
|
|
107
|
-
["id", "name", "topic_id", "topic_name", "conversation_count"],
|
|
108
|
-
).strip()
|
|
109
|
-
)
|
|
93
|
+
"""Run a server-side ClickHouse GROUP BY query on conversation data.
|
|
110
94
|
|
|
111
|
-
|
|
95
|
+
Dimensions: label_id, label_name, sublabel_id, sublabel_name,
|
|
96
|
+
agent_id, agent_name, resolution, sentiment, score, type, user_id
|
|
97
|
+
Metrics: count, avg_score, avg_ttr, resolution_rate
|
|
98
|
+
Filters: agent_id, label_id, sublabel_id, type, resolution, sentiment, score, user_id
|
|
99
|
+
"""
|
|
100
|
+
data = await client.analytics_query(
|
|
101
|
+
group_by=group_by,
|
|
102
|
+
metrics=metrics,
|
|
103
|
+
filters=filters,
|
|
104
|
+
start=start,
|
|
105
|
+
end=end,
|
|
106
|
+
limit=limit,
|
|
107
|
+
)
|
|
108
|
+
rows = data.get("rows", [])
|
|
109
|
+
if not rows:
|
|
110
|
+
return "No data found for the given query."
|
|
111
|
+
if output_format == "json":
|
|
112
|
+
return to_json(data)
|
|
113
|
+
columns = data.get("dimensions", group_by) + data.get(
|
|
114
|
+
"metrics", metrics or ["count"]
|
|
115
|
+
)
|
|
116
|
+
return to_csv(rows, columns)
|
|
112
117
|
|
|
113
118
|
|
|
114
|
-
async def
|
|
119
|
+
async def analytics_metrics(
|
|
115
120
|
client: AppliedClient,
|
|
121
|
+
metric_name: str,
|
|
122
|
+
group_by: str | None = None,
|
|
123
|
+
period: str | None = None,
|
|
124
|
+
filters: list[str] | None = None,
|
|
116
125
|
start: str | None = None,
|
|
117
126
|
end: str | None = None,
|
|
118
|
-
|
|
119
|
-
include_zero: bool = True,
|
|
127
|
+
object_type: str = "conversation",
|
|
120
128
|
output_format: str = "csv",
|
|
121
129
|
) -> str:
|
|
122
|
-
"""
|
|
123
|
-
Aggregate conversation counts by topic and intent.
|
|
124
|
-
|
|
125
|
-
Args:
|
|
126
|
-
client: Authenticated AppliedClient
|
|
127
|
-
start: Inclusive start timestamp/date string
|
|
128
|
-
end: Inclusive end timestamp/date string
|
|
129
|
-
date_field: Conversation date field to filter on: 'created_at' or 'last_message_at'
|
|
130
|
-
include_zero: Include zero-count taxonomy rows
|
|
131
|
-
output_format: 'csv' or 'json'
|
|
130
|
+
"""Query metric events with optional property grouping and trending.
|
|
132
131
|
|
|
133
|
-
|
|
134
|
-
Topic and intent counts with optional time-range summary.
|
|
132
|
+
Use for custom business metrics: cancel reasons, save rates, etc.
|
|
135
133
|
"""
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
filters: dict[str, dict[str, str]] = {}
|
|
146
|
-
if start or end:
|
|
147
|
-
filters[date_field] = {}
|
|
148
|
-
if start:
|
|
149
|
-
filters[date_field]["gte"] = start
|
|
150
|
-
if end:
|
|
151
|
-
filters[date_field]["lte"] = end
|
|
152
|
-
|
|
153
|
-
conversations = await client.query_conversations(
|
|
154
|
-
filters=filters or None,
|
|
155
|
-
limit=100,
|
|
156
|
-
fetch_all=True,
|
|
134
|
+
data = await client.analytics_metrics(
|
|
135
|
+
metric_name=metric_name,
|
|
136
|
+
group_by=group_by,
|
|
137
|
+
period=period,
|
|
138
|
+
filters=filters,
|
|
139
|
+
start=start,
|
|
140
|
+
end=end,
|
|
141
|
+
object_type=object_type,
|
|
157
142
|
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
conversation.get("label_id")
|
|
162
|
-
or (conversation.get("label") or {}).get("id")
|
|
163
|
-
or (conversation.get("label") or {}).get("uuid")
|
|
164
|
-
)
|
|
165
|
-
intent_id = (
|
|
166
|
-
conversation.get("sublabel_id")
|
|
167
|
-
or (conversation.get("sublabel") or {}).get("id")
|
|
168
|
-
or (conversation.get("sublabel") or {}).get("uuid")
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
if topic_id is not None:
|
|
172
|
-
topic_key = str(topic_id)
|
|
173
|
-
if topic_key in topic_counts:
|
|
174
|
-
topic_counts[topic_key] += 1
|
|
175
|
-
if intent_id is not None:
|
|
176
|
-
intent_key = str(intent_id)
|
|
177
|
-
if intent_key in intent_counts:
|
|
178
|
-
intent_counts[intent_key] += 1
|
|
179
|
-
|
|
180
|
-
topic_rows = [
|
|
181
|
-
{
|
|
182
|
-
"id": str(topic.get("id")),
|
|
183
|
-
"name": topic.get("name", ""),
|
|
184
|
-
"conversation_count": topic_counts.get(str(topic.get("id")), 0),
|
|
185
|
-
}
|
|
186
|
-
for topic in topics
|
|
187
|
-
]
|
|
188
|
-
intent_rows = [
|
|
189
|
-
{
|
|
190
|
-
"id": str(intent.get("id")),
|
|
191
|
-
"name": intent.get("name", ""),
|
|
192
|
-
"topic_id": str(intent.get("parent_choice_id") or ""),
|
|
193
|
-
"topic_name": intent.get("parent_choice_name", ""),
|
|
194
|
-
"conversation_count": intent_counts.get(str(intent.get("id")), 0),
|
|
195
|
-
}
|
|
196
|
-
for intent in intents
|
|
197
|
-
]
|
|
198
|
-
|
|
199
|
-
if not include_zero:
|
|
200
|
-
topic_rows = [row for row in topic_rows if row["conversation_count"] > 0]
|
|
201
|
-
intent_rows = [row for row in intent_rows if row["conversation_count"] > 0]
|
|
202
|
-
|
|
203
|
-
topic_rows.sort(key=lambda row: (-row["conversation_count"], row["name"]))
|
|
204
|
-
intent_rows.sort(key=lambda row: (-row["conversation_count"], row["name"]))
|
|
205
|
-
|
|
206
|
-
summary = {
|
|
207
|
-
"start": start or "",
|
|
208
|
-
"end": end or "",
|
|
209
|
-
"date_field": date_field,
|
|
210
|
-
"total_conversations": len(conversations),
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
payload = {
|
|
214
|
-
"summary": summary,
|
|
215
|
-
"topics": topic_rows,
|
|
216
|
-
"intents": intent_rows,
|
|
217
|
-
}
|
|
218
|
-
|
|
143
|
+
rows = data.get("rows", [])
|
|
144
|
+
if not rows:
|
|
145
|
+
return "No data found for the given query."
|
|
219
146
|
if output_format == "json":
|
|
220
|
-
return to_json(
|
|
221
|
-
|
|
147
|
+
return to_json(data)
|
|
148
|
+
columns: list[str] = []
|
|
149
|
+
if group_by:
|
|
150
|
+
columns.append("prop_group")
|
|
151
|
+
if period:
|
|
152
|
+
columns.append("period")
|
|
153
|
+
columns.extend(["event_count", "object_count"])
|
|
154
|
+
return to_csv(rows, columns)
|
|
222
155
|
|
|
223
156
|
|
|
224
157
|
async def conversation_query(
|
|
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
|