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.
@@ -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")