applied-cli 0.1.0__py3-none-any.whl
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/__init__.py +2 -0
- applied_cli/auth_store.py +263 -0
- applied_cli/commands/__init__.py +2 -0
- applied_cli/commands/_hints.py +11 -0
- applied_cli/commands/_normalize.py +79 -0
- applied_cli/commands/_parsers.py +58 -0
- applied_cli/commands/_ui.py +33 -0
- applied_cli/commands/agent.py +1231 -0
- applied_cli/commands/auth.py +739 -0
- applied_cli/commands/chat.py +379 -0
- applied_cli/commands/coverage.py +348 -0
- applied_cli/commands/discover.py +1006 -0
- applied_cli/commands/fix.py +1204 -0
- applied_cli/commands/insights.py +614 -0
- applied_cli/commands/intents.py +447 -0
- applied_cli/commands/rate.py +508 -0
- applied_cli/commands/responses.py +604 -0
- applied_cli/commands/shop.py +1757 -0
- applied_cli/commands/simulate.py +330 -0
- applied_cli/commands/spec.py +238 -0
- applied_cli/config.py +50 -0
- applied_cli/error_reporting.py +38 -0
- applied_cli/http.py +1614 -0
- applied_cli/main.py +90 -0
- applied_cli/mcp_server.py +738 -0
- applied_cli/presets/demo.yaml +170 -0
- applied_cli/runtime.py +53 -0
- applied_cli/shop_spec.py +398 -0
- applied_cli/spec_workflow.py +432 -0
- applied_cli-0.1.0.dist-info/METADATA +176 -0
- applied_cli-0.1.0.dist-info/RECORD +34 -0
- applied_cli-0.1.0.dist-info/WHEEL +5 -0
- applied_cli-0.1.0.dist-info/entry_points.txt +3 -0
- applied_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from applied_cli.error_reporting import render_api_error
|
|
8
|
+
from applied_cli.http import APIError, list_conversation_scenarios, list_scenario_runs
|
|
9
|
+
from applied_cli.runtime import resolve_runtime
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Summarize test coverage by intent/topic and flow paths.")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _validate_uuid(value: str, *, field_name: str) -> None:
|
|
15
|
+
try:
|
|
16
|
+
uuid.UUID(value)
|
|
17
|
+
except ValueError as exc:
|
|
18
|
+
raise typer.BadParameter(f"{field_name} must be a valid UUID.") from exc
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _extract_name(value: Any) -> Optional[str]:
|
|
22
|
+
if isinstance(value, dict):
|
|
23
|
+
name = value.get("name")
|
|
24
|
+
if isinstance(name, str) and name.strip():
|
|
25
|
+
return name.strip()
|
|
26
|
+
return None
|
|
27
|
+
if isinstance(value, str) and value.strip():
|
|
28
|
+
return value.strip()
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _extract_conversation(run: dict[str, Any], scenario: dict[str, Any]) -> dict[str, Any]:
|
|
33
|
+
for key in ("output_conversation", "input_conversation"):
|
|
34
|
+
candidate = run.get(key)
|
|
35
|
+
if isinstance(candidate, dict):
|
|
36
|
+
return candidate
|
|
37
|
+
scenario_input = scenario.get("input_conversation")
|
|
38
|
+
if isinstance(scenario_input, dict):
|
|
39
|
+
return scenario_input
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _extract_flow_ids_and_paths(conversation: dict[str, Any]) -> tuple[set[str], set[str]]:
|
|
44
|
+
flow_ids: set[str] = set()
|
|
45
|
+
flow_paths: set[str] = set()
|
|
46
|
+
|
|
47
|
+
metadata = conversation.get("metadata")
|
|
48
|
+
if isinstance(metadata, dict):
|
|
49
|
+
state = metadata.get("state")
|
|
50
|
+
if isinstance(state, dict):
|
|
51
|
+
state_flow_id = state.get("flow_id")
|
|
52
|
+
if isinstance(state_flow_id, str) and state_flow_id.strip():
|
|
53
|
+
flow_ids.add(state_flow_id.strip())
|
|
54
|
+
|
|
55
|
+
current_node = state.get("current_node")
|
|
56
|
+
if isinstance(current_node, dict):
|
|
57
|
+
node_name = current_node.get("name")
|
|
58
|
+
if isinstance(node_name, str) and node_name.strip():
|
|
59
|
+
if isinstance(state_flow_id, str) and state_flow_id.strip():
|
|
60
|
+
flow_paths.add(f"{state_flow_id.strip()}:{node_name.strip()}")
|
|
61
|
+
else:
|
|
62
|
+
flow_paths.add(node_name.strip())
|
|
63
|
+
|
|
64
|
+
messages = conversation.get("messages")
|
|
65
|
+
if not isinstance(messages, list):
|
|
66
|
+
return flow_ids, flow_paths
|
|
67
|
+
|
|
68
|
+
seq: list[str] = []
|
|
69
|
+
for item in messages:
|
|
70
|
+
if not isinstance(item, dict):
|
|
71
|
+
continue
|
|
72
|
+
flow_id_raw = item.get("flow_id")
|
|
73
|
+
node_raw = item.get("conversational_node_id")
|
|
74
|
+
|
|
75
|
+
flow_id = flow_id_raw.strip() if isinstance(flow_id_raw, str) else ""
|
|
76
|
+
node = node_raw.strip() if isinstance(node_raw, str) else ""
|
|
77
|
+
|
|
78
|
+
if flow_id:
|
|
79
|
+
flow_ids.add(flow_id)
|
|
80
|
+
if not node:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
token = f"{flow_id}:{node}" if flow_id else node
|
|
84
|
+
if not seq or seq[-1] != token:
|
|
85
|
+
seq.append(token)
|
|
86
|
+
|
|
87
|
+
if seq:
|
|
88
|
+
flow_paths.add(" -> ".join(seq))
|
|
89
|
+
|
|
90
|
+
return flow_ids, flow_paths
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _build_coverage(
|
|
94
|
+
*,
|
|
95
|
+
scenarios: list[dict[str, Any]],
|
|
96
|
+
scenario_runs: dict[str, list[dict[str, Any]]],
|
|
97
|
+
) -> dict[str, Any]:
|
|
98
|
+
intent_buckets: dict[str, dict[str, Any]] = {}
|
|
99
|
+
flow_buckets: dict[str, dict[str, Any]] = {}
|
|
100
|
+
flow_path_counts: dict[str, int] = {}
|
|
101
|
+
|
|
102
|
+
total_runs = 0
|
|
103
|
+
for scenario in scenarios:
|
|
104
|
+
scenario_id = str(scenario.get("id") or "")
|
|
105
|
+
runs = scenario_runs.get(scenario_id, [])
|
|
106
|
+
for run in runs:
|
|
107
|
+
total_runs += 1
|
|
108
|
+
conversation = _extract_conversation(run, scenario)
|
|
109
|
+
topic = _extract_name(conversation.get("label")) or "(none)"
|
|
110
|
+
intent = _extract_name(conversation.get("sublabel")) or "(none)"
|
|
111
|
+
pass_status = str(run.get("pass_status") or "").lower()
|
|
112
|
+
conv_id = str(conversation.get("id") or "")
|
|
113
|
+
|
|
114
|
+
intent_key = f"{topic}|||{intent}"
|
|
115
|
+
bucket = intent_buckets.setdefault(
|
|
116
|
+
intent_key,
|
|
117
|
+
{
|
|
118
|
+
"topic": topic,
|
|
119
|
+
"intent": intent,
|
|
120
|
+
"runs": 0,
|
|
121
|
+
"pass": 0,
|
|
122
|
+
"fail": 0,
|
|
123
|
+
"other": 0,
|
|
124
|
+
"scenario_ids": set(),
|
|
125
|
+
"conversation_ids": set(),
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
bucket["runs"] += 1
|
|
129
|
+
bucket["scenario_ids"].add(scenario_id)
|
|
130
|
+
if conv_id:
|
|
131
|
+
bucket["conversation_ids"].add(conv_id)
|
|
132
|
+
if pass_status == "pass":
|
|
133
|
+
bucket["pass"] += 1
|
|
134
|
+
elif pass_status == "fail":
|
|
135
|
+
bucket["fail"] += 1
|
|
136
|
+
else:
|
|
137
|
+
bucket["other"] += 1
|
|
138
|
+
|
|
139
|
+
flow_ids, flow_paths = _extract_flow_ids_and_paths(conversation)
|
|
140
|
+
if not flow_ids and not flow_paths:
|
|
141
|
+
flow_ids = {"(none)"}
|
|
142
|
+
for flow_id in flow_ids:
|
|
143
|
+
flow_bucket = flow_buckets.setdefault(
|
|
144
|
+
flow_id,
|
|
145
|
+
{
|
|
146
|
+
"flow_id": flow_id,
|
|
147
|
+
"runs": 0,
|
|
148
|
+
"pass": 0,
|
|
149
|
+
"fail": 0,
|
|
150
|
+
"other": 0,
|
|
151
|
+
"scenario_ids": set(),
|
|
152
|
+
"conversation_ids": set(),
|
|
153
|
+
},
|
|
154
|
+
)
|
|
155
|
+
flow_bucket["runs"] += 1
|
|
156
|
+
flow_bucket["scenario_ids"].add(scenario_id)
|
|
157
|
+
if conv_id:
|
|
158
|
+
flow_bucket["conversation_ids"].add(conv_id)
|
|
159
|
+
if pass_status == "pass":
|
|
160
|
+
flow_bucket["pass"] += 1
|
|
161
|
+
elif pass_status == "fail":
|
|
162
|
+
flow_bucket["fail"] += 1
|
|
163
|
+
else:
|
|
164
|
+
flow_bucket["other"] += 1
|
|
165
|
+
|
|
166
|
+
for path in flow_paths:
|
|
167
|
+
flow_path_counts[path] = flow_path_counts.get(path, 0) + 1
|
|
168
|
+
|
|
169
|
+
intents = []
|
|
170
|
+
for row in intent_buckets.values():
|
|
171
|
+
intents.append(
|
|
172
|
+
{
|
|
173
|
+
"topic": row["topic"],
|
|
174
|
+
"intent": row["intent"],
|
|
175
|
+
"runs": row["runs"],
|
|
176
|
+
"pass": row["pass"],
|
|
177
|
+
"fail": row["fail"],
|
|
178
|
+
"other": row["other"],
|
|
179
|
+
"scenario_count": len(row["scenario_ids"]),
|
|
180
|
+
"conversation_count": len(row["conversation_ids"]),
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
flows = []
|
|
185
|
+
for row in flow_buckets.values():
|
|
186
|
+
flows.append(
|
|
187
|
+
{
|
|
188
|
+
"flow_id": row["flow_id"],
|
|
189
|
+
"runs": row["runs"],
|
|
190
|
+
"pass": row["pass"],
|
|
191
|
+
"fail": row["fail"],
|
|
192
|
+
"other": row["other"],
|
|
193
|
+
"scenario_count": len(row["scenario_ids"]),
|
|
194
|
+
"conversation_count": len(row["conversation_ids"]),
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
flow_paths = [{"path": key, "runs": value} for key, value in flow_path_counts.items()]
|
|
199
|
+
flow_paths.sort(key=lambda x: (-x["runs"], x["path"]))
|
|
200
|
+
|
|
201
|
+
intents.sort(key=lambda x: (-x["runs"], x["topic"], x["intent"]))
|
|
202
|
+
flows.sort(key=lambda x: (-x["runs"], x["flow_id"]))
|
|
203
|
+
|
|
204
|
+
scenarios_with_runs = sum(1 for value in scenario_runs.values() if value)
|
|
205
|
+
return {
|
|
206
|
+
"summary": {
|
|
207
|
+
"scenario_count": len(scenarios),
|
|
208
|
+
"scenarios_with_runs": scenarios_with_runs,
|
|
209
|
+
"scenarios_without_runs": max(0, len(scenarios) - scenarios_with_runs),
|
|
210
|
+
"run_count": total_runs,
|
|
211
|
+
"intent_bucket_count": len(intents),
|
|
212
|
+
"flow_bucket_count": len(flows),
|
|
213
|
+
"flow_path_count": len(flow_paths),
|
|
214
|
+
},
|
|
215
|
+
"intents": intents,
|
|
216
|
+
"flows": flows,
|
|
217
|
+
"flow_paths": flow_paths,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@app.command(
|
|
222
|
+
"summary",
|
|
223
|
+
help=(
|
|
224
|
+
"Summarize coverage by topic/intent and flow paths. Example: applied-cli test coverage "
|
|
225
|
+
"summary --agent-id <uuid> --benchmark-id <uuid> --all"
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
def summary_cmd(
|
|
229
|
+
agent_id: str = typer.Option(..., "--agent-id", "--agent", help="Target agent UUID."),
|
|
230
|
+
benchmark_id: Optional[str] = typer.Option(
|
|
231
|
+
None, "--benchmark-id", "--benchmark", help="Optional benchmark UUID."
|
|
232
|
+
),
|
|
233
|
+
latest: bool = typer.Option(
|
|
234
|
+
True, "--latest/--all", help="Use latest run per scenario or include all runs."
|
|
235
|
+
),
|
|
236
|
+
include_none_flow: bool = typer.Option(
|
|
237
|
+
True,
|
|
238
|
+
"--include-none-flow/--no-include-none-flow",
|
|
239
|
+
help="Include '(none)' flow bucket for runs without flow metadata.",
|
|
240
|
+
),
|
|
241
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
242
|
+
base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
|
|
243
|
+
shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
|
|
244
|
+
api_token: Optional[str] = typer.Option(None, help="Applied API token."),
|
|
245
|
+
) -> None:
|
|
246
|
+
_validate_uuid(agent_id, field_name="agent-id")
|
|
247
|
+
if benchmark_id:
|
|
248
|
+
_validate_uuid(benchmark_id, field_name="benchmark-id")
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
|
|
252
|
+
base_url=base_url,
|
|
253
|
+
shop_id=shop_id,
|
|
254
|
+
api_token=api_token,
|
|
255
|
+
)
|
|
256
|
+
scenarios = list_conversation_scenarios(
|
|
257
|
+
base_url=resolved_base_url,
|
|
258
|
+
shop_id=resolved_shop_id,
|
|
259
|
+
api_token=resolved_token,
|
|
260
|
+
agent_id=agent_id,
|
|
261
|
+
benchmark_id=benchmark_id,
|
|
262
|
+
limit=500,
|
|
263
|
+
ordering="-created_at",
|
|
264
|
+
)
|
|
265
|
+
except APIError as exc:
|
|
266
|
+
typer.echo(render_api_error(exc, action="list scenarios for coverage summary"), err=True)
|
|
267
|
+
raise typer.Exit(code=1) from exc
|
|
268
|
+
|
|
269
|
+
scenario_runs: dict[str, list[dict[str, Any]]] = {}
|
|
270
|
+
for scenario in scenarios:
|
|
271
|
+
scenario_id = str(scenario.get("id") or "")
|
|
272
|
+
if not scenario_id:
|
|
273
|
+
continue
|
|
274
|
+
try:
|
|
275
|
+
runs = list_scenario_runs(
|
|
276
|
+
base_url=resolved_base_url,
|
|
277
|
+
shop_id=resolved_shop_id,
|
|
278
|
+
api_token=resolved_token,
|
|
279
|
+
scenario_id=scenario_id,
|
|
280
|
+
latest_only=latest,
|
|
281
|
+
)
|
|
282
|
+
except APIError as exc:
|
|
283
|
+
typer.echo(
|
|
284
|
+
render_api_error(
|
|
285
|
+
exc, action=f"list runs for coverage scenario {scenario_id}"
|
|
286
|
+
),
|
|
287
|
+
err=True,
|
|
288
|
+
)
|
|
289
|
+
raise typer.Exit(code=1) from exc
|
|
290
|
+
scenario_runs[scenario_id] = runs
|
|
291
|
+
|
|
292
|
+
coverage = _build_coverage(scenarios=scenarios, scenario_runs=scenario_runs)
|
|
293
|
+
if not include_none_flow:
|
|
294
|
+
coverage["flows"] = [
|
|
295
|
+
row for row in coverage["flows"] if row.get("flow_id") != "(none)"
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
result = {
|
|
299
|
+
"agent_id": agent_id,
|
|
300
|
+
"benchmark_id": benchmark_id,
|
|
301
|
+
"latest_only": latest,
|
|
302
|
+
**coverage,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if output_json:
|
|
306
|
+
typer.echo(json.dumps(result, indent=2, default=str))
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
summary = result["summary"]
|
|
310
|
+
typer.echo(
|
|
311
|
+
"summary="
|
|
312
|
+
+ ", ".join(
|
|
313
|
+
[
|
|
314
|
+
f"scenarios={summary['scenario_count']}",
|
|
315
|
+
f"scenarios_with_runs={summary['scenarios_with_runs']}",
|
|
316
|
+
f"scenarios_without_runs={summary['scenarios_without_runs']}",
|
|
317
|
+
f"runs={summary['run_count']}",
|
|
318
|
+
f"intent_buckets={summary['intent_bucket_count']}",
|
|
319
|
+
f"flow_buckets={summary['flow_bucket_count']}",
|
|
320
|
+
f"flow_paths={summary['flow_path_count']}",
|
|
321
|
+
]
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
typer.echo("\nIntent Coverage:")
|
|
326
|
+
if not result["intents"]:
|
|
327
|
+
typer.echo("- no runs")
|
|
328
|
+
else:
|
|
329
|
+
for row in result["intents"]:
|
|
330
|
+
typer.echo(
|
|
331
|
+
f"- topic={row['topic']} | intent={row['intent']} | runs={row['runs']} | pass={row['pass']} | fail={row['fail']} | other={row['other']}"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
typer.echo("\nFlow Coverage:")
|
|
335
|
+
if not result["flows"]:
|
|
336
|
+
typer.echo("- no runs")
|
|
337
|
+
else:
|
|
338
|
+
for row in result["flows"]:
|
|
339
|
+
typer.echo(
|
|
340
|
+
f"- flow_id={row['flow_id']} | runs={row['runs']} | pass={row['pass']} | fail={row['fail']} | other={row['other']}"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
typer.echo("\nFlow Paths:")
|
|
344
|
+
if not result["flow_paths"]:
|
|
345
|
+
typer.echo("- no flow paths observed")
|
|
346
|
+
else:
|
|
347
|
+
for row in result["flow_paths"]:
|
|
348
|
+
typer.echo(f"- runs={row['runs']} | path={row['path']}")
|