applied-cli 0.5.69__tar.gz → 0.5.71__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.69 → applied_cli-0.5.71}/PKG-INFO +1 -1
- {applied_cli-0.5.69 → applied_cli-0.5.71}/applied_cli/__init__.py +1 -1
- {applied_cli-0.5.69 → applied_cli-0.5.71}/applied_cli/cli.py +2 -2
- {applied_cli-0.5.69 → applied_cli-0.5.71}/applied_cli/client.py +212 -20
- {applied_cli-0.5.69 → applied_cli-0.5.71}/applied_cli/flow_helpers.py +3 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/applied_cli/tools.py +84 -13
- {applied_cli-0.5.69 → applied_cli-0.5.71}/applied_cli.egg-info/PKG-INFO +1 -1
- {applied_cli-0.5.69 → applied_cli-0.5.71}/pyproject.toml +1 -1
- {applied_cli-0.5.69 → applied_cli-0.5.71}/tests/test_client.py +68 -40
- {applied_cli-0.5.69 → applied_cli-0.5.71}/README.md +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/applied_cli/agent_scoped_flows.py +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/applied_cli/conversation_lookup.py +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/applied_cli/conversations.py +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/applied_cli/credentials.py +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/applied_cli/formatters.py +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/applied_cli.egg-info/SOURCES.txt +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/applied_cli.egg-info/requires.txt +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/setup.cfg +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/tests/test_agent_scoped_flows.py +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/tests/test_audit_tools.py +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/tests/test_benchmark_scenario_tools.py +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/tests/test_cli.py +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/tests/test_conversation_tools.py +0 -0
- {applied_cli-0.5.69 → applied_cli-0.5.71}/tests/test_flow_tools.py +0 -0
|
@@ -289,7 +289,7 @@ def analytics(
|
|
|
289
289
|
"--group-by",
|
|
290
290
|
"-g",
|
|
291
291
|
help=(
|
|
292
|
-
"Comma-separated dimensions: label_name, sublabel_name,
|
|
292
|
+
"Comma-separated dimensions: topic, intent, label_name, sublabel_name, "
|
|
293
293
|
"resolution, sentiment, type, user_id, label_id, sublabel_id, agent_id"
|
|
294
294
|
),
|
|
295
295
|
),
|
|
@@ -297,7 +297,7 @@ def analytics(
|
|
|
297
297
|
"count",
|
|
298
298
|
"--metrics",
|
|
299
299
|
"-m",
|
|
300
|
-
help="Comma-separated metrics: count
|
|
300
|
+
help="Comma-separated metrics: count",
|
|
301
301
|
),
|
|
302
302
|
filter: list[str] | None = typer.Option( # noqa: B008
|
|
303
303
|
None,
|
|
@@ -192,6 +192,23 @@ _ANALYTICS_DIMENSION_SQL = {
|
|
|
192
192
|
"type": "type",
|
|
193
193
|
"user_id": "user_id",
|
|
194
194
|
}
|
|
195
|
+
_ANALYTICS_AUTOMATION_DIMENSIONS = {
|
|
196
|
+
"intent": "sublabel_name",
|
|
197
|
+
"label_id": "label_id",
|
|
198
|
+
"label_name": "label_name",
|
|
199
|
+
"sublabel_id": "sublabel_id",
|
|
200
|
+
"sublabel_name": "sublabel_name",
|
|
201
|
+
"topic": "label_name",
|
|
202
|
+
}
|
|
203
|
+
_ANALYTICS_DASHBOARD_DIMENSIONS = {
|
|
204
|
+
"agent_id": "agent_id",
|
|
205
|
+
"label_id": "label_id",
|
|
206
|
+
"resolution": "resolution",
|
|
207
|
+
"sentiment": "sentiment",
|
|
208
|
+
"sublabel_id": "sublabel_id",
|
|
209
|
+
"type": "type",
|
|
210
|
+
"user_id": "user_id",
|
|
211
|
+
}
|
|
195
212
|
_ANALYTICS_FILTER_SQL = {
|
|
196
213
|
**_ANALYTICS_DIMENSION_SQL,
|
|
197
214
|
"score": "score",
|
|
@@ -297,6 +314,52 @@ def _build_legacy_analytics_sql(
|
|
|
297
314
|
return "\n".join(sql_parts)
|
|
298
315
|
|
|
299
316
|
|
|
317
|
+
def _parse_legacy_analytics_filters(filters: list[str] | None) -> dict[str, Any]:
|
|
318
|
+
parsed: dict[str, Any] = {}
|
|
319
|
+
for raw_filter in filters or []:
|
|
320
|
+
if "=" not in raw_filter:
|
|
321
|
+
raise ValueError(f"Invalid analytics filter: {raw_filter}")
|
|
322
|
+
field, raw_value = raw_filter.split("=", 1)
|
|
323
|
+
field = field.strip()
|
|
324
|
+
if field not in _ANALYTICS_FILTER_SQL:
|
|
325
|
+
raise ValueError(f"Unsupported analytics filter field: {field}")
|
|
326
|
+
parsed[field] = _decode_filter_value(raw_value)
|
|
327
|
+
return parsed
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _validate_analytics_count_metrics(metrics: list[str] | None) -> list[str]:
|
|
331
|
+
metric_names = metrics or ["count"]
|
|
332
|
+
unsupported = [metric for metric in metric_names if metric != "count"]
|
|
333
|
+
if unsupported:
|
|
334
|
+
raise ValueError(
|
|
335
|
+
"The current analytics endpoint only supports the count metric for "
|
|
336
|
+
"applied analytics. Use applied analytics-report for richer metrics."
|
|
337
|
+
)
|
|
338
|
+
return metric_names
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _decode_dashboard_group(group: Any, expected_count: int) -> list[Any]:
|
|
342
|
+
if expected_count == 1:
|
|
343
|
+
return [group]
|
|
344
|
+
if isinstance(group, str):
|
|
345
|
+
try:
|
|
346
|
+
decoded = json.loads(group)
|
|
347
|
+
except json.JSONDecodeError:
|
|
348
|
+
decoded = group
|
|
349
|
+
if isinstance(decoded, list):
|
|
350
|
+
return decoded
|
|
351
|
+
if isinstance(group, list):
|
|
352
|
+
return group
|
|
353
|
+
return [group]
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _sort_analytics_rows(rows: list[dict[str, Any]], group_by: list[str]) -> None:
|
|
357
|
+
def sort_key(row: dict[str, Any]) -> tuple[str, ...]:
|
|
358
|
+
return tuple("" if row.get(dimension) is None else str(row.get(dimension)) for dimension in group_by)
|
|
359
|
+
|
|
360
|
+
rows.sort(key=sort_key)
|
|
361
|
+
|
|
362
|
+
|
|
300
363
|
class AppliedClient:
|
|
301
364
|
"""Async client for Applied Labs API."""
|
|
302
365
|
|
|
@@ -612,20 +675,154 @@ class AppliedClient:
|
|
|
612
675
|
end: str | None = None,
|
|
613
676
|
limit: int | None = None,
|
|
614
677
|
) -> dict:
|
|
615
|
-
"""Run a
|
|
616
|
-
|
|
617
|
-
group_by
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
678
|
+
"""Run a grouped conversation count query through the analytics API."""
|
|
679
|
+
if not group_by:
|
|
680
|
+
raise ValueError("group_by is required")
|
|
681
|
+
|
|
682
|
+
metric_names = _validate_analytics_count_metrics(metrics)
|
|
683
|
+
parsed_filters = _parse_legacy_analytics_filters(filters)
|
|
684
|
+
|
|
685
|
+
if all(dimension in _ANALYTICS_AUTOMATION_DIMENSIONS for dimension in group_by):
|
|
686
|
+
data = await self._taxonomy_analytics_query(
|
|
687
|
+
group_by=group_by,
|
|
688
|
+
filters=parsed_filters,
|
|
689
|
+
start=start,
|
|
690
|
+
end=end,
|
|
691
|
+
limit=limit,
|
|
692
|
+
)
|
|
693
|
+
else:
|
|
694
|
+
data = await self._dashboard_analytics_query(
|
|
695
|
+
group_by=group_by,
|
|
696
|
+
filters=parsed_filters,
|
|
697
|
+
start=start,
|
|
698
|
+
end=end,
|
|
699
|
+
limit=limit,
|
|
700
|
+
)
|
|
701
|
+
|
|
625
702
|
data["dimensions"] = group_by
|
|
626
|
-
data["metrics"] =
|
|
703
|
+
data["metrics"] = metric_names
|
|
627
704
|
return data
|
|
628
705
|
|
|
706
|
+
async def _taxonomy_analytics_query(
|
|
707
|
+
self,
|
|
708
|
+
*,
|
|
709
|
+
group_by: list[str],
|
|
710
|
+
filters: dict[str, Any],
|
|
711
|
+
start: str | None,
|
|
712
|
+
end: str | None,
|
|
713
|
+
limit: int | None,
|
|
714
|
+
) -> dict:
|
|
715
|
+
payload: dict[str, Any] = {
|
|
716
|
+
"view": "automation_aggregates",
|
|
717
|
+
"model": "conversation",
|
|
718
|
+
**filters,
|
|
719
|
+
}
|
|
720
|
+
if start:
|
|
721
|
+
payload["start_date"] = start
|
|
722
|
+
if end:
|
|
723
|
+
payload["end_date"] = end
|
|
724
|
+
|
|
725
|
+
data = await self.analytics_report(payload)
|
|
726
|
+
source_key = (
|
|
727
|
+
"by_intent"
|
|
728
|
+
if any(
|
|
729
|
+
dimension in {"intent", "sublabel_id", "sublabel_name"}
|
|
730
|
+
for dimension in group_by
|
|
731
|
+
)
|
|
732
|
+
else "by_topic"
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
grouped: dict[tuple[Any, ...], int] = {}
|
|
736
|
+
for row in data.get(source_key, []):
|
|
737
|
+
key = tuple(
|
|
738
|
+
row.get(_ANALYTICS_AUTOMATION_DIMENSIONS[dimension])
|
|
739
|
+
for dimension in group_by
|
|
740
|
+
)
|
|
741
|
+
grouped[key] = grouped.get(key, 0) + int(row.get("total") or 0)
|
|
742
|
+
|
|
743
|
+
rows = [
|
|
744
|
+
{
|
|
745
|
+
**dict(zip(group_by, key, strict=False)),
|
|
746
|
+
"count": count,
|
|
747
|
+
}
|
|
748
|
+
for key, count in grouped.items()
|
|
749
|
+
]
|
|
750
|
+
_sort_analytics_rows(rows, group_by)
|
|
751
|
+
|
|
752
|
+
total_rows = len(rows)
|
|
753
|
+
if limit is not None:
|
|
754
|
+
rows = rows[:limit]
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
"columns": [*group_by, "count"],
|
|
758
|
+
"rows": rows,
|
|
759
|
+
"row_count": len(rows),
|
|
760
|
+
"truncated": limit is not None and total_rows > len(rows),
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async def _dashboard_analytics_query(
|
|
764
|
+
self,
|
|
765
|
+
*,
|
|
766
|
+
group_by: list[str],
|
|
767
|
+
filters: dict[str, Any],
|
|
768
|
+
start: str | None,
|
|
769
|
+
end: str | None,
|
|
770
|
+
limit: int | None,
|
|
771
|
+
) -> dict:
|
|
772
|
+
backend_group_by: list[str] = []
|
|
773
|
+
unsupported: list[str] = []
|
|
774
|
+
for dimension in group_by:
|
|
775
|
+
backend_dimension = _ANALYTICS_DASHBOARD_DIMENSIONS.get(dimension)
|
|
776
|
+
if backend_dimension is None:
|
|
777
|
+
unsupported.append(dimension)
|
|
778
|
+
else:
|
|
779
|
+
backend_group_by.append(backend_dimension)
|
|
780
|
+
|
|
781
|
+
if unsupported:
|
|
782
|
+
raise ValueError(
|
|
783
|
+
"Unsupported analytics dimension for the current endpoint: "
|
|
784
|
+
+ ", ".join(unsupported)
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
payload: dict[str, Any] = {
|
|
788
|
+
"view": "dashboard_rate_metrics",
|
|
789
|
+
"model": "conversation",
|
|
790
|
+
"group_by": ",".join(backend_group_by),
|
|
791
|
+
"custom_rate_metrics_requests": [
|
|
792
|
+
{"rate": "count", "numerator": "total"},
|
|
793
|
+
],
|
|
794
|
+
}
|
|
795
|
+
if start:
|
|
796
|
+
payload["start_date"] = start
|
|
797
|
+
if end:
|
|
798
|
+
payload["end_date"] = end
|
|
799
|
+
if filters:
|
|
800
|
+
payload["conversation_filters"] = filters
|
|
801
|
+
|
|
802
|
+
data = await self.analytics_report(payload)
|
|
803
|
+
rows: list[dict[str, Any]] = []
|
|
804
|
+
for item in data.get("count", []):
|
|
805
|
+
group_values = _decode_dashboard_group(item.get("group"), len(group_by))
|
|
806
|
+
group_values = [*group_values, *([None] * len(group_by))][: len(group_by)]
|
|
807
|
+
rows.append(
|
|
808
|
+
{
|
|
809
|
+
**dict(zip(group_by, group_values, strict=False)),
|
|
810
|
+
"count": item.get("numerator", item.get("rate", 0)),
|
|
811
|
+
}
|
|
812
|
+
)
|
|
813
|
+
_sort_analytics_rows(rows, group_by)
|
|
814
|
+
|
|
815
|
+
total_rows = len(rows)
|
|
816
|
+
if limit is not None:
|
|
817
|
+
rows = rows[:limit]
|
|
818
|
+
|
|
819
|
+
return {
|
|
820
|
+
"columns": [*group_by, "count"],
|
|
821
|
+
"rows": rows,
|
|
822
|
+
"row_count": len(rows),
|
|
823
|
+
"truncated": limit is not None and total_rows > len(rows),
|
|
824
|
+
}
|
|
825
|
+
|
|
629
826
|
async def analytics_sql(
|
|
630
827
|
self,
|
|
631
828
|
*,
|
|
@@ -633,15 +830,10 @@ class AppliedClient:
|
|
|
633
830
|
parameters: dict[str, Any] | None = None,
|
|
634
831
|
max_rows: int | None = None,
|
|
635
832
|
) -> dict:
|
|
636
|
-
|
|
637
|
-
"
|
|
638
|
-
"
|
|
639
|
-
|
|
640
|
-
if parameters:
|
|
641
|
-
body["parameters"] = parameters
|
|
642
|
-
if max_rows is not None:
|
|
643
|
-
body["max_rows"] = max_rows
|
|
644
|
-
return await self._request("POST", "/v2/agents/analytics/query/", body=body)
|
|
833
|
+
raise ValueError(
|
|
834
|
+
"Raw analytics SQL is no longer available through the public API. "
|
|
835
|
+
"Use applied analytics or applied analytics-report, which call /v2/analytics/."
|
|
836
|
+
)
|
|
645
837
|
|
|
646
838
|
async def analytics_metrics(
|
|
647
839
|
self,
|
|
@@ -16,6 +16,7 @@ _CANONICAL_EXECUTOR_TYPES: tuple[str, ...] = (
|
|
|
16
16
|
"code",
|
|
17
17
|
"http_request",
|
|
18
18
|
"loop",
|
|
19
|
+
"agent_loop",
|
|
19
20
|
"memory",
|
|
20
21
|
"search",
|
|
21
22
|
"recommend",
|
|
@@ -38,6 +39,7 @@ _EXECUTOR_UI_LABELS: dict[str, str] = {
|
|
|
38
39
|
"code": "Code",
|
|
39
40
|
"http_request": "HTTP Request",
|
|
40
41
|
"loop": "Loop",
|
|
42
|
+
"agent_loop": "Agent Loop",
|
|
41
43
|
"memory": "Memory",
|
|
42
44
|
"search": "Search",
|
|
43
45
|
"recommend": "Recommend",
|
|
@@ -62,6 +64,7 @@ _EXECUTOR_ALIASES: dict[str, tuple[str, ...]] = {
|
|
|
62
64
|
"structured completion",
|
|
63
65
|
),
|
|
64
66
|
"flow": ("run flow", "run_flow", "subflow"),
|
|
67
|
+
"agent_loop": ("agent loop", "planner loop", "tool belt", "agentic loop"),
|
|
65
68
|
"end_flow": (
|
|
66
69
|
"end flow return",
|
|
67
70
|
"end flow and return",
|
|
@@ -660,21 +660,24 @@ async def analytics_query(
|
|
|
660
660
|
limit: int | None = None,
|
|
661
661
|
output_format: str = "csv",
|
|
662
662
|
) -> str:
|
|
663
|
-
"""Run a server-side
|
|
663
|
+
"""Run a server-side GROUP BY count query on conversation data.
|
|
664
664
|
|
|
665
|
-
Dimensions: label_id, label_name, sublabel_id, sublabel_name,
|
|
666
|
-
agent_id,
|
|
667
|
-
Metrics: count
|
|
665
|
+
Dimensions: topic, intent, label_id, label_name, sublabel_id, sublabel_name,
|
|
666
|
+
agent_id, resolution, sentiment, type, user_id
|
|
667
|
+
Metrics: count
|
|
668
668
|
Filters: agent_id, label_id, sublabel_id, type, resolution, sentiment, score, user_id
|
|
669
669
|
"""
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
670
|
+
try:
|
|
671
|
+
data = await client.analytics_query(
|
|
672
|
+
group_by=group_by,
|
|
673
|
+
metrics=metrics,
|
|
674
|
+
filters=filters,
|
|
675
|
+
start=start,
|
|
676
|
+
end=end,
|
|
677
|
+
limit=limit,
|
|
678
|
+
)
|
|
679
|
+
except ValueError as exc:
|
|
680
|
+
return _format_argument_error(str(exc))
|
|
678
681
|
rows = data.get("rows", [])
|
|
679
682
|
if not rows:
|
|
680
683
|
return "No data found for the given query."
|
|
@@ -695,7 +698,12 @@ async def analytics_sql(
|
|
|
695
698
|
output_format: str = "csv",
|
|
696
699
|
) -> str:
|
|
697
700
|
"""Run a raw scoped analytics SQL query."""
|
|
698
|
-
|
|
701
|
+
try:
|
|
702
|
+
data = await client.analytics_sql(
|
|
703
|
+
sql=sql, parameters=parameters, max_rows=limit
|
|
704
|
+
)
|
|
705
|
+
except ValueError as exc:
|
|
706
|
+
return _format_argument_error(str(exc))
|
|
699
707
|
rows = data.get("rows", [])
|
|
700
708
|
if not rows:
|
|
701
709
|
return "No data found for the given query."
|
|
@@ -2383,6 +2391,39 @@ async def flow_node_create(
|
|
|
2383
2391
|
'"position": {"x": 0, "y": 600}}\''
|
|
2384
2392
|
)
|
|
2385
2393
|
|
|
2394
|
+
# Validate agent_loop metadata
|
|
2395
|
+
if canonical_executor_type == "agent_loop":
|
|
2396
|
+
tool_belt = metadata.get("tool_belt")
|
|
2397
|
+
if not tool_belt:
|
|
2398
|
+
return (
|
|
2399
|
+
"❌ Error: agent_loop node requires a non-empty 'tool_belt' in metadata.\n\n"
|
|
2400
|
+
"💡 Required fields:\n"
|
|
2401
|
+
' - tool_belt: list of {"action_name", "flow_id", "description"} entries\n'
|
|
2402
|
+
" Each entry maps a planner action name to an operational subflow.\n"
|
|
2403
|
+
" - model: \"fast\" or \"powerful\" (optional, default \"fast\")\n"
|
|
2404
|
+
" - content: string - LLM context block (optional; auto-built from\n"
|
|
2405
|
+
" the conversation trigger when empty and include_trigger_context=true)\n"
|
|
2406
|
+
" - include_trigger_context: bool (optional, default true)\n"
|
|
2407
|
+
" - planner_schema: list of SchemaField dicts for extra planner outputs\n"
|
|
2408
|
+
" - max_steps: int >= 1 (optional, default 10)\n"
|
|
2409
|
+
" - max_parallel_actions: int >= 1 (optional, default 3)\n\n"
|
|
2410
|
+
"Example:\n"
|
|
2411
|
+
'metadata=\'{"model": "fast", "max_steps": 8, "tool_belt": ['
|
|
2412
|
+
'{"action_name": "lookup_order", "flow_id": "<subflow-uuid>", '
|
|
2413
|
+
'"description": "Fetch an order by number"}], '
|
|
2414
|
+
'"position": {"x": 0, "y": 300}}\''
|
|
2415
|
+
)
|
|
2416
|
+
if not isinstance(tool_belt, list) or not all(
|
|
2417
|
+
isinstance(entry, dict)
|
|
2418
|
+
and entry.get("action_name")
|
|
2419
|
+
and entry.get("flow_id")
|
|
2420
|
+
for entry in tool_belt
|
|
2421
|
+
):
|
|
2422
|
+
return (
|
|
2423
|
+
"❌ Error: agent_loop 'tool_belt' must be a list of objects, "
|
|
2424
|
+
"each with 'action_name' and 'flow_id'."
|
|
2425
|
+
)
|
|
2426
|
+
|
|
2386
2427
|
# Validate mutate_ticket metadata
|
|
2387
2428
|
is_mutate_ticket = canonical_executor_type == "mutate_ticket"
|
|
2388
2429
|
ticket_missing_name = not metadata or "name" not in metadata
|
|
@@ -3766,6 +3807,36 @@ async def executor_list(
|
|
|
3766
3807
|
},
|
|
3767
3808
|
"output_fields": ["results", "count"],
|
|
3768
3809
|
},
|
|
3810
|
+
{
|
|
3811
|
+
"name": "agent_loop",
|
|
3812
|
+
"ui_label": "Agent Loop",
|
|
3813
|
+
"aliases": executor_aliases("agent_loop"),
|
|
3814
|
+
"description": "Planner-driven loop that dispatches tool-belt subflows until it finishes",
|
|
3815
|
+
"use_case": (
|
|
3816
|
+
"Let an LLM planner iteratively choose which operational "
|
|
3817
|
+
"subflows to call (in parallel where safe) before replying. "
|
|
3818
|
+
"Each tool-belt entry maps an action name to an operational flow; "
|
|
3819
|
+
"the planner emits one or more calls per step and ends with 'finish'."
|
|
3820
|
+
),
|
|
3821
|
+
"metadata_fields": {
|
|
3822
|
+
"model": "string - 'fast' or 'powerful' (default 'fast')",
|
|
3823
|
+
"content": "string - LLM context block; leave empty to auto-build from the conversation trigger",
|
|
3824
|
+
"include_trigger_context": "bool - auto-assemble trigger context when content is empty (default true)",
|
|
3825
|
+
"tool_belt": (
|
|
3826
|
+
"array (required) - list of {action_name, flow_id, description} entries; "
|
|
3827
|
+
"each flow_id must be an operational subflow"
|
|
3828
|
+
),
|
|
3829
|
+
"planner_schema": "array - SchemaField dicts surfaced on the loop's output and per-step payload",
|
|
3830
|
+
"max_steps": "int >= 1 - hard cap on planner iterations (default 10)",
|
|
3831
|
+
"max_parallel_actions": "int >= 1 - max actions the planner may dispatch per step (default 3)",
|
|
3832
|
+
},
|
|
3833
|
+
"output_fields": [
|
|
3834
|
+
"outcome",
|
|
3835
|
+
"steps_taken",
|
|
3836
|
+
"max_steps_reached",
|
|
3837
|
+
"(plus fields defined by planner_schema)",
|
|
3838
|
+
],
|
|
3839
|
+
},
|
|
3769
3840
|
{
|
|
3770
3841
|
"name": "memory",
|
|
3771
3842
|
"ui_label": "Memory",
|
|
@@ -170,52 +170,42 @@ async def test_analytics_report_posts_payload(monkeypatch):
|
|
|
170
170
|
|
|
171
171
|
|
|
172
172
|
@pytest.mark.asyncio
|
|
173
|
-
async def
|
|
173
|
+
async def test_analytics_sql_fails_fast_without_public_endpoint(monkeypatch):
|
|
174
174
|
client = AppliedClient(token="test-token")
|
|
175
|
-
seen = {}
|
|
176
175
|
|
|
177
176
|
async def fake_request(method, path, params=None, body=None, shop_id=None):
|
|
178
|
-
|
|
179
|
-
seen["path"] = path
|
|
180
|
-
seen["body"] = body
|
|
181
|
-
return {"rows": []}
|
|
177
|
+
raise AssertionError("analytics_sql should not call removed endpoints")
|
|
182
178
|
|
|
183
179
|
monkeypatch.setattr(client, "_request", fake_request)
|
|
184
180
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
assert result == {"rows": []}
|
|
192
|
-
assert seen == {
|
|
193
|
-
"method": "POST",
|
|
194
|
-
"path": "/v2/agents/analytics/query/",
|
|
195
|
-
"body": {
|
|
196
|
-
"mode": "sql",
|
|
197
|
-
"sql": "SELECT count() AS total FROM conversation_latest_state",
|
|
198
|
-
"parameters": {"resolution": "escalated"},
|
|
199
|
-
"max_rows": 25,
|
|
200
|
-
},
|
|
201
|
-
}
|
|
181
|
+
with pytest.raises(ValueError, match="/v2/analytics/"):
|
|
182
|
+
await client.analytics_sql(
|
|
183
|
+
sql="SELECT count() AS total FROM conversation_latest_state",
|
|
184
|
+
parameters={"resolution": "escalated"},
|
|
185
|
+
max_rows=25,
|
|
186
|
+
)
|
|
202
187
|
|
|
203
188
|
|
|
204
189
|
@pytest.mark.asyncio
|
|
205
|
-
async def
|
|
190
|
+
async def test_analytics_query_uses_dashboard_rate_metrics(monkeypatch):
|
|
206
191
|
client = AppliedClient(token="test-token")
|
|
207
192
|
seen = {}
|
|
208
193
|
|
|
209
|
-
async def
|
|
210
|
-
seen["
|
|
211
|
-
seen["parameters"] = parameters
|
|
212
|
-
seen["max_rows"] = max_rows
|
|
194
|
+
async def fake_analytics_report(payload):
|
|
195
|
+
seen["payload"] = payload
|
|
213
196
|
return {
|
|
214
|
-
"
|
|
215
|
-
|
|
197
|
+
"count": [
|
|
198
|
+
{
|
|
199
|
+
"period": None,
|
|
200
|
+
"group": "escalated",
|
|
201
|
+
"denominator": 0,
|
|
202
|
+
"numerator": 4,
|
|
203
|
+
"rate": 4,
|
|
204
|
+
}
|
|
205
|
+
]
|
|
216
206
|
}
|
|
217
207
|
|
|
218
|
-
monkeypatch.setattr(client, "
|
|
208
|
+
monkeypatch.setattr(client, "analytics_report", fake_analytics_report)
|
|
219
209
|
|
|
220
210
|
result = await client.analytics_query(
|
|
221
211
|
group_by=["resolution"],
|
|
@@ -229,15 +219,53 @@ async def test_analytics_query_builds_legacy_sql(monkeypatch):
|
|
|
229
219
|
assert result["rows"] == [{"resolution": "escalated", "count": 4}]
|
|
230
220
|
assert result["dimensions"] == ["resolution"]
|
|
231
221
|
assert result["metrics"] == ["count"]
|
|
232
|
-
assert
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
222
|
+
assert seen["payload"] == {
|
|
223
|
+
"view": "dashboard_rate_metrics",
|
|
224
|
+
"model": "conversation",
|
|
225
|
+
"group_by": "resolution",
|
|
226
|
+
"custom_rate_metrics_requests": [{"rate": "count", "numerator": "total"}],
|
|
227
|
+
"start_date": "2026-03-01",
|
|
228
|
+
"end_date": "2026-03-31",
|
|
229
|
+
"conversation_filters": {"agent_id": "agent-1", "score": 2},
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@pytest.mark.asyncio
|
|
234
|
+
async def test_analytics_query_uses_automation_aggregates_for_intent_names(monkeypatch):
|
|
235
|
+
client = AppliedClient(token="test-token")
|
|
236
|
+
seen = {}
|
|
237
|
+
|
|
238
|
+
async def fake_analytics_report(payload):
|
|
239
|
+
seen["payload"] = payload
|
|
240
|
+
return {
|
|
241
|
+
"by_intent": [
|
|
242
|
+
{"sublabel_name": "Returns", "total": 2},
|
|
243
|
+
{"sublabel_name": "Returns", "total": 3},
|
|
244
|
+
{"sublabel_name": "Shipping", "total": 1},
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
monkeypatch.setattr(client, "analytics_report", fake_analytics_report)
|
|
249
|
+
|
|
250
|
+
result = await client.analytics_query(
|
|
251
|
+
group_by=["sublabel_name"],
|
|
252
|
+
metrics=["count"],
|
|
253
|
+
start="2026-04-27",
|
|
254
|
+
end="2026-05-01",
|
|
255
|
+
limit=10,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
assert result["columns"] == ["sublabel_name", "count"]
|
|
259
|
+
assert result["rows"] == [
|
|
260
|
+
{"sublabel_name": "Returns", "count": 5},
|
|
261
|
+
{"sublabel_name": "Shipping", "count": 1},
|
|
262
|
+
]
|
|
263
|
+
assert seen["payload"] == {
|
|
264
|
+
"view": "automation_aggregates",
|
|
265
|
+
"model": "conversation",
|
|
266
|
+
"start_date": "2026-04-27",
|
|
267
|
+
"end_date": "2026-05-01",
|
|
268
|
+
}
|
|
241
269
|
|
|
242
270
|
|
|
243
271
|
@pytest.mark.asyncio
|
|
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
|