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,1231 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from applied_cli.commands._normalize import (
|
|
7
|
+
normalize_agent_type,
|
|
8
|
+
normalize_modality,
|
|
9
|
+
normalize_question,
|
|
10
|
+
normalize_response_type,
|
|
11
|
+
)
|
|
12
|
+
from applied_cli.commands._parsers import (
|
|
13
|
+
parse_json_dict as _parse_json_dict,
|
|
14
|
+
parse_optional_bool as _parse_optional_bool,
|
|
15
|
+
validate_uuid as _validate_uuid,
|
|
16
|
+
)
|
|
17
|
+
from applied_cli.commands._ui import (
|
|
18
|
+
confirm_or_exit,
|
|
19
|
+
emit_dry_run,
|
|
20
|
+
emit_success,
|
|
21
|
+
show_target,
|
|
22
|
+
)
|
|
23
|
+
from applied_cli.error_reporting import render_api_error
|
|
24
|
+
from applied_cli.http import (
|
|
25
|
+
APIError,
|
|
26
|
+
create_agent,
|
|
27
|
+
create_response,
|
|
28
|
+
get_agent,
|
|
29
|
+
list_agents,
|
|
30
|
+
list_responses,
|
|
31
|
+
patch_response,
|
|
32
|
+
update_agent,
|
|
33
|
+
)
|
|
34
|
+
from applied_cli.runtime import resolve_runtime
|
|
35
|
+
|
|
36
|
+
app = typer.Typer(help="Create and configure agents.")
|
|
37
|
+
settings_app = typer.Typer(help="Update specific agent settings safely.")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _deep_merge_dict(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
|
|
41
|
+
out = dict(base)
|
|
42
|
+
for key, value in patch.items():
|
|
43
|
+
existing = out.get(key)
|
|
44
|
+
if isinstance(existing, dict) and isinstance(value, dict):
|
|
45
|
+
out[key] = _deep_merge_dict(existing, value)
|
|
46
|
+
else:
|
|
47
|
+
out[key] = value
|
|
48
|
+
return out
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_agent_attached(row: dict[str, Any], *, agent_id: str) -> bool:
|
|
52
|
+
agents = row.get("agents")
|
|
53
|
+
if not isinstance(agents, list):
|
|
54
|
+
return False
|
|
55
|
+
return any(isinstance(item, dict) and str(item.get("id")) == agent_id for item in agents)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _upsert_inline_responses(
|
|
59
|
+
*,
|
|
60
|
+
base_url: str,
|
|
61
|
+
shop_id: str,
|
|
62
|
+
api_token: str,
|
|
63
|
+
agent_id: str,
|
|
64
|
+
response_rows: list[dict[str, Any]],
|
|
65
|
+
dry_run: bool,
|
|
66
|
+
) -> dict[str, Any]:
|
|
67
|
+
created = 0
|
|
68
|
+
updated = 0
|
|
69
|
+
unchanged = 0
|
|
70
|
+
for row in response_rows:
|
|
71
|
+
raw_type = row.get("type")
|
|
72
|
+
raw_q = row.get("question")
|
|
73
|
+
raw_a = row.get("answer")
|
|
74
|
+
if not isinstance(raw_type, str) or not isinstance(raw_q, str) or not isinstance(
|
|
75
|
+
raw_a, str
|
|
76
|
+
):
|
|
77
|
+
raise typer.BadParameter(
|
|
78
|
+
"Each response must include string fields: type, question, answer."
|
|
79
|
+
)
|
|
80
|
+
normalized_type = normalize_response_type(raw_type, field_label="response type")
|
|
81
|
+
normalized_question = normalize_question(raw_q)
|
|
82
|
+
payload: dict[str, Any] = {
|
|
83
|
+
"agent_ids": [agent_id],
|
|
84
|
+
"type": normalized_type,
|
|
85
|
+
"question": raw_q.strip(),
|
|
86
|
+
"answer": raw_a.strip(),
|
|
87
|
+
"active": bool(row.get("active", True)),
|
|
88
|
+
}
|
|
89
|
+
guardrail = row.get("guardrail")
|
|
90
|
+
if isinstance(guardrail, str):
|
|
91
|
+
payload["guardrail"] = guardrail
|
|
92
|
+
fields_to_extract = row.get("fields_to_extract")
|
|
93
|
+
if isinstance(fields_to_extract, list):
|
|
94
|
+
payload["fields_to_extract"] = fields_to_extract
|
|
95
|
+
|
|
96
|
+
candidates = list_responses(
|
|
97
|
+
base_url=base_url,
|
|
98
|
+
shop_id=shop_id,
|
|
99
|
+
api_token=api_token,
|
|
100
|
+
agent_id=agent_id,
|
|
101
|
+
response_type=normalized_type,
|
|
102
|
+
limit=500,
|
|
103
|
+
)
|
|
104
|
+
matched: Optional[dict[str, Any]] = None
|
|
105
|
+
for candidate in candidates:
|
|
106
|
+
if not _is_agent_attached(candidate, agent_id=agent_id):
|
|
107
|
+
continue
|
|
108
|
+
if normalize_question(str(candidate.get("question") or "")) == normalized_question:
|
|
109
|
+
matched = candidate
|
|
110
|
+
break
|
|
111
|
+
if matched is None:
|
|
112
|
+
created += 1
|
|
113
|
+
if not dry_run:
|
|
114
|
+
create_response(
|
|
115
|
+
base_url=base_url,
|
|
116
|
+
shop_id=shop_id,
|
|
117
|
+
api_token=api_token,
|
|
118
|
+
payload=payload,
|
|
119
|
+
)
|
|
120
|
+
continue
|
|
121
|
+
raw_current_fields = matched.get("fields_to_extract")
|
|
122
|
+
raw_target_fields = payload.get("fields_to_extract")
|
|
123
|
+
current_reduced = {
|
|
124
|
+
"type": str(matched.get("type") or "").strip().lower(),
|
|
125
|
+
"question": str(matched.get("question") or "").strip(),
|
|
126
|
+
"answer": str(matched.get("answer") or "").strip(),
|
|
127
|
+
"guardrail": str(matched.get("guardrail") or "").strip(),
|
|
128
|
+
"active": bool(matched.get("active")),
|
|
129
|
+
"fields_to_extract": raw_current_fields
|
|
130
|
+
if isinstance(raw_current_fields, list)
|
|
131
|
+
else [],
|
|
132
|
+
}
|
|
133
|
+
target_reduced = {
|
|
134
|
+
"type": str(payload.get("type") or "").strip().lower(),
|
|
135
|
+
"question": str(payload.get("question") or "").strip(),
|
|
136
|
+
"answer": str(payload.get("answer") or "").strip(),
|
|
137
|
+
"guardrail": str(payload.get("guardrail") or "").strip(),
|
|
138
|
+
"active": bool(payload.get("active")),
|
|
139
|
+
"fields_to_extract": raw_target_fields
|
|
140
|
+
if isinstance(raw_target_fields, list)
|
|
141
|
+
else [],
|
|
142
|
+
}
|
|
143
|
+
if current_reduced == target_reduced:
|
|
144
|
+
unchanged += 1
|
|
145
|
+
else:
|
|
146
|
+
updated += 1
|
|
147
|
+
if not dry_run:
|
|
148
|
+
patch_response(
|
|
149
|
+
base_url=base_url,
|
|
150
|
+
shop_id=shop_id,
|
|
151
|
+
api_token=api_token,
|
|
152
|
+
response_id=str(matched.get("id")),
|
|
153
|
+
payload=payload,
|
|
154
|
+
)
|
|
155
|
+
return {"created": created, "updated": updated, "unchanged": unchanged}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _resolve_metadata_options(
|
|
159
|
+
*,
|
|
160
|
+
merge_raw: Optional[str],
|
|
161
|
+
replace_raw: Optional[str],
|
|
162
|
+
current_metadata: Optional[dict[str, Any]] = None,
|
|
163
|
+
) -> Optional[dict[str, Any]]:
|
|
164
|
+
metadata_merge = _parse_json_dict(merge_raw, field_name="metadata-merge-json")
|
|
165
|
+
metadata_replace = _parse_json_dict(replace_raw, field_name="metadata-replace-json")
|
|
166
|
+
if metadata_merge is not None and metadata_replace is not None:
|
|
167
|
+
raise typer.BadParameter(
|
|
168
|
+
"Use either --metadata-merge-json or --metadata-replace-json, not both."
|
|
169
|
+
)
|
|
170
|
+
if metadata_replace is not None:
|
|
171
|
+
return metadata_replace
|
|
172
|
+
if metadata_merge is not None:
|
|
173
|
+
base = current_metadata or {}
|
|
174
|
+
return _deep_merge_dict(base, metadata_merge)
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@app.command(
|
|
179
|
+
"list",
|
|
180
|
+
help="List agents in a shop. Example: applied-cli agent list --limit 20",
|
|
181
|
+
)
|
|
182
|
+
def list_cmd(
|
|
183
|
+
limit: int = typer.Option(100, "--limit", help="Maximum number of agents."),
|
|
184
|
+
ordering: str = typer.Option(
|
|
185
|
+
"-created_at", "--ordering", help="Ordering field, e.g. -created_at."
|
|
186
|
+
),
|
|
187
|
+
base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
|
|
188
|
+
shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
|
|
189
|
+
api_token: Optional[str] = typer.Option(None, help="Applied API token."),
|
|
190
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
191
|
+
) -> None:
|
|
192
|
+
try:
|
|
193
|
+
resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
|
|
194
|
+
base_url=base_url,
|
|
195
|
+
shop_id=shop_id,
|
|
196
|
+
api_token=api_token,
|
|
197
|
+
)
|
|
198
|
+
rows = list_agents(
|
|
199
|
+
base_url=resolved_base_url,
|
|
200
|
+
shop_id=resolved_shop_id,
|
|
201
|
+
api_token=resolved_token,
|
|
202
|
+
limit=limit,
|
|
203
|
+
ordering=ordering,
|
|
204
|
+
)
|
|
205
|
+
except APIError as exc:
|
|
206
|
+
typer.echo(render_api_error(exc, action="list agents"), err=True)
|
|
207
|
+
raise typer.Exit(code=1) from exc
|
|
208
|
+
|
|
209
|
+
if output_json:
|
|
210
|
+
typer.echo(json.dumps(rows, indent=2, default=str))
|
|
211
|
+
return
|
|
212
|
+
if not rows:
|
|
213
|
+
typer.echo("No results.")
|
|
214
|
+
return
|
|
215
|
+
for row in rows:
|
|
216
|
+
typer.echo(
|
|
217
|
+
f"id={row.get('id')} | name={row.get('name')} | modality={row.get('modality')} | type={row.get('type')} | auto_reply={row.get('auto_reply')} | created_at={row.get('created_at')}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@app.command(
|
|
222
|
+
"show",
|
|
223
|
+
help="Show one agent by UUID. Example: applied-cli agent show --agent-id <uuid>",
|
|
224
|
+
)
|
|
225
|
+
def show(
|
|
226
|
+
agent_id: str = typer.Option(
|
|
227
|
+
..., "--agent-id", "--agent", "--id", help="Target agent UUID."
|
|
228
|
+
),
|
|
229
|
+
base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
|
|
230
|
+
shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
|
|
231
|
+
api_token: Optional[str] = typer.Option(None, help="Applied API token."),
|
|
232
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
233
|
+
) -> None:
|
|
234
|
+
_validate_uuid(agent_id, field_name="agent-id")
|
|
235
|
+
try:
|
|
236
|
+
resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
|
|
237
|
+
base_url=base_url,
|
|
238
|
+
shop_id=shop_id,
|
|
239
|
+
api_token=api_token,
|
|
240
|
+
)
|
|
241
|
+
row = get_agent(
|
|
242
|
+
base_url=resolved_base_url,
|
|
243
|
+
shop_id=resolved_shop_id,
|
|
244
|
+
api_token=resolved_token,
|
|
245
|
+
agent_id=agent_id,
|
|
246
|
+
)
|
|
247
|
+
except APIError as exc:
|
|
248
|
+
typer.echo(render_api_error(exc, action="show agent"), err=True)
|
|
249
|
+
if exc.status_code == 404:
|
|
250
|
+
typer.echo(
|
|
251
|
+
"Hint: verify `agent-id` and shop scope with `applied-cli agent list --json`.",
|
|
252
|
+
err=True,
|
|
253
|
+
)
|
|
254
|
+
raise typer.Exit(code=1) from exc
|
|
255
|
+
|
|
256
|
+
if output_json:
|
|
257
|
+
typer.echo(json.dumps(row, indent=2, default=str))
|
|
258
|
+
return
|
|
259
|
+
typer.echo(f"id={row.get('id')}")
|
|
260
|
+
typer.echo(f"name={row.get('name')}")
|
|
261
|
+
typer.echo(f"modality={row.get('modality')}")
|
|
262
|
+
typer.echo(f"type={row.get('type')}")
|
|
263
|
+
typer.echo(f"auto_reply={row.get('auto_reply')}")
|
|
264
|
+
typer.echo(f"created_at={row.get('created_at')}")
|
|
265
|
+
typer.echo(f"updated_at={row.get('updated_at')}")
|
|
266
|
+
typer.echo(f"escalation_mode={row.get('escalation_mode')}")
|
|
267
|
+
typer.echo(f"escalation_wait_time_mode={row.get('escalation_wait_time_mode')}")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
app.command(
|
|
271
|
+
"describe",
|
|
272
|
+
help="Describe one agent by UUID. Example: applied-cli agent describe --agent-id <uuid>",
|
|
273
|
+
)(show)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@app.command(
|
|
277
|
+
"create",
|
|
278
|
+
help=(
|
|
279
|
+
"Create an agent. Example: applied-cli agent create --name Smalls "
|
|
280
|
+
"--modality chat --type customer_support"
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
def create(
|
|
284
|
+
name: str = typer.Option(..., "--name", help="Agent name."),
|
|
285
|
+
modality: str = typer.Option(..., "--modality", help="all/call/sms/email/chat/internal."),
|
|
286
|
+
agent_type: str = typer.Option("customer_support", "--type", help="Agent type."),
|
|
287
|
+
description: Optional[str] = typer.Option(None, "--description", help="Agent description."),
|
|
288
|
+
model: Optional[str] = typer.Option(None, "--model", help="Model identifier."),
|
|
289
|
+
metadata_merge_json: Optional[str] = typer.Option(
|
|
290
|
+
None, "--metadata-merge-json", help="Merge JSON object into metadata."
|
|
291
|
+
),
|
|
292
|
+
metadata_replace_json: Optional[str] = typer.Option(
|
|
293
|
+
None,
|
|
294
|
+
"--metadata-replace-json",
|
|
295
|
+
help="Replace metadata with JSON object (destructive).",
|
|
296
|
+
),
|
|
297
|
+
suggestions: list[str] = typer.Option(
|
|
298
|
+
[], "--suggestion", help="Suggestion text. Repeat option to add many."
|
|
299
|
+
),
|
|
300
|
+
auto_reply: bool = typer.Option(
|
|
301
|
+
True,
|
|
302
|
+
"--auto-reply/--no-auto-reply",
|
|
303
|
+
help="Whether agent automatically replies to incoming messages.",
|
|
304
|
+
),
|
|
305
|
+
guardrail: Optional[str] = typer.Option(None, "--guardrail", help="General guardrail text."),
|
|
306
|
+
escalation_mode: Optional[str] = typer.Option(None, "--escalation-mode"),
|
|
307
|
+
escalation_wait_time_mode: Optional[str] = typer.Option(
|
|
308
|
+
None, "--escalation-wait-time-mode"
|
|
309
|
+
),
|
|
310
|
+
response_delay_in_seconds: Optional[int] = typer.Option(
|
|
311
|
+
None, "--response-delay-seconds"
|
|
312
|
+
),
|
|
313
|
+
base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
|
|
314
|
+
shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
|
|
315
|
+
api_token: Optional[str] = typer.Option(None, help="Applied API token."),
|
|
316
|
+
dry_run: bool = typer.Option(False, help="Preview payload without writes."),
|
|
317
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
|
|
318
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
319
|
+
) -> None:
|
|
320
|
+
normalized_modality = normalize_modality(modality)
|
|
321
|
+
normalized_type = normalize_agent_type(agent_type)
|
|
322
|
+
metadata = _resolve_metadata_options(
|
|
323
|
+
merge_raw=metadata_merge_json,
|
|
324
|
+
replace_raw=metadata_replace_json,
|
|
325
|
+
current_metadata=None,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
|
|
330
|
+
base_url=base_url, shop_id=shop_id, api_token=api_token
|
|
331
|
+
)
|
|
332
|
+
except APIError as exc:
|
|
333
|
+
typer.echo(render_api_error(exc, action="resolve runtime for agent create"), err=True)
|
|
334
|
+
raise typer.Exit(code=1) from exc
|
|
335
|
+
|
|
336
|
+
payload: dict[str, Any] = {
|
|
337
|
+
"name": name.strip(),
|
|
338
|
+
"modality": normalized_modality,
|
|
339
|
+
"type": normalized_type,
|
|
340
|
+
}
|
|
341
|
+
if description is not None:
|
|
342
|
+
payload["description"] = description
|
|
343
|
+
if model is not None:
|
|
344
|
+
payload["model"] = model
|
|
345
|
+
if metadata is not None:
|
|
346
|
+
payload["metadata"] = metadata
|
|
347
|
+
if suggestions:
|
|
348
|
+
payload["suggestions"] = [item.strip() for item in suggestions if item.strip()]
|
|
349
|
+
payload["auto_reply"] = auto_reply
|
|
350
|
+
if guardrail is not None:
|
|
351
|
+
payload["guardrail"] = guardrail
|
|
352
|
+
if escalation_mode is not None:
|
|
353
|
+
payload["escalation_mode"] = escalation_mode
|
|
354
|
+
if escalation_wait_time_mode is not None:
|
|
355
|
+
payload["escalation_wait_time_mode"] = escalation_wait_time_mode
|
|
356
|
+
if response_delay_in_seconds is not None:
|
|
357
|
+
payload["response_delay_in_seconds"] = response_delay_in_seconds
|
|
358
|
+
|
|
359
|
+
show_target(
|
|
360
|
+
{
|
|
361
|
+
"base_url": resolved_base_url,
|
|
362
|
+
"shop_id": resolved_shop_id,
|
|
363
|
+
"name": name,
|
|
364
|
+
"modality": normalized_modality,
|
|
365
|
+
"dry_run": dry_run,
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
if metadata_replace_json is not None and not yes:
|
|
369
|
+
typer.echo(
|
|
370
|
+
"Warning: --metadata-replace-json overwrites existing metadata keys."
|
|
371
|
+
)
|
|
372
|
+
confirm_or_exit(yes=yes)
|
|
373
|
+
|
|
374
|
+
if dry_run:
|
|
375
|
+
emit_dry_run(payload={"payload": payload, "dry_run": True}, output_json=output_json)
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
created = create_agent(
|
|
379
|
+
base_url=resolved_base_url,
|
|
380
|
+
shop_id=resolved_shop_id,
|
|
381
|
+
api_token=resolved_token,
|
|
382
|
+
payload=payload,
|
|
383
|
+
)
|
|
384
|
+
except APIError as exc:
|
|
385
|
+
typer.echo(render_api_error(exc, action="create agent"), err=True)
|
|
386
|
+
raise typer.Exit(code=1) from exc
|
|
387
|
+
emit_success(
|
|
388
|
+
output_json=output_json,
|
|
389
|
+
payload=created,
|
|
390
|
+
fields={"agent_id": created.get("id")},
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@app.command(
|
|
395
|
+
"update",
|
|
396
|
+
help=(
|
|
397
|
+
"Update an existing agent. Example: applied-cli agent update --agent-id <uuid> "
|
|
398
|
+
"--no-auto-reply --guardrail 'Never disclose personal data.'"
|
|
399
|
+
),
|
|
400
|
+
)
|
|
401
|
+
def update(
|
|
402
|
+
agent_id: str = typer.Option(
|
|
403
|
+
..., "--agent-id", "--agent", "--id", help="Target agent UUID."
|
|
404
|
+
),
|
|
405
|
+
name: Optional[str] = typer.Option(None, "--name", help="Agent name."),
|
|
406
|
+
modality: Optional[str] = typer.Option(
|
|
407
|
+
None, "--modality", help="all/call/sms/email/chat/internal."
|
|
408
|
+
),
|
|
409
|
+
agent_type: Optional[str] = typer.Option(None, "--type", help="Agent type."),
|
|
410
|
+
description: Optional[str] = typer.Option(None, "--description", help="Agent description."),
|
|
411
|
+
model: Optional[str] = typer.Option(None, "--model", help="Model identifier."),
|
|
412
|
+
metadata_merge_json: Optional[str] = typer.Option(
|
|
413
|
+
None, "--metadata-merge-json", help="Merge JSON object into metadata."
|
|
414
|
+
),
|
|
415
|
+
metadata_replace_json: Optional[str] = typer.Option(
|
|
416
|
+
None,
|
|
417
|
+
"--metadata-replace-json",
|
|
418
|
+
help="Replace metadata with JSON object (destructive).",
|
|
419
|
+
),
|
|
420
|
+
suggestions: list[str] = typer.Option(
|
|
421
|
+
[], "--suggestion", help="Suggestion text. Repeat option to add many."
|
|
422
|
+
),
|
|
423
|
+
auto_reply: Optional[bool] = typer.Option(
|
|
424
|
+
None,
|
|
425
|
+
"--auto-reply/--no-auto-reply",
|
|
426
|
+
help="Set whether agent automatically replies to messages.",
|
|
427
|
+
),
|
|
428
|
+
guardrail: Optional[str] = typer.Option(None, "--guardrail", help="General guardrail text."),
|
|
429
|
+
escalation_mode: Optional[str] = typer.Option(None, "--escalation-mode"),
|
|
430
|
+
escalation_wait_time_mode: Optional[str] = typer.Option(
|
|
431
|
+
None, "--escalation-wait-time-mode"
|
|
432
|
+
),
|
|
433
|
+
response_delay_in_seconds: Optional[int] = typer.Option(
|
|
434
|
+
None, "--response-delay-seconds"
|
|
435
|
+
),
|
|
436
|
+
base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
|
|
437
|
+
shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
|
|
438
|
+
api_token: Optional[str] = typer.Option(None, help="Applied API token."),
|
|
439
|
+
dry_run: bool = typer.Option(False, help="Preview payload without writes."),
|
|
440
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
|
|
441
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
442
|
+
) -> None:
|
|
443
|
+
_validate_uuid(agent_id, field_name="agent-id")
|
|
444
|
+
normalized_modality = normalize_modality(modality) if modality is not None else None
|
|
445
|
+
normalized_type = normalize_agent_type(agent_type) if agent_type is not None else None
|
|
446
|
+
try:
|
|
447
|
+
resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
|
|
448
|
+
base_url=base_url, shop_id=shop_id, api_token=api_token
|
|
449
|
+
)
|
|
450
|
+
except APIError as exc:
|
|
451
|
+
typer.echo(render_api_error(exc, action="resolve runtime for agent update"), err=True)
|
|
452
|
+
raise typer.Exit(code=1) from exc
|
|
453
|
+
|
|
454
|
+
current_metadata: Optional[dict[str, Any]] = None
|
|
455
|
+
if metadata_merge_json is not None:
|
|
456
|
+
try:
|
|
457
|
+
current_agent = get_agent(
|
|
458
|
+
base_url=resolved_base_url,
|
|
459
|
+
shop_id=resolved_shop_id,
|
|
460
|
+
api_token=resolved_token,
|
|
461
|
+
agent_id=agent_id,
|
|
462
|
+
)
|
|
463
|
+
except APIError as exc:
|
|
464
|
+
typer.echo(render_api_error(exc, action="read current agent before update"), err=True)
|
|
465
|
+
raise typer.Exit(code=1) from exc
|
|
466
|
+
raw_metadata = current_agent.get("metadata")
|
|
467
|
+
current_metadata = raw_metadata if isinstance(raw_metadata, dict) else {}
|
|
468
|
+
metadata = _resolve_metadata_options(
|
|
469
|
+
merge_raw=metadata_merge_json,
|
|
470
|
+
replace_raw=metadata_replace_json,
|
|
471
|
+
current_metadata=current_metadata,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
payload: dict[str, Any] = {}
|
|
475
|
+
if name is not None:
|
|
476
|
+
payload["name"] = name.strip()
|
|
477
|
+
if normalized_modality is not None:
|
|
478
|
+
payload["modality"] = normalized_modality
|
|
479
|
+
if normalized_type is not None:
|
|
480
|
+
payload["type"] = normalized_type
|
|
481
|
+
if description is not None:
|
|
482
|
+
payload["description"] = description
|
|
483
|
+
if model is not None:
|
|
484
|
+
payload["model"] = model
|
|
485
|
+
if metadata is not None:
|
|
486
|
+
payload["metadata"] = metadata
|
|
487
|
+
if suggestions:
|
|
488
|
+
payload["suggestions"] = [item.strip() for item in suggestions if item.strip()]
|
|
489
|
+
if auto_reply is not None:
|
|
490
|
+
payload["auto_reply"] = auto_reply
|
|
491
|
+
if guardrail is not None:
|
|
492
|
+
payload["guardrail"] = guardrail
|
|
493
|
+
if escalation_mode is not None:
|
|
494
|
+
payload["escalation_mode"] = escalation_mode
|
|
495
|
+
if escalation_wait_time_mode is not None:
|
|
496
|
+
payload["escalation_wait_time_mode"] = escalation_wait_time_mode
|
|
497
|
+
if response_delay_in_seconds is not None:
|
|
498
|
+
payload["response_delay_in_seconds"] = response_delay_in_seconds
|
|
499
|
+
if not payload:
|
|
500
|
+
raise typer.BadParameter("No fields provided. Pass at least one update option.")
|
|
501
|
+
|
|
502
|
+
show_target(
|
|
503
|
+
{
|
|
504
|
+
"base_url": resolved_base_url,
|
|
505
|
+
"shop_id": resolved_shop_id,
|
|
506
|
+
"agent_id": agent_id,
|
|
507
|
+
"fields": ", ".join(sorted(payload.keys())),
|
|
508
|
+
"dry_run": dry_run,
|
|
509
|
+
}
|
|
510
|
+
)
|
|
511
|
+
if metadata_replace_json is not None and not yes:
|
|
512
|
+
typer.echo(
|
|
513
|
+
"Warning: --metadata-replace-json overwrites existing metadata keys."
|
|
514
|
+
)
|
|
515
|
+
confirm_or_exit(yes=yes)
|
|
516
|
+
|
|
517
|
+
if dry_run:
|
|
518
|
+
emit_dry_run(
|
|
519
|
+
payload={"agent_id": agent_id, "payload": payload, "dry_run": True},
|
|
520
|
+
output_json=output_json,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
updated = update_agent(
|
|
525
|
+
base_url=resolved_base_url,
|
|
526
|
+
shop_id=resolved_shop_id,
|
|
527
|
+
api_token=resolved_token,
|
|
528
|
+
agent_id=agent_id,
|
|
529
|
+
payload=payload,
|
|
530
|
+
)
|
|
531
|
+
except APIError as exc:
|
|
532
|
+
typer.echo(render_api_error(exc, action="update agent"), err=True)
|
|
533
|
+
if exc.status_code == 404:
|
|
534
|
+
typer.echo(
|
|
535
|
+
"Hint: this often means the `agent-id` is wrong for the selected shop. Run `applied-cli agent list` first.",
|
|
536
|
+
err=True,
|
|
537
|
+
)
|
|
538
|
+
raise typer.Exit(code=1) from exc
|
|
539
|
+
emit_success(
|
|
540
|
+
output_json=output_json,
|
|
541
|
+
payload=updated,
|
|
542
|
+
fields={"agent_id": updated.get("id")},
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
@app.command(
|
|
547
|
+
"setup",
|
|
548
|
+
help=(
|
|
549
|
+
"Create or update an agent and optional responses. Example: applied-cli agent setup "
|
|
550
|
+
"--name Smalls --modality chat --responses-json '[{\"type\":\"qa\",\"question\":\"Shipping?\",\"answer\":\"We ship in US and Canada.\"}]'"
|
|
551
|
+
),
|
|
552
|
+
)
|
|
553
|
+
def setup(
|
|
554
|
+
agent_id: Optional[str] = typer.Option(
|
|
555
|
+
None,
|
|
556
|
+
"--agent-id",
|
|
557
|
+
"--agent",
|
|
558
|
+
"--id",
|
|
559
|
+
help="Existing agent UUID. Omit to create a new agent.",
|
|
560
|
+
),
|
|
561
|
+
name: Optional[str] = typer.Option(
|
|
562
|
+
None, "--name", help="Agent name (required when creating)."
|
|
563
|
+
),
|
|
564
|
+
modality: str = typer.Option("chat", "--modality", help="all/call/sms/email/chat/internal."),
|
|
565
|
+
agent_type: str = typer.Option("customer_support", "--type", help="Agent type."),
|
|
566
|
+
description: Optional[str] = typer.Option(None, "--description"),
|
|
567
|
+
guardrail: Optional[str] = typer.Option(None, "--guardrail"),
|
|
568
|
+
auto_reply: Optional[bool] = typer.Option(
|
|
569
|
+
None, "--auto-reply/--no-auto-reply", help="Enable/disable automatic replies."
|
|
570
|
+
),
|
|
571
|
+
escalation_mode: Optional[str] = typer.Option(None, "--escalation-mode"),
|
|
572
|
+
escalation_wait_time_mode: Optional[str] = typer.Option(
|
|
573
|
+
None, "--escalation-wait-time-mode"
|
|
574
|
+
),
|
|
575
|
+
response_delay_in_seconds: Optional[int] = typer.Option(
|
|
576
|
+
None, "--response-delay-seconds"
|
|
577
|
+
),
|
|
578
|
+
add_suggestion: list[str] = typer.Option(
|
|
579
|
+
[], "--add-suggestion", help="Append suggestion text."
|
|
580
|
+
),
|
|
581
|
+
metadata_merge_json: Optional[str] = typer.Option(
|
|
582
|
+
None, "--metadata-merge-json", help="Merge JSON object into metadata."
|
|
583
|
+
),
|
|
584
|
+
responses_json: Optional[str] = typer.Option(
|
|
585
|
+
None,
|
|
586
|
+
"--responses-json",
|
|
587
|
+
help="JSON array of response rule objects to upsert.",
|
|
588
|
+
),
|
|
589
|
+
base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
|
|
590
|
+
shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
|
|
591
|
+
api_token: Optional[str] = typer.Option(None, help="Applied API token."),
|
|
592
|
+
dry_run: bool = typer.Option(False, help="Preview without writes."),
|
|
593
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
|
|
594
|
+
output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
595
|
+
) -> None:
|
|
596
|
+
if agent_id:
|
|
597
|
+
_validate_uuid(agent_id, field_name="agent-id")
|
|
598
|
+
try:
|
|
599
|
+
resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
|
|
600
|
+
base_url=base_url, shop_id=shop_id, api_token=api_token
|
|
601
|
+
)
|
|
602
|
+
except APIError as exc:
|
|
603
|
+
typer.echo(render_api_error(exc, action="resolve runtime for agent setup"), err=True)
|
|
604
|
+
raise typer.Exit(code=1) from exc
|
|
605
|
+
|
|
606
|
+
resolved_agent_id = agent_id
|
|
607
|
+
normalized_modality = normalize_modality(modality)
|
|
608
|
+
normalized_type = normalize_agent_type(agent_type)
|
|
609
|
+
if not resolved_agent_id and not (name and name.strip()):
|
|
610
|
+
raise typer.BadParameter("`--name` is required when creating a new agent.")
|
|
611
|
+
|
|
612
|
+
responses_rows: list[dict[str, Any]] = []
|
|
613
|
+
if responses_json:
|
|
614
|
+
try:
|
|
615
|
+
parsed = json.loads(responses_json)
|
|
616
|
+
except json.JSONDecodeError as exc:
|
|
617
|
+
raise typer.BadParameter("responses-json must be valid JSON.") from exc
|
|
618
|
+
if not isinstance(parsed, list):
|
|
619
|
+
raise typer.BadParameter("responses-json must decode to a JSON array.")
|
|
620
|
+
responses_rows = [item for item in parsed if isinstance(item, dict)]
|
|
621
|
+
if len(responses_rows) != len(parsed):
|
|
622
|
+
raise typer.BadParameter("Each entry in responses-json must be a JSON object.")
|
|
623
|
+
|
|
624
|
+
show_target(
|
|
625
|
+
{
|
|
626
|
+
"base_url": resolved_base_url,
|
|
627
|
+
"shop_id": resolved_shop_id,
|
|
628
|
+
"agent_id": resolved_agent_id or "(create new)",
|
|
629
|
+
"responses_upsert_count": len(responses_rows),
|
|
630
|
+
"dry_run": dry_run,
|
|
631
|
+
}
|
|
632
|
+
)
|
|
633
|
+
confirm_or_exit(yes=yes)
|
|
634
|
+
|
|
635
|
+
try:
|
|
636
|
+
if not resolved_agent_id:
|
|
637
|
+
create_payload: dict[str, Any] = {
|
|
638
|
+
"name": str(name).strip(),
|
|
639
|
+
"modality": normalized_modality,
|
|
640
|
+
"type": normalized_type,
|
|
641
|
+
}
|
|
642
|
+
if description is not None:
|
|
643
|
+
create_payload["description"] = description
|
|
644
|
+
if auto_reply is not None:
|
|
645
|
+
create_payload["auto_reply"] = auto_reply
|
|
646
|
+
if guardrail is not None:
|
|
647
|
+
create_payload["guardrail"] = guardrail
|
|
648
|
+
if escalation_mode is not None:
|
|
649
|
+
create_payload["escalation_mode"] = escalation_mode
|
|
650
|
+
if escalation_wait_time_mode is not None:
|
|
651
|
+
create_payload["escalation_wait_time_mode"] = escalation_wait_time_mode
|
|
652
|
+
if response_delay_in_seconds is not None:
|
|
653
|
+
create_payload["response_delay_in_seconds"] = response_delay_in_seconds
|
|
654
|
+
metadata_payload = _resolve_metadata_options(
|
|
655
|
+
merge_raw=metadata_merge_json,
|
|
656
|
+
replace_raw=None,
|
|
657
|
+
current_metadata={},
|
|
658
|
+
)
|
|
659
|
+
if metadata_payload is not None:
|
|
660
|
+
create_payload["metadata"] = metadata_payload
|
|
661
|
+
if add_suggestion:
|
|
662
|
+
create_payload["suggestions"] = [s.strip() for s in add_suggestion if s.strip()]
|
|
663
|
+
if dry_run:
|
|
664
|
+
resolved_agent_id = "(planned-create)"
|
|
665
|
+
else:
|
|
666
|
+
created = create_agent(
|
|
667
|
+
base_url=resolved_base_url,
|
|
668
|
+
shop_id=resolved_shop_id,
|
|
669
|
+
api_token=resolved_token,
|
|
670
|
+
payload=create_payload,
|
|
671
|
+
)
|
|
672
|
+
resolved_agent_id = str(created.get("id"))
|
|
673
|
+
else:
|
|
674
|
+
update_payload: dict[str, Any] = {}
|
|
675
|
+
if name is not None:
|
|
676
|
+
update_payload["name"] = name.strip()
|
|
677
|
+
if modality is not None:
|
|
678
|
+
update_payload["modality"] = normalized_modality
|
|
679
|
+
if agent_type is not None:
|
|
680
|
+
update_payload["type"] = normalized_type
|
|
681
|
+
if description is not None:
|
|
682
|
+
update_payload["description"] = description
|
|
683
|
+
if auto_reply is not None:
|
|
684
|
+
update_payload["auto_reply"] = auto_reply
|
|
685
|
+
if guardrail is not None:
|
|
686
|
+
update_payload["guardrail"] = guardrail
|
|
687
|
+
if escalation_mode is not None:
|
|
688
|
+
update_payload["escalation_mode"] = escalation_mode
|
|
689
|
+
if escalation_wait_time_mode is not None:
|
|
690
|
+
update_payload["escalation_wait_time_mode"] = escalation_wait_time_mode
|
|
691
|
+
if response_delay_in_seconds is not None:
|
|
692
|
+
update_payload["response_delay_in_seconds"] = response_delay_in_seconds
|
|
693
|
+
if add_suggestion:
|
|
694
|
+
current = get_agent(
|
|
695
|
+
base_url=resolved_base_url,
|
|
696
|
+
shop_id=resolved_shop_id,
|
|
697
|
+
api_token=resolved_token,
|
|
698
|
+
agent_id=resolved_agent_id,
|
|
699
|
+
)
|
|
700
|
+
existing = current.get("suggestions")
|
|
701
|
+
existing_list = existing if isinstance(existing, list) else []
|
|
702
|
+
merged = [str(x).strip() for x in existing_list if str(x).strip()]
|
|
703
|
+
for item in add_suggestion:
|
|
704
|
+
norm = item.strip()
|
|
705
|
+
if norm and norm not in merged:
|
|
706
|
+
merged.append(norm)
|
|
707
|
+
update_payload["suggestions"] = merged
|
|
708
|
+
if metadata_merge_json is not None:
|
|
709
|
+
current = get_agent(
|
|
710
|
+
base_url=resolved_base_url,
|
|
711
|
+
shop_id=resolved_shop_id,
|
|
712
|
+
api_token=resolved_token,
|
|
713
|
+
agent_id=resolved_agent_id,
|
|
714
|
+
)
|
|
715
|
+
raw_metadata = current.get("metadata")
|
|
716
|
+
current_metadata = raw_metadata if isinstance(raw_metadata, dict) else {}
|
|
717
|
+
update_payload["metadata"] = _resolve_metadata_options(
|
|
718
|
+
merge_raw=metadata_merge_json,
|
|
719
|
+
replace_raw=None,
|
|
720
|
+
current_metadata=current_metadata,
|
|
721
|
+
)
|
|
722
|
+
if update_payload and not dry_run:
|
|
723
|
+
update_agent(
|
|
724
|
+
base_url=resolved_base_url,
|
|
725
|
+
shop_id=resolved_shop_id,
|
|
726
|
+
api_token=resolved_token,
|
|
727
|
+
agent_id=resolved_agent_id,
|
|
728
|
+
payload=update_payload,
|
|
729
|
+
)
|
|
730
|
+
except APIError as exc:
|
|
731
|
+
typer.echo(render_api_error(exc, action="run agent setup"), err=True)
|
|
732
|
+
raise typer.Exit(code=1) from exc
|
|
733
|
+
|
|
734
|
+
response_summary = {"created": 0, "updated": 0, "unchanged": 0}
|
|
735
|
+
if responses_rows and resolved_agent_id and resolved_agent_id != "(planned-create)":
|
|
736
|
+
try:
|
|
737
|
+
response_summary = _upsert_inline_responses(
|
|
738
|
+
base_url=resolved_base_url,
|
|
739
|
+
shop_id=resolved_shop_id,
|
|
740
|
+
api_token=resolved_token,
|
|
741
|
+
agent_id=resolved_agent_id,
|
|
742
|
+
response_rows=responses_rows,
|
|
743
|
+
dry_run=dry_run,
|
|
744
|
+
)
|
|
745
|
+
except (APIError, typer.BadParameter) as exc:
|
|
746
|
+
if isinstance(exc, APIError):
|
|
747
|
+
typer.echo(
|
|
748
|
+
render_api_error(exc, action="upsert setup responses"), err=True
|
|
749
|
+
)
|
|
750
|
+
else:
|
|
751
|
+
typer.echo(str(exc), err=True)
|
|
752
|
+
raise typer.Exit(code=1) from exc
|
|
753
|
+
elif responses_rows and dry_run:
|
|
754
|
+
response_summary = {"created": len(responses_rows), "updated": 0, "unchanged": 0}
|
|
755
|
+
|
|
756
|
+
out = {
|
|
757
|
+
"agent_id": resolved_agent_id,
|
|
758
|
+
"responses_created": response_summary["created"],
|
|
759
|
+
"responses_updated": response_summary["updated"],
|
|
760
|
+
"responses_unchanged": response_summary["unchanged"],
|
|
761
|
+
"dry_run": dry_run,
|
|
762
|
+
}
|
|
763
|
+
if output_json:
|
|
764
|
+
typer.echo(json.dumps(out, indent=2, default=str))
|
|
765
|
+
else:
|
|
766
|
+
emit_success(
|
|
767
|
+
output_json=output_json,
|
|
768
|
+
payload=out,
|
|
769
|
+
fields={
|
|
770
|
+
"agent_id": out["agent_id"],
|
|
771
|
+
"responses_created": out["responses_created"],
|
|
772
|
+
"responses_updated": out["responses_updated"],
|
|
773
|
+
"responses_unchanged": out["responses_unchanged"],
|
|
774
|
+
"dry_run": out["dry_run"],
|
|
775
|
+
},
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
@settings_app.command(
|
|
780
|
+
"escalation",
|
|
781
|
+
help=(
|
|
782
|
+
"Update escalation settings. Example: applied-cli agent settings escalation "
|
|
783
|
+
"--agent-id <uuid> --escalation-mode after_response --response-delay-seconds 30"
|
|
784
|
+
),
|
|
785
|
+
)
|
|
786
|
+
def settings_escalation(
|
|
787
|
+
agent_id: str = typer.Option(
|
|
788
|
+
..., "--agent-id", "--agent", "--id", help="Target agent UUID."
|
|
789
|
+
),
|
|
790
|
+
escalation_mode: Optional[str] = typer.Option(None, "--escalation-mode"),
|
|
791
|
+
escalation_wait_time_mode: Optional[str] = typer.Option(
|
|
792
|
+
None, "--escalation-wait-time-mode"
|
|
793
|
+
),
|
|
794
|
+
response_delay_in_seconds: Optional[int] = typer.Option(
|
|
795
|
+
None, "--response-delay-seconds"
|
|
796
|
+
),
|
|
797
|
+
escalation_phone_number: Optional[str] = typer.Option(
|
|
798
|
+
None, "--escalation-phone-number"
|
|
799
|
+
),
|
|
800
|
+
base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
|
|
801
|
+
shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
|
|
802
|
+
api_token: Optional[str] = typer.Option(None, help="Applied API token."),
|
|
803
|
+
dry_run: bool = typer.Option(False),
|
|
804
|
+
yes: bool = typer.Option(False, "--yes", "-y"),
|
|
805
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
806
|
+
) -> None:
|
|
807
|
+
_validate_uuid(agent_id, field_name="agent-id")
|
|
808
|
+
try:
|
|
809
|
+
resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
|
|
810
|
+
base_url=base_url, shop_id=shop_id, api_token=api_token
|
|
811
|
+
)
|
|
812
|
+
current = get_agent(
|
|
813
|
+
base_url=resolved_base_url,
|
|
814
|
+
shop_id=resolved_shop_id,
|
|
815
|
+
api_token=resolved_token,
|
|
816
|
+
agent_id=agent_id,
|
|
817
|
+
)
|
|
818
|
+
except APIError as exc:
|
|
819
|
+
typer.echo(render_api_error(exc, action="read agent for escalation settings"), err=True)
|
|
820
|
+
raise typer.Exit(code=1) from exc
|
|
821
|
+
|
|
822
|
+
payload: dict[str, Any] = {}
|
|
823
|
+
if escalation_mode is not None:
|
|
824
|
+
payload["escalation_mode"] = escalation_mode
|
|
825
|
+
if escalation_wait_time_mode is not None:
|
|
826
|
+
payload["escalation_wait_time_mode"] = escalation_wait_time_mode
|
|
827
|
+
if response_delay_in_seconds is not None:
|
|
828
|
+
payload["response_delay_in_seconds"] = response_delay_in_seconds
|
|
829
|
+
if escalation_phone_number is not None:
|
|
830
|
+
raw_metadata = current.get("metadata")
|
|
831
|
+
metadata: dict[str, Any] = raw_metadata if isinstance(raw_metadata, dict) else {}
|
|
832
|
+
payload["metadata"] = _deep_merge_dict(
|
|
833
|
+
metadata,
|
|
834
|
+
{"escalation_phone_number": escalation_phone_number},
|
|
835
|
+
)
|
|
836
|
+
if not payload:
|
|
837
|
+
raise typer.BadParameter("No update fields provided.")
|
|
838
|
+
|
|
839
|
+
show_target(
|
|
840
|
+
{
|
|
841
|
+
"agent_id": agent_id,
|
|
842
|
+
"fields": ", ".join(sorted(payload.keys())),
|
|
843
|
+
"dry_run": dry_run,
|
|
844
|
+
}
|
|
845
|
+
)
|
|
846
|
+
confirm_or_exit(yes=yes)
|
|
847
|
+
|
|
848
|
+
if dry_run:
|
|
849
|
+
emit_dry_run(
|
|
850
|
+
payload={"agent_id": agent_id, "payload": payload, "dry_run": True},
|
|
851
|
+
output_json=output_json,
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
try:
|
|
855
|
+
updated = update_agent(
|
|
856
|
+
base_url=resolved_base_url,
|
|
857
|
+
shop_id=resolved_shop_id,
|
|
858
|
+
api_token=resolved_token,
|
|
859
|
+
agent_id=agent_id,
|
|
860
|
+
payload=payload,
|
|
861
|
+
)
|
|
862
|
+
except APIError as exc:
|
|
863
|
+
typer.echo(render_api_error(exc, action="update escalation settings"), err=True)
|
|
864
|
+
raise typer.Exit(code=1) from exc
|
|
865
|
+
emit_success(
|
|
866
|
+
output_json=output_json,
|
|
867
|
+
payload=updated,
|
|
868
|
+
fields={"agent_id": updated.get("id")},
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
@settings_app.command(
|
|
873
|
+
"style",
|
|
874
|
+
help=(
|
|
875
|
+
"Merge style metadata settings. Example: applied-cli agent settings style "
|
|
876
|
+
"--agent-id <uuid> --style-json '{\"tone\":\"friendly\"}'"
|
|
877
|
+
),
|
|
878
|
+
)
|
|
879
|
+
def settings_style(
|
|
880
|
+
agent_id: str = typer.Option(
|
|
881
|
+
..., "--agent-id", "--agent", "--id", help="Target agent UUID."
|
|
882
|
+
),
|
|
883
|
+
style_json: str = typer.Option(
|
|
884
|
+
..., "--style-json", help="JSON object merged into metadata.style."
|
|
885
|
+
),
|
|
886
|
+
base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
|
|
887
|
+
shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
|
|
888
|
+
api_token: Optional[str] = typer.Option(None, help="Applied API token."),
|
|
889
|
+
dry_run: bool = typer.Option(False),
|
|
890
|
+
yes: bool = typer.Option(False, "--yes", "-y"),
|
|
891
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
892
|
+
) -> None:
|
|
893
|
+
_validate_uuid(agent_id, field_name="agent-id")
|
|
894
|
+
style_patch = _parse_json_dict(style_json, field_name="style-json")
|
|
895
|
+
if style_patch is None:
|
|
896
|
+
raise typer.BadParameter("style-json is required.")
|
|
897
|
+
try:
|
|
898
|
+
resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
|
|
899
|
+
base_url=base_url, shop_id=shop_id, api_token=api_token
|
|
900
|
+
)
|
|
901
|
+
current = get_agent(
|
|
902
|
+
base_url=resolved_base_url,
|
|
903
|
+
shop_id=resolved_shop_id,
|
|
904
|
+
api_token=resolved_token,
|
|
905
|
+
agent_id=agent_id,
|
|
906
|
+
)
|
|
907
|
+
except APIError as exc:
|
|
908
|
+
typer.echo(render_api_error(exc, action="read agent for style settings"), err=True)
|
|
909
|
+
raise typer.Exit(code=1) from exc
|
|
910
|
+
|
|
911
|
+
raw_metadata = current.get("metadata")
|
|
912
|
+
metadata: dict[str, Any] = raw_metadata if isinstance(raw_metadata, dict) else {}
|
|
913
|
+
raw_style = metadata.get("style")
|
|
914
|
+
style_current: dict[str, Any] = raw_style if isinstance(raw_style, dict) else {}
|
|
915
|
+
merged_style = _deep_merge_dict(style_current, style_patch)
|
|
916
|
+
payload = {"metadata": _deep_merge_dict(metadata, {"style": merged_style})}
|
|
917
|
+
|
|
918
|
+
show_target(
|
|
919
|
+
{
|
|
920
|
+
"agent_id": agent_id,
|
|
921
|
+
"fields": "metadata.style",
|
|
922
|
+
"dry_run": dry_run,
|
|
923
|
+
}
|
|
924
|
+
)
|
|
925
|
+
confirm_or_exit(yes=yes)
|
|
926
|
+
if dry_run:
|
|
927
|
+
emit_dry_run(
|
|
928
|
+
payload={"agent_id": agent_id, "payload": payload, "dry_run": True},
|
|
929
|
+
output_json=output_json,
|
|
930
|
+
)
|
|
931
|
+
try:
|
|
932
|
+
updated = update_agent(
|
|
933
|
+
base_url=resolved_base_url,
|
|
934
|
+
shop_id=resolved_shop_id,
|
|
935
|
+
api_token=resolved_token,
|
|
936
|
+
agent_id=agent_id,
|
|
937
|
+
payload=payload,
|
|
938
|
+
)
|
|
939
|
+
except APIError as exc:
|
|
940
|
+
typer.echo(render_api_error(exc, action="update style settings"), err=True)
|
|
941
|
+
raise typer.Exit(code=1) from exc
|
|
942
|
+
emit_success(
|
|
943
|
+
output_json=output_json,
|
|
944
|
+
payload=updated,
|
|
945
|
+
fields={"agent_id": updated.get("id")},
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
@settings_app.command(
|
|
950
|
+
"chatbot",
|
|
951
|
+
help=(
|
|
952
|
+
"Update chatbot metadata flags. Example: applied-cli agent settings chatbot "
|
|
953
|
+
"--agent-id <uuid> --allow-uploads true --hide-chatbot false"
|
|
954
|
+
),
|
|
955
|
+
)
|
|
956
|
+
def settings_chatbot(
|
|
957
|
+
agent_id: str = typer.Option(
|
|
958
|
+
..., "--agent-id", "--agent", "--id", help="Target agent UUID."
|
|
959
|
+
),
|
|
960
|
+
allow_uploads: Optional[str] = typer.Option(None, "--allow-uploads"),
|
|
961
|
+
hide_chatbot: Optional[str] = typer.Option(None, "--hide-chatbot"),
|
|
962
|
+
prevent_multiple_inbound_webchats: Optional[str] = typer.Option(
|
|
963
|
+
None, "--prevent-multiple-inbound-webchats"
|
|
964
|
+
),
|
|
965
|
+
should_subscribe_to_chatbot_events: Optional[str] = typer.Option(
|
|
966
|
+
None, "--should-subscribe-to-chatbot-events"
|
|
967
|
+
),
|
|
968
|
+
hide_upsell: Optional[str] = typer.Option(None, "--hide-upsell"),
|
|
969
|
+
conversation_hard_close: Optional[str] = typer.Option(None, "--conversation-hard-close"),
|
|
970
|
+
full_size_blank_completion: Optional[str] = typer.Option(
|
|
971
|
+
None, "--full-size-blank-completion"
|
|
972
|
+
),
|
|
973
|
+
always_show_suggestions: Optional[str] = typer.Option(
|
|
974
|
+
None, "--always-show-suggestions"
|
|
975
|
+
),
|
|
976
|
+
feedback_period_in_minutes: Optional[int] = typer.Option(
|
|
977
|
+
None, "--feedback-period-in-minutes"
|
|
978
|
+
),
|
|
979
|
+
base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
|
|
980
|
+
shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
|
|
981
|
+
api_token: Optional[str] = typer.Option(None, help="Applied API token."),
|
|
982
|
+
dry_run: bool = typer.Option(False),
|
|
983
|
+
yes: bool = typer.Option(False, "--yes", "-y"),
|
|
984
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
985
|
+
) -> None:
|
|
986
|
+
_validate_uuid(agent_id, field_name="agent-id")
|
|
987
|
+
bool_updates: dict[str, Optional[bool]] = {
|
|
988
|
+
"allow_uploads": _parse_optional_bool(allow_uploads, field_name="allow-uploads"),
|
|
989
|
+
"hide_chatbot": _parse_optional_bool(hide_chatbot, field_name="hide-chatbot"),
|
|
990
|
+
"prevent_multiple_inbound_webchats": _parse_optional_bool(
|
|
991
|
+
prevent_multiple_inbound_webchats,
|
|
992
|
+
field_name="prevent-multiple-inbound-webchats",
|
|
993
|
+
),
|
|
994
|
+
"should_subscribe_to_chatbot_events": _parse_optional_bool(
|
|
995
|
+
should_subscribe_to_chatbot_events,
|
|
996
|
+
field_name="should-subscribe-to-chatbot-events",
|
|
997
|
+
),
|
|
998
|
+
"hide_upsell": _parse_optional_bool(hide_upsell, field_name="hide-upsell"),
|
|
999
|
+
"conversation_hard_close": _parse_optional_bool(
|
|
1000
|
+
conversation_hard_close, field_name="conversation-hard-close"
|
|
1001
|
+
),
|
|
1002
|
+
"full_size_blank_completion": _parse_optional_bool(
|
|
1003
|
+
full_size_blank_completion, field_name="full-size-blank-completion"
|
|
1004
|
+
),
|
|
1005
|
+
"always_show_suggestions": _parse_optional_bool(
|
|
1006
|
+
always_show_suggestions, field_name="always-show-suggestions"
|
|
1007
|
+
),
|
|
1008
|
+
}
|
|
1009
|
+
patch: dict[str, Any] = {k: v for k, v in bool_updates.items() if v is not None}
|
|
1010
|
+
if feedback_period_in_minutes is not None:
|
|
1011
|
+
patch["feedback_period_in_minutes"] = feedback_period_in_minutes
|
|
1012
|
+
if not patch:
|
|
1013
|
+
raise typer.BadParameter("No update fields provided.")
|
|
1014
|
+
|
|
1015
|
+
try:
|
|
1016
|
+
resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
|
|
1017
|
+
base_url=base_url, shop_id=shop_id, api_token=api_token
|
|
1018
|
+
)
|
|
1019
|
+
current = get_agent(
|
|
1020
|
+
base_url=resolved_base_url,
|
|
1021
|
+
shop_id=resolved_shop_id,
|
|
1022
|
+
api_token=resolved_token,
|
|
1023
|
+
agent_id=agent_id,
|
|
1024
|
+
)
|
|
1025
|
+
except APIError as exc:
|
|
1026
|
+
typer.echo(render_api_error(exc, action="read agent for chatbot settings"), err=True)
|
|
1027
|
+
raise typer.Exit(code=1) from exc
|
|
1028
|
+
raw_metadata = current.get("metadata")
|
|
1029
|
+
metadata: dict[str, Any] = raw_metadata if isinstance(raw_metadata, dict) else {}
|
|
1030
|
+
payload = {"metadata": _deep_merge_dict(metadata, patch)}
|
|
1031
|
+
|
|
1032
|
+
show_target(
|
|
1033
|
+
{
|
|
1034
|
+
"agent_id": agent_id,
|
|
1035
|
+
"fields": f"metadata ({', '.join(sorted(patch.keys()))})",
|
|
1036
|
+
"dry_run": dry_run,
|
|
1037
|
+
}
|
|
1038
|
+
)
|
|
1039
|
+
confirm_or_exit(yes=yes)
|
|
1040
|
+
if dry_run:
|
|
1041
|
+
emit_dry_run(
|
|
1042
|
+
payload={"agent_id": agent_id, "payload": payload, "dry_run": True},
|
|
1043
|
+
output_json=output_json,
|
|
1044
|
+
)
|
|
1045
|
+
try:
|
|
1046
|
+
updated = update_agent(
|
|
1047
|
+
base_url=resolved_base_url,
|
|
1048
|
+
shop_id=resolved_shop_id,
|
|
1049
|
+
api_token=resolved_token,
|
|
1050
|
+
agent_id=agent_id,
|
|
1051
|
+
payload=payload,
|
|
1052
|
+
)
|
|
1053
|
+
except APIError as exc:
|
|
1054
|
+
typer.echo(render_api_error(exc, action="update chatbot settings"), err=True)
|
|
1055
|
+
raise typer.Exit(code=1) from exc
|
|
1056
|
+
emit_success(
|
|
1057
|
+
output_json=output_json,
|
|
1058
|
+
payload=updated,
|
|
1059
|
+
fields={"agent_id": updated.get("id")},
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
@settings_app.command(
|
|
1064
|
+
"guidance",
|
|
1065
|
+
help=(
|
|
1066
|
+
"Update guardrail and suggestions. Example: applied-cli agent settings guidance "
|
|
1067
|
+
"--agent-id <uuid> --guardrail 'Do not provide medical advice.'"
|
|
1068
|
+
),
|
|
1069
|
+
)
|
|
1070
|
+
def settings_guidance(
|
|
1071
|
+
agent_id: str = typer.Option(
|
|
1072
|
+
..., "--agent-id", "--agent", "--id", help="Target agent UUID."
|
|
1073
|
+
),
|
|
1074
|
+
guardrail: Optional[str] = typer.Option(None, "--guardrail"),
|
|
1075
|
+
add_suggestion: list[str] = typer.Option(
|
|
1076
|
+
[], "--add-suggestion", help="Append suggestion text. Repeat option for many."
|
|
1077
|
+
),
|
|
1078
|
+
replace_suggestions_json: Optional[str] = typer.Option(
|
|
1079
|
+
None, "--replace-suggestions-json", help="JSON string array."
|
|
1080
|
+
),
|
|
1081
|
+
base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
|
|
1082
|
+
shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
|
|
1083
|
+
api_token: Optional[str] = typer.Option(None, help="Applied API token."),
|
|
1084
|
+
dry_run: bool = typer.Option(False),
|
|
1085
|
+
yes: bool = typer.Option(False, "--yes", "-y"),
|
|
1086
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
1087
|
+
) -> None:
|
|
1088
|
+
_validate_uuid(agent_id, field_name="agent-id")
|
|
1089
|
+
replace_suggestions: Optional[list[str]] = None
|
|
1090
|
+
if replace_suggestions_json is not None:
|
|
1091
|
+
try:
|
|
1092
|
+
parsed = json.loads(replace_suggestions_json)
|
|
1093
|
+
except json.JSONDecodeError as exc:
|
|
1094
|
+
raise typer.BadParameter("replace-suggestions-json must be valid JSON.") from exc
|
|
1095
|
+
if not isinstance(parsed, list) or not all(isinstance(x, str) for x in parsed):
|
|
1096
|
+
raise typer.BadParameter(
|
|
1097
|
+
"replace-suggestions-json must decode to a JSON string array."
|
|
1098
|
+
)
|
|
1099
|
+
replace_suggestions = [x.strip() for x in parsed if x.strip()]
|
|
1100
|
+
if guardrail is None and not add_suggestion and replace_suggestions is None:
|
|
1101
|
+
raise typer.BadParameter("No update fields provided.")
|
|
1102
|
+
|
|
1103
|
+
try:
|
|
1104
|
+
resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
|
|
1105
|
+
base_url=base_url, shop_id=shop_id, api_token=api_token
|
|
1106
|
+
)
|
|
1107
|
+
current = get_agent(
|
|
1108
|
+
base_url=resolved_base_url,
|
|
1109
|
+
shop_id=resolved_shop_id,
|
|
1110
|
+
api_token=resolved_token,
|
|
1111
|
+
agent_id=agent_id,
|
|
1112
|
+
)
|
|
1113
|
+
except APIError as exc:
|
|
1114
|
+
typer.echo(render_api_error(exc, action="read agent for guidance settings"), err=True)
|
|
1115
|
+
raise typer.Exit(code=1) from exc
|
|
1116
|
+
|
|
1117
|
+
payload: dict[str, Any] = {}
|
|
1118
|
+
if guardrail is not None:
|
|
1119
|
+
payload["guardrail"] = guardrail
|
|
1120
|
+
if replace_suggestions is not None:
|
|
1121
|
+
payload["suggestions"] = replace_suggestions
|
|
1122
|
+
elif add_suggestion:
|
|
1123
|
+
raw_existing = current.get("suggestions")
|
|
1124
|
+
existing: list[Any] = raw_existing if isinstance(raw_existing, list) else []
|
|
1125
|
+
merged = [str(x).strip() for x in existing if str(x).strip()]
|
|
1126
|
+
for item in add_suggestion:
|
|
1127
|
+
normalized = item.strip()
|
|
1128
|
+
if normalized and normalized not in merged:
|
|
1129
|
+
merged.append(normalized)
|
|
1130
|
+
payload["suggestions"] = merged
|
|
1131
|
+
|
|
1132
|
+
show_target(
|
|
1133
|
+
{
|
|
1134
|
+
"agent_id": agent_id,
|
|
1135
|
+
"fields": ", ".join(sorted(payload.keys())),
|
|
1136
|
+
"dry_run": dry_run,
|
|
1137
|
+
}
|
|
1138
|
+
)
|
|
1139
|
+
confirm_or_exit(yes=yes)
|
|
1140
|
+
if dry_run:
|
|
1141
|
+
emit_dry_run(
|
|
1142
|
+
payload={"agent_id": agent_id, "payload": payload, "dry_run": True},
|
|
1143
|
+
output_json=output_json,
|
|
1144
|
+
)
|
|
1145
|
+
try:
|
|
1146
|
+
updated = update_agent(
|
|
1147
|
+
base_url=resolved_base_url,
|
|
1148
|
+
shop_id=resolved_shop_id,
|
|
1149
|
+
api_token=resolved_token,
|
|
1150
|
+
agent_id=agent_id,
|
|
1151
|
+
payload=payload,
|
|
1152
|
+
)
|
|
1153
|
+
except APIError as exc:
|
|
1154
|
+
typer.echo(render_api_error(exc, action="update guidance settings"), err=True)
|
|
1155
|
+
raise typer.Exit(code=1) from exc
|
|
1156
|
+
emit_success(
|
|
1157
|
+
output_json=output_json,
|
|
1158
|
+
payload=updated,
|
|
1159
|
+
fields={"agent_id": updated.get("id")},
|
|
1160
|
+
)
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
@settings_app.command(
|
|
1164
|
+
"reply",
|
|
1165
|
+
help=(
|
|
1166
|
+
"Toggle auto-reply. Example: applied-cli agent settings reply "
|
|
1167
|
+
"--agent-id <uuid> --auto-reply"
|
|
1168
|
+
),
|
|
1169
|
+
)
|
|
1170
|
+
def settings_reply(
|
|
1171
|
+
agent_id: str = typer.Option(
|
|
1172
|
+
..., "--agent-id", "--agent", "--id", help="Target agent UUID."
|
|
1173
|
+
),
|
|
1174
|
+
auto_reply: Optional[bool] = typer.Option(
|
|
1175
|
+
None,
|
|
1176
|
+
"--auto-reply/--no-auto-reply",
|
|
1177
|
+
help="Whether agent automatically replies to incoming messages.",
|
|
1178
|
+
),
|
|
1179
|
+
base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
|
|
1180
|
+
shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
|
|
1181
|
+
api_token: Optional[str] = typer.Option(None, help="Applied API token."),
|
|
1182
|
+
dry_run: bool = typer.Option(False),
|
|
1183
|
+
yes: bool = typer.Option(False, "--yes", "-y"),
|
|
1184
|
+
output_json: bool = typer.Option(False, "--json"),
|
|
1185
|
+
) -> None:
|
|
1186
|
+
_validate_uuid(agent_id, field_name="agent-id")
|
|
1187
|
+
if auto_reply is None:
|
|
1188
|
+
raise typer.BadParameter("Provide either --auto-reply or --no-auto-reply.")
|
|
1189
|
+
try:
|
|
1190
|
+
resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
|
|
1191
|
+
base_url=base_url, shop_id=shop_id, api_token=api_token
|
|
1192
|
+
)
|
|
1193
|
+
except APIError as exc:
|
|
1194
|
+
typer.echo(render_api_error(exc, action="resolve runtime for reply settings"), err=True)
|
|
1195
|
+
raise typer.Exit(code=1) from exc
|
|
1196
|
+
|
|
1197
|
+
payload: dict[str, Any] = {"auto_reply": auto_reply}
|
|
1198
|
+
show_target(
|
|
1199
|
+
{
|
|
1200
|
+
"agent_id": agent_id,
|
|
1201
|
+
"fields": f"auto_reply={auto_reply}",
|
|
1202
|
+
"dry_run": dry_run,
|
|
1203
|
+
}
|
|
1204
|
+
)
|
|
1205
|
+
confirm_or_exit(yes=yes)
|
|
1206
|
+
|
|
1207
|
+
if dry_run:
|
|
1208
|
+
emit_dry_run(
|
|
1209
|
+
payload={"agent_id": agent_id, "payload": payload, "dry_run": True},
|
|
1210
|
+
output_json=output_json,
|
|
1211
|
+
)
|
|
1212
|
+
|
|
1213
|
+
try:
|
|
1214
|
+
updated = update_agent(
|
|
1215
|
+
base_url=resolved_base_url,
|
|
1216
|
+
shop_id=resolved_shop_id,
|
|
1217
|
+
api_token=resolved_token,
|
|
1218
|
+
agent_id=agent_id,
|
|
1219
|
+
payload=payload,
|
|
1220
|
+
)
|
|
1221
|
+
except APIError as exc:
|
|
1222
|
+
typer.echo(render_api_error(exc, action="update reply settings"), err=True)
|
|
1223
|
+
raise typer.Exit(code=1) from exc
|
|
1224
|
+
emit_success(
|
|
1225
|
+
output_json=output_json,
|
|
1226
|
+
payload=updated,
|
|
1227
|
+
fields={"agent_id": updated.get("id")},
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
app.add_typer(settings_app, name="settings")
|