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,330 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from applied_cli.commands._parsers import parse_json_dict, validate_uuid
|
|
7
|
+
from applied_cli.error_reporting import render_api_error
|
|
8
|
+
from applied_cli.http import APIError, simulate_agent_conversation
|
|
9
|
+
from applied_cli.runtime import resolve_runtime
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(
|
|
12
|
+
help="Generate simulated conversations (including backdated timestamps)."
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command(
|
|
17
|
+
"run",
|
|
18
|
+
help=(
|
|
19
|
+
"Simulate conversations for an agent with sensible defaults. Example: "
|
|
20
|
+
"applied-cli simulate run --agent-id <uuid> --count 10 "
|
|
21
|
+
"--created-at-from 2026-01-26T00:00:00Z --created-at-to 2026-02-26T23:59:59Z "
|
|
22
|
+
"--is-test false --conversation-type web_chat"
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
@app.command(
|
|
26
|
+
"conversations",
|
|
27
|
+
hidden=True,
|
|
28
|
+
help="Alias for `simulate run`.",
|
|
29
|
+
)
|
|
30
|
+
def conversations_cmd(
|
|
31
|
+
agent_id: str = typer.Option(
|
|
32
|
+
...,
|
|
33
|
+
"--agent-id",
|
|
34
|
+
"--agent",
|
|
35
|
+
"--id",
|
|
36
|
+
help="Target agent UUID.",
|
|
37
|
+
),
|
|
38
|
+
count: int = typer.Option(
|
|
39
|
+
1,
|
|
40
|
+
"--count",
|
|
41
|
+
help="Number of conversations to simulate.",
|
|
42
|
+
),
|
|
43
|
+
max_turns: int = typer.Option(
|
|
44
|
+
6,
|
|
45
|
+
"--max-turns",
|
|
46
|
+
help="Maximum turns per simulated conversation (1-10).",
|
|
47
|
+
),
|
|
48
|
+
agent_subtype: Optional[str] = typer.Option(
|
|
49
|
+
None,
|
|
50
|
+
"--agent-subtype",
|
|
51
|
+
help="Optional agent subtype forwarded to simulate endpoint.",
|
|
52
|
+
),
|
|
53
|
+
conversation_type: Optional[str] = typer.Option(
|
|
54
|
+
None,
|
|
55
|
+
"--conversation-type",
|
|
56
|
+
help="Optional conversation type (e.g. web_chat, email, sms).",
|
|
57
|
+
),
|
|
58
|
+
direction: Optional[str] = typer.Option(
|
|
59
|
+
None,
|
|
60
|
+
"--direction",
|
|
61
|
+
help="Optional direction (e.g. inbound, outbound).",
|
|
62
|
+
),
|
|
63
|
+
is_test: bool = typer.Option(
|
|
64
|
+
True,
|
|
65
|
+
"--is-test/--no-is-test",
|
|
66
|
+
help="Mark simulated conversations as test data.",
|
|
67
|
+
),
|
|
68
|
+
is_historical: bool = typer.Option(
|
|
69
|
+
True,
|
|
70
|
+
"--is-historical/--no-is-historical",
|
|
71
|
+
help="Mark simulated conversations as historical.",
|
|
72
|
+
),
|
|
73
|
+
remote_platform: Optional[str] = typer.Option(
|
|
74
|
+
None,
|
|
75
|
+
"--remote-platform",
|
|
76
|
+
help="Optional remote platform tag (for example: instagram).",
|
|
77
|
+
),
|
|
78
|
+
created_at: Optional[str] = typer.Option(
|
|
79
|
+
None,
|
|
80
|
+
"--created-at",
|
|
81
|
+
help="Explicit timestamp (ISO-8601) for each simulation run.",
|
|
82
|
+
),
|
|
83
|
+
created_at_from: Optional[str] = typer.Option(
|
|
84
|
+
None,
|
|
85
|
+
"--created-at-from",
|
|
86
|
+
help="Start timestamp (ISO-8601) for random timestamp simulation.",
|
|
87
|
+
),
|
|
88
|
+
created_at_to: Optional[str] = typer.Option(
|
|
89
|
+
None,
|
|
90
|
+
"--created-at-to",
|
|
91
|
+
help="End timestamp (ISO-8601) for random timestamp simulation.",
|
|
92
|
+
),
|
|
93
|
+
created_at_distribution: str = typer.Option(
|
|
94
|
+
"daily_peaks",
|
|
95
|
+
"--created-at-distribution",
|
|
96
|
+
help="Timestamp distribution: flat, daily_peaks, weekly_peaks.",
|
|
97
|
+
),
|
|
98
|
+
label_id: Optional[str] = typer.Option(
|
|
99
|
+
None,
|
|
100
|
+
"--label-id",
|
|
101
|
+
help="Optional topic UUID.",
|
|
102
|
+
),
|
|
103
|
+
sublabel_id: Optional[str] = typer.Option(
|
|
104
|
+
None,
|
|
105
|
+
"--sublabel-id",
|
|
106
|
+
help="Optional intent UUID.",
|
|
107
|
+
),
|
|
108
|
+
metadata_json: Optional[str] = typer.Option(
|
|
109
|
+
None,
|
|
110
|
+
"--metadata-json",
|
|
111
|
+
help='Optional conversation metadata JSON object. Example: \'{"source":"cli"}\'',
|
|
112
|
+
),
|
|
113
|
+
simulate_experiments: bool = typer.Option(
|
|
114
|
+
False,
|
|
115
|
+
"--simulate-experiments/--no-simulate-experiments",
|
|
116
|
+
help="Enable experiment traffic simulation fields.",
|
|
117
|
+
),
|
|
118
|
+
experiment_id: Optional[str] = typer.Option(
|
|
119
|
+
None,
|
|
120
|
+
"--experiment-id",
|
|
121
|
+
help="Optional experiment UUID when simulating experiments.",
|
|
122
|
+
),
|
|
123
|
+
traffic_rule_id: Optional[str] = typer.Option(
|
|
124
|
+
None,
|
|
125
|
+
"--traffic-rule-id",
|
|
126
|
+
help="Optional traffic rule UUID when simulating experiments.",
|
|
127
|
+
),
|
|
128
|
+
populate_workforce_actions: bool = typer.Option(
|
|
129
|
+
False,
|
|
130
|
+
"--populate-workforce-actions/--no-populate-workforce-actions",
|
|
131
|
+
help="Emit workforce-style actions/metrics in simulation.",
|
|
132
|
+
),
|
|
133
|
+
assigned_user_id: Optional[str] = typer.Option(
|
|
134
|
+
None,
|
|
135
|
+
"--assigned-user-id",
|
|
136
|
+
help="Optional assignee UUID for workforce simulation.",
|
|
137
|
+
),
|
|
138
|
+
user_model: Optional[str] = typer.Option(
|
|
139
|
+
None,
|
|
140
|
+
"--user-model",
|
|
141
|
+
help="Optional simulated user model (for example: openai:gpt-5-nano).",
|
|
142
|
+
),
|
|
143
|
+
user_temperature: Optional[float] = typer.Option(
|
|
144
|
+
None,
|
|
145
|
+
"--user-temperature",
|
|
146
|
+
help="Optional simulated user temperature (0.0-2.0).",
|
|
147
|
+
),
|
|
148
|
+
seed: Optional[int] = typer.Option(
|
|
149
|
+
None,
|
|
150
|
+
"--seed",
|
|
151
|
+
help="Optional deterministic seed for user LLM timestamp sampling.",
|
|
152
|
+
),
|
|
153
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
154
|
+
continue_on_error: bool = typer.Option(
|
|
155
|
+
False,
|
|
156
|
+
"--continue-on-error/--no-continue-on-error",
|
|
157
|
+
help="Continue remaining simulations even if one fails.",
|
|
158
|
+
),
|
|
159
|
+
base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
|
|
160
|
+
shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
|
|
161
|
+
api_token: Optional[str] = typer.Option(None, help="Applied API token."),
|
|
162
|
+
) -> None:
|
|
163
|
+
validate_uuid(agent_id, field_name="agent-id")
|
|
164
|
+
if experiment_id:
|
|
165
|
+
validate_uuid(experiment_id, field_name="experiment-id")
|
|
166
|
+
if traffic_rule_id:
|
|
167
|
+
validate_uuid(traffic_rule_id, field_name="traffic-rule-id")
|
|
168
|
+
if assigned_user_id:
|
|
169
|
+
validate_uuid(assigned_user_id, field_name="assigned-user-id")
|
|
170
|
+
if label_id:
|
|
171
|
+
validate_uuid(label_id, field_name="label-id")
|
|
172
|
+
if sublabel_id:
|
|
173
|
+
validate_uuid(sublabel_id, field_name="sublabel-id")
|
|
174
|
+
metadata = parse_json_dict(metadata_json, field_name="metadata-json")
|
|
175
|
+
if count < 1:
|
|
176
|
+
raise typer.BadParameter("count must be >= 1.")
|
|
177
|
+
if not (1 <= max_turns <= 10):
|
|
178
|
+
raise typer.BadParameter("max-turns must be between 1 and 10.")
|
|
179
|
+
if user_temperature is not None and not (0.0 <= user_temperature <= 2.0):
|
|
180
|
+
raise typer.BadParameter("user-temperature must be between 0.0 and 2.0.")
|
|
181
|
+
if conversation_type and conversation_type not in {"web_chat", "email", "sms", "phone_call"}:
|
|
182
|
+
raise typer.BadParameter(
|
|
183
|
+
"conversation-type must be one of: web_chat, email, sms, phone_call."
|
|
184
|
+
)
|
|
185
|
+
if direction and direction not in {"inbound", "outbound"}:
|
|
186
|
+
raise typer.BadParameter("direction must be one of: inbound, outbound.")
|
|
187
|
+
if bool(created_at_from) != bool(created_at_to):
|
|
188
|
+
raise typer.BadParameter(
|
|
189
|
+
"Provide both --created-at-from and --created-at-to, or neither."
|
|
190
|
+
)
|
|
191
|
+
if created_at and (created_at_from or created_at_to):
|
|
192
|
+
raise typer.BadParameter(
|
|
193
|
+
"Use either --created-at OR --created-at-from/--created-at-to."
|
|
194
|
+
)
|
|
195
|
+
if created_at_distribution not in {"flat", "daily_peaks", "weekly_peaks"}:
|
|
196
|
+
raise typer.BadParameter(
|
|
197
|
+
"created-at-distribution must be one of: flat, daily_peaks, weekly_peaks."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
|
|
202
|
+
base_url=base_url,
|
|
203
|
+
shop_id=shop_id,
|
|
204
|
+
api_token=api_token,
|
|
205
|
+
)
|
|
206
|
+
except APIError as exc:
|
|
207
|
+
typer.echo(render_api_error(exc, action="resolve runtime for simulation"), err=True)
|
|
208
|
+
raise typer.Exit(code=1) from exc
|
|
209
|
+
|
|
210
|
+
successes: list[dict[str, Any]] = []
|
|
211
|
+
failures: list[dict[str, Any]] = []
|
|
212
|
+
for idx in range(count):
|
|
213
|
+
payload: dict[str, Any] = {
|
|
214
|
+
"max_turns": max_turns,
|
|
215
|
+
"is_test": is_test,
|
|
216
|
+
"is_historical": is_historical,
|
|
217
|
+
"created_at_distribution": created_at_distribution,
|
|
218
|
+
"simulate_experiments": simulate_experiments,
|
|
219
|
+
"populate_workforce_actions": populate_workforce_actions,
|
|
220
|
+
}
|
|
221
|
+
if agent_subtype:
|
|
222
|
+
payload["agent_subtype"] = agent_subtype
|
|
223
|
+
if conversation_type:
|
|
224
|
+
payload["conversation_type"] = conversation_type
|
|
225
|
+
if direction:
|
|
226
|
+
payload["direction"] = direction
|
|
227
|
+
if remote_platform:
|
|
228
|
+
payload["remote_platform"] = remote_platform
|
|
229
|
+
if created_at:
|
|
230
|
+
payload["created_at"] = created_at
|
|
231
|
+
if created_at_from and created_at_to:
|
|
232
|
+
payload["created_at_from"] = created_at_from
|
|
233
|
+
payload["created_at_to"] = created_at_to
|
|
234
|
+
if label_id:
|
|
235
|
+
payload["label_id"] = label_id
|
|
236
|
+
if sublabel_id:
|
|
237
|
+
payload["sublabel_id"] = sublabel_id
|
|
238
|
+
if metadata is not None:
|
|
239
|
+
payload["metadata"] = metadata
|
|
240
|
+
if experiment_id:
|
|
241
|
+
payload["experiment_id"] = experiment_id
|
|
242
|
+
if traffic_rule_id:
|
|
243
|
+
payload["traffic_rule_id"] = traffic_rule_id
|
|
244
|
+
if assigned_user_id:
|
|
245
|
+
payload["assigned_user_id"] = assigned_user_id
|
|
246
|
+
if seed is not None or user_model is not None or user_temperature is not None:
|
|
247
|
+
payload["user_llm"] = {}
|
|
248
|
+
if seed is not None:
|
|
249
|
+
payload["user_llm"]["seed"] = seed + idx
|
|
250
|
+
if user_model is not None:
|
|
251
|
+
payload["user_llm"]["model"] = user_model
|
|
252
|
+
if user_temperature is not None:
|
|
253
|
+
payload["user_llm"]["temperature"] = user_temperature
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
result = simulate_agent_conversation(
|
|
257
|
+
base_url=resolved_base_url,
|
|
258
|
+
shop_id=resolved_shop_id,
|
|
259
|
+
api_token=resolved_token,
|
|
260
|
+
agent_id=agent_id,
|
|
261
|
+
payload=payload,
|
|
262
|
+
)
|
|
263
|
+
successes.append(result)
|
|
264
|
+
except APIError as exc:
|
|
265
|
+
failure = {
|
|
266
|
+
"attempt": idx + 1,
|
|
267
|
+
"error": render_api_error(exc, action="simulate conversation"),
|
|
268
|
+
}
|
|
269
|
+
failures.append(failure)
|
|
270
|
+
if not continue_on_error:
|
|
271
|
+
if output_json:
|
|
272
|
+
typer.echo(
|
|
273
|
+
json.dumps(
|
|
274
|
+
{
|
|
275
|
+
"result": "failed",
|
|
276
|
+
"success_count": len(successes),
|
|
277
|
+
"failure_count": len(failures),
|
|
278
|
+
"failures": failures,
|
|
279
|
+
},
|
|
280
|
+
indent=2,
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
else:
|
|
284
|
+
typer.echo(failure["error"], err=True)
|
|
285
|
+
raise typer.Exit(code=1) from exc
|
|
286
|
+
|
|
287
|
+
summary = {
|
|
288
|
+
"result": "success" if not failures else "partial_success",
|
|
289
|
+
"agent_id": agent_id,
|
|
290
|
+
"count_requested": count,
|
|
291
|
+
"success_count": len(successes),
|
|
292
|
+
"failure_count": len(failures),
|
|
293
|
+
"is_test": is_test,
|
|
294
|
+
"is_historical": is_historical,
|
|
295
|
+
"simulate_experiments": simulate_experiments,
|
|
296
|
+
"populate_workforce_actions": populate_workforce_actions,
|
|
297
|
+
"created_at": created_at,
|
|
298
|
+
"created_at_from": created_at_from,
|
|
299
|
+
"created_at_to": created_at_to,
|
|
300
|
+
"created_at_distribution": created_at_distribution,
|
|
301
|
+
"simulations": successes if output_json else None,
|
|
302
|
+
"failures": failures if failures else None,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if output_json:
|
|
306
|
+
typer.echo(json.dumps(summary, indent=2, default=str))
|
|
307
|
+
return
|
|
308
|
+
conversation_ids = [
|
|
309
|
+
str(row.get("conversation_id"))
|
|
310
|
+
for row in successes
|
|
311
|
+
if isinstance(row, dict) and row.get("conversation_id")
|
|
312
|
+
]
|
|
313
|
+
preview_ids = ",".join(conversation_ids[:5]) if conversation_ids else ""
|
|
314
|
+
|
|
315
|
+
typer.echo(
|
|
316
|
+
"result="
|
|
317
|
+
+ summary["result"]
|
|
318
|
+
+ " | "
|
|
319
|
+
+ " | ".join(
|
|
320
|
+
[
|
|
321
|
+
f"agent_id={agent_id}",
|
|
322
|
+
f"requested={count}",
|
|
323
|
+
f"success={len(successes)}",
|
|
324
|
+
f"failed={len(failures)}",
|
|
325
|
+
f"is_test={is_test}",
|
|
326
|
+
f"is_historical={is_historical}",
|
|
327
|
+
f"conversation_ids={preview_ids}" if preview_ids else "conversation_ids=(none)",
|
|
328
|
+
]
|
|
329
|
+
)
|
|
330
|
+
)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from applied_cli.commands.chat import (
|
|
9
|
+
_create_conversation,
|
|
10
|
+
_stream_v1_completion,
|
|
11
|
+
_verify_agent_scope,
|
|
12
|
+
)
|
|
13
|
+
from applied_cli.commands.rate import conversation as rate_conversation_command
|
|
14
|
+
from applied_cli.error_reporting import render_api_error
|
|
15
|
+
from applied_cli.http import APIError
|
|
16
|
+
from applied_cli.spec_workflow import (
|
|
17
|
+
apply_agent_settings_from_spec,
|
|
18
|
+
create_agent_from_spec,
|
|
19
|
+
load_and_validate_spec,
|
|
20
|
+
upsert_spec_responses,
|
|
21
|
+
)
|
|
22
|
+
from applied_cli.runtime import resolve_runtime
|
|
23
|
+
|
|
24
|
+
app = typer.Typer(help="Run spec-driven bootstrap/test/rate workflow.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _run_test_case(
|
|
28
|
+
*,
|
|
29
|
+
client: httpx.Client,
|
|
30
|
+
base_url: str,
|
|
31
|
+
shop_id: str,
|
|
32
|
+
api_token: str,
|
|
33
|
+
agent_id: str,
|
|
34
|
+
test_case: dict[str, Any],
|
|
35
|
+
) -> dict[str, Any]:
|
|
36
|
+
channel = str(test_case["channel"])
|
|
37
|
+
conversation_id = _create_conversation(
|
|
38
|
+
client,
|
|
39
|
+
base_url=base_url,
|
|
40
|
+
agent_id=agent_id,
|
|
41
|
+
channel=channel,
|
|
42
|
+
)
|
|
43
|
+
transcript_results: list[dict[str, Any]] = []
|
|
44
|
+
for turn in test_case["turns"]:
|
|
45
|
+
transcript = [
|
|
46
|
+
{
|
|
47
|
+
"id": str(uuid.uuid4()),
|
|
48
|
+
"role": "user",
|
|
49
|
+
"content": turn,
|
|
50
|
+
"text": turn,
|
|
51
|
+
"format": "TEXT",
|
|
52
|
+
"entity": {"type": "user"},
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
payload: dict[str, object] = {
|
|
56
|
+
"conversation_id": conversation_id,
|
|
57
|
+
"context": "EVALUATE",
|
|
58
|
+
"transcript": transcript,
|
|
59
|
+
"metadata": {
|
|
60
|
+
"source": "applied-cli-spec",
|
|
61
|
+
"isTest": True,
|
|
62
|
+
"channel": channel,
|
|
63
|
+
"test_name": test_case["name"],
|
|
64
|
+
},
|
|
65
|
+
"draft": False,
|
|
66
|
+
}
|
|
67
|
+
assistant_text = _stream_v1_completion(
|
|
68
|
+
client,
|
|
69
|
+
base_url=base_url,
|
|
70
|
+
shop_id=shop_id,
|
|
71
|
+
api_token=api_token,
|
|
72
|
+
agent_id=agent_id,
|
|
73
|
+
payload=payload,
|
|
74
|
+
)
|
|
75
|
+
transcript_results.append(
|
|
76
|
+
{"user": turn, "assistant": assistant_text, "assistant_chars": len(assistant_text)}
|
|
77
|
+
)
|
|
78
|
+
return {
|
|
79
|
+
"name": test_case["name"],
|
|
80
|
+
"channel": channel,
|
|
81
|
+
"conversation_id": conversation_id,
|
|
82
|
+
"turns": transcript_results,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command("run")
|
|
87
|
+
def run(
|
|
88
|
+
spec: str = typer.Option(..., "--spec", help="Path to spec JSON."),
|
|
89
|
+
agent_id: Optional[str] = typer.Option(
|
|
90
|
+
None, "--agent-id", help="Reuse existing agent id instead of creating one."
|
|
91
|
+
),
|
|
92
|
+
base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
|
|
93
|
+
shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
|
|
94
|
+
api_token: Optional[str] = typer.Option(None, help="Applied API token."),
|
|
95
|
+
dry_run: bool = typer.Option(False, help="Validate and print plan without writes."),
|
|
96
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
|
|
97
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
98
|
+
) -> None:
|
|
99
|
+
if agent_id:
|
|
100
|
+
try:
|
|
101
|
+
uuid.UUID(agent_id)
|
|
102
|
+
except ValueError as exc:
|
|
103
|
+
raise typer.BadParameter("agent-id must be a valid UUID.") from exc
|
|
104
|
+
try:
|
|
105
|
+
validated_spec = load_and_validate_spec(spec)
|
|
106
|
+
resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
|
|
107
|
+
base_url=base_url,
|
|
108
|
+
shop_id=shop_id,
|
|
109
|
+
api_token=api_token,
|
|
110
|
+
)
|
|
111
|
+
except ValueError as exc:
|
|
112
|
+
typer.echo(f"Spec validation failed: {exc}", err=True)
|
|
113
|
+
raise typer.Exit(code=2) from exc
|
|
114
|
+
except APIError as exc:
|
|
115
|
+
typer.echo(render_api_error(exc, action="resolve runtime for spec run"), err=True)
|
|
116
|
+
raise typer.Exit(code=1) from exc
|
|
117
|
+
|
|
118
|
+
typer.echo("Plan:")
|
|
119
|
+
typer.echo(f"- base_url: {resolved_base_url}")
|
|
120
|
+
typer.echo(f"- shop_id: {resolved_shop_id}")
|
|
121
|
+
typer.echo(f"- tests: {len(validated_spec['tests'])}")
|
|
122
|
+
typer.echo(f"- responses: {len(validated_spec['responses'])}")
|
|
123
|
+
typer.echo(f"- dry_run: {dry_run}")
|
|
124
|
+
if not yes and not typer.confirm("Continue?"):
|
|
125
|
+
raise typer.Exit(code=1)
|
|
126
|
+
|
|
127
|
+
working_agent_id = agent_id
|
|
128
|
+
try:
|
|
129
|
+
if working_agent_id:
|
|
130
|
+
apply_agent_settings_from_spec(
|
|
131
|
+
base_url=resolved_base_url,
|
|
132
|
+
shop_id=resolved_shop_id,
|
|
133
|
+
api_token=resolved_token,
|
|
134
|
+
agent_id=working_agent_id,
|
|
135
|
+
spec=validated_spec,
|
|
136
|
+
dry_run=dry_run,
|
|
137
|
+
)
|
|
138
|
+
upsert_spec_responses(
|
|
139
|
+
base_url=resolved_base_url,
|
|
140
|
+
shop_id=resolved_shop_id,
|
|
141
|
+
api_token=resolved_token,
|
|
142
|
+
agent_id=working_agent_id,
|
|
143
|
+
responses=validated_spec["responses"],
|
|
144
|
+
dry_run=dry_run,
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
created = create_agent_from_spec(
|
|
148
|
+
base_url=resolved_base_url,
|
|
149
|
+
shop_id=resolved_shop_id,
|
|
150
|
+
api_token=resolved_token,
|
|
151
|
+
spec=validated_spec,
|
|
152
|
+
dry_run=dry_run,
|
|
153
|
+
)
|
|
154
|
+
working_agent_id = str(created.get("id"))
|
|
155
|
+
upsert_spec_responses(
|
|
156
|
+
base_url=resolved_base_url,
|
|
157
|
+
shop_id=resolved_shop_id,
|
|
158
|
+
api_token=resolved_token,
|
|
159
|
+
agent_id=working_agent_id,
|
|
160
|
+
responses=validated_spec["responses"],
|
|
161
|
+
dry_run=dry_run,
|
|
162
|
+
)
|
|
163
|
+
except APIError as exc:
|
|
164
|
+
typer.echo(render_api_error(exc, action="bootstrap spec run"), err=True)
|
|
165
|
+
raise typer.Exit(code=1) from exc
|
|
166
|
+
|
|
167
|
+
report: dict[str, Any] = {
|
|
168
|
+
"agent_id": working_agent_id,
|
|
169
|
+
"benchmark_name": validated_spec["benchmark_name"],
|
|
170
|
+
"tests": [],
|
|
171
|
+
"dry_run": dry_run,
|
|
172
|
+
}
|
|
173
|
+
if dry_run:
|
|
174
|
+
if output_json:
|
|
175
|
+
typer.echo(json.dumps(report, indent=2))
|
|
176
|
+
else:
|
|
177
|
+
typer.echo("Dry run complete. No tests executed.")
|
|
178
|
+
raise typer.Exit(code=0)
|
|
179
|
+
|
|
180
|
+
if not working_agent_id or working_agent_id == "dry-run":
|
|
181
|
+
typer.echo("Unable to resolve agent id for execution.", err=True)
|
|
182
|
+
raise typer.Exit(code=1)
|
|
183
|
+
|
|
184
|
+
with httpx.Client() as client:
|
|
185
|
+
try:
|
|
186
|
+
_verify_agent_scope(
|
|
187
|
+
client,
|
|
188
|
+
base_url=resolved_base_url,
|
|
189
|
+
shop_id=resolved_shop_id,
|
|
190
|
+
api_token=resolved_token,
|
|
191
|
+
agent_id=working_agent_id,
|
|
192
|
+
)
|
|
193
|
+
for test_case in validated_spec["tests"]:
|
|
194
|
+
typer.echo(f"\nRunning test: {test_case['name']}")
|
|
195
|
+
result = _run_test_case(
|
|
196
|
+
client=client,
|
|
197
|
+
base_url=resolved_base_url,
|
|
198
|
+
shop_id=resolved_shop_id,
|
|
199
|
+
api_token=resolved_token,
|
|
200
|
+
agent_id=working_agent_id,
|
|
201
|
+
test_case=test_case,
|
|
202
|
+
)
|
|
203
|
+
report["tests"].append(result)
|
|
204
|
+
except APIError as exc:
|
|
205
|
+
typer.echo(render_api_error(exc, action="run spec tests"), err=True)
|
|
206
|
+
raise typer.Exit(code=1) from exc
|
|
207
|
+
|
|
208
|
+
for test_result, test_case in zip(report["tests"], validated_spec["tests"]):
|
|
209
|
+
try:
|
|
210
|
+
rate_conversation_command(
|
|
211
|
+
conversation_id=test_result["conversation_id"],
|
|
212
|
+
agent_id=working_agent_id,
|
|
213
|
+
benchmark_name=validated_spec["benchmark_name"],
|
|
214
|
+
auto=bool(test_case.get("auto_rate", True)),
|
|
215
|
+
include_references=bool(test_case.get("include_references", True)),
|
|
216
|
+
pass_status=test_case.get("pass_status"),
|
|
217
|
+
csat_score=test_case.get("csat_score"),
|
|
218
|
+
feedback=test_case.get("feedback"),
|
|
219
|
+
reference_score=test_case.get("reference_score"),
|
|
220
|
+
reference_notes=test_case.get("reference_notes"),
|
|
221
|
+
base_url=resolved_base_url,
|
|
222
|
+
shop_id=resolved_shop_id,
|
|
223
|
+
api_token=resolved_token,
|
|
224
|
+
dry_run=False,
|
|
225
|
+
yes=True,
|
|
226
|
+
)
|
|
227
|
+
except Exception as exc:
|
|
228
|
+
typer.echo(
|
|
229
|
+
f"Rating failed for conversation {test_result['conversation_id']}: {exc}",
|
|
230
|
+
err=True,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if output_json:
|
|
234
|
+
typer.echo(json.dumps(report, indent=2, default=str))
|
|
235
|
+
else:
|
|
236
|
+
typer.echo("\nSpec run complete:")
|
|
237
|
+
typer.echo(f"- agent_id: {working_agent_id}")
|
|
238
|
+
typer.echo(f"- tests_executed: {len(report['tests'])}")
|
applied_cli/config.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from urllib.parse import urlsplit
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class Credentials:
|
|
7
|
+
base_url: str
|
|
8
|
+
shop_id: str
|
|
9
|
+
api_token: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DEFAULT_BASE_URL = "https://api.appliedlabs.ai"
|
|
13
|
+
ENDPOINT_ALIASES = {
|
|
14
|
+
"prod": "https://api.appliedlabs.ai",
|
|
15
|
+
"production": "https://api.appliedlabs.ai",
|
|
16
|
+
"api.appliedlabs.ai": "https://api.appliedlabs.ai",
|
|
17
|
+
"dev": "https://api.appliedlabs.dev",
|
|
18
|
+
"development": "https://api.appliedlabs.dev",
|
|
19
|
+
"api.appliedlabs.dev": "https://api.appliedlabs.dev",
|
|
20
|
+
"local": "http://localhost:8000",
|
|
21
|
+
"localhost": "http://localhost:8000",
|
|
22
|
+
"127.0.0.1": "http://127.0.0.1:8000",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def normalize_base_url(value: str) -> str:
|
|
27
|
+
raw = value.strip()
|
|
28
|
+
if not raw:
|
|
29
|
+
return ""
|
|
30
|
+
alias = ENDPOINT_ALIASES.get(raw.lower())
|
|
31
|
+
if alias:
|
|
32
|
+
return alias
|
|
33
|
+
|
|
34
|
+
with_scheme = raw
|
|
35
|
+
if not with_scheme.startswith(("http://", "https://")):
|
|
36
|
+
if with_scheme.startswith(("localhost", "127.0.0.1")):
|
|
37
|
+
with_scheme = f"http://{with_scheme}"
|
|
38
|
+
else:
|
|
39
|
+
with_scheme = f"https://{with_scheme}"
|
|
40
|
+
|
|
41
|
+
parsed = urlsplit(with_scheme)
|
|
42
|
+
if parsed.scheme and parsed.netloc:
|
|
43
|
+
return f"{parsed.scheme}://{parsed.netloc}".rstrip("/")
|
|
44
|
+
return with_scheme.rstrip("/")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def mask_token(token: str) -> str:
|
|
48
|
+
if len(token) <= 10:
|
|
49
|
+
return token[:2] + "..." if len(token) > 2 else "***"
|
|
50
|
+
return f"{token[:4]}...{token[-4:]}"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from applied_cli.http import APIError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _extract_request_id(detail: Any) -> str | None:
|
|
7
|
+
if not isinstance(detail, dict):
|
|
8
|
+
return None
|
|
9
|
+
if isinstance(detail.get("request_id"), str):
|
|
10
|
+
return detail["request_id"]
|
|
11
|
+
error_payload = detail.get("error")
|
|
12
|
+
if isinstance(error_payload, dict) and isinstance(
|
|
13
|
+
error_payload.get("request_id"), str
|
|
14
|
+
):
|
|
15
|
+
return error_payload["request_id"]
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def render_api_error(exc: APIError, *, action: str) -> str:
|
|
20
|
+
lines: list[str] = []
|
|
21
|
+
lines.append(f"Error during {action}.")
|
|
22
|
+
if exc.code:
|
|
23
|
+
lines.append(f"- code: {exc.code}")
|
|
24
|
+
if exc.status_code is not None:
|
|
25
|
+
lines.append(f"- http_status: {exc.status_code}")
|
|
26
|
+
if exc.method and exc.url:
|
|
27
|
+
lines.append(f"- request: {exc.method} {exc.url}")
|
|
28
|
+
lines.append(f"- message: {exc}")
|
|
29
|
+
if exc.hint:
|
|
30
|
+
lines.append(f"- fix: {exc.hint}")
|
|
31
|
+
request_id = _extract_request_id(exc.detail)
|
|
32
|
+
if request_id:
|
|
33
|
+
lines.append(f"- request_id: {request_id}")
|
|
34
|
+
if exc.suggestions:
|
|
35
|
+
lines.append("- next_steps:")
|
|
36
|
+
for step in exc.suggestions:
|
|
37
|
+
lines.append(f" - {step}")
|
|
38
|
+
return "\n".join(lines)
|