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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: applied-cli
3
- Version: 0.5.37
3
+ Version: 0.5.39
4
4
  Summary: CLI and shared client library for Applied Labs AI support agents
5
5
  Author: Applied Labs
6
6
  License-Expression: MIT
@@ -178,40 +178,97 @@ def taxonomy(
178
178
 
179
179
 
180
180
  @app.command()
181
- def taxonomy_counts(
182
- start: str = typer.Option(
183
- None,
184
- "--start",
185
- help="Inclusive start timestamp/date, e.g. 2026-03-01 or 2026-03-01T00:00:00Z",
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
- end: str = typer.Option(
188
- None,
189
- "--end",
190
- help="Inclusive end timestamp/date, e.g. 2026-03-18 or 2026-03-18T23:59:59Z",
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
- date_field: str = typer.Option(
193
- "created_at",
194
- "--date-field",
195
- help="Conversation date field to filter on: created_at or last_message_at",
197
+ filter: list[str] = typer.Option(
198
+ [],
199
+ "--filter",
200
+ help='Filter as "field=value". Repeatable.',
196
201
  ),
197
- include_zero: bool = typer.Option(
198
- True,
199
- "--include-zero/--exclude-zero",
200
- help="Include zero-count topics and intents",
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
- """Aggregate conversation counts by topic and intent."""
260
+ """Query metric events with property grouping and trending."""
207
261
  client = get_client()
208
262
  result = asyncio.run(
209
- tools.taxonomy_counts(
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
- date_field=date_field,
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 _taxonomy_counts_csv(
86
- summary: dict,
87
- topics: list[dict],
88
- intents: list[dict],
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
- sections: list[str] = []
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
- return "\n\n".join(sections) + "\n"
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 taxonomy_counts(
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
- date_field: str = "created_at",
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
- Returns:
134
- Topic and intent counts with optional time-range summary.
132
+ Use for custom business metrics: cancel reasons, save rates, etc.
135
133
  """
136
- if date_field not in {"created_at", "last_message_at"}:
137
- raise ValueError("date_field must be 'created_at' or 'last_message_at'")
138
-
139
- topics = await client.list_taxonomy("topics")
140
- intents = await client.list_taxonomy("intents")
141
-
142
- topic_counts = {str(topic.get("id")): 0 for topic in topics}
143
- intent_counts = {str(intent.get("id")): 0 for intent in intents}
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
- for conversation in conversations:
160
- topic_id = (
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(payload)
221
- return _taxonomy_counts_csv(summary, topic_rows, intent_rows)
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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: applied-cli
3
- Version: 0.5.37
3
+ Version: 0.5.39
4
4
  Summary: CLI and shared client library for Applied Labs AI support agents
5
5
  Author: Applied Labs
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "applied-cli"
3
- version = "0.5.37"
3
+ version = "0.5.39"
4
4
  description = "CLI and shared client library for Applied Labs AI support agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
File without changes
File without changes