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,604 @@
1
+ import json
2
+ from typing import Any, Optional
3
+
4
+ import typer
5
+
6
+ from applied_cli.commands._normalize import (
7
+ normalize_question,
8
+ normalize_response_type,
9
+ )
10
+ from applied_cli.commands._parsers import (
11
+ parse_json_array as _parse_json_array,
12
+ parse_optional_bool as _parse_optional_bool,
13
+ validate_uuid as _validate_uuid,
14
+ )
15
+ from applied_cli.commands._ui import (
16
+ confirm_or_exit,
17
+ emit_dry_run,
18
+ emit_success,
19
+ show_target,
20
+ )
21
+ from applied_cli.error_reporting import render_api_error
22
+ from applied_cli.http import (
23
+ APIError,
24
+ create_response,
25
+ get_response,
26
+ list_responses,
27
+ patch_response,
28
+ )
29
+ from applied_cli.runtime import resolve_runtime
30
+
31
+ app = typer.Typer(help="Manage agent knowledge base: Q&A entries, escalation rules, and context.")
32
+ RESPONSE_SEMANTICS: dict[str, dict[str, Any]] = {
33
+ "escalate": {
34
+ "summary": (
35
+ "Escalation template match. Uses question as escalation criteria; answer controls "
36
+ "direct-escalation vs deflection-first behavior."
37
+ ),
38
+ "match_behavior": (
39
+ "Semantic match on `question` as escalation criteria (not strict keyword equality)."
40
+ ),
41
+ "fields": {
42
+ "question": (
43
+ "Escalation trigger criteria (examples: 'are you affiliated with palantir', "
44
+ "'requests about palantir')."
45
+ ),
46
+ "answer": (
47
+ "If empty, system tends toward direct escalation flow. If non-empty, it is "
48
+ "treated as deflection/template guidance first, then escalation on follow-up."
49
+ ),
50
+ "guardrail": (
51
+ "Conditional escalation/deflection instructions used during escalation handling."
52
+ ),
53
+ "active": "Enables/disables this rule.",
54
+ },
55
+ "notes": [
56
+ "Escalation tool invocation still depends on required tool fields being available.",
57
+ "If required escalation fields are missing, assistant asks for missing fields first.",
58
+ "Matched escalation templates are tracked in MessageReference/metadata for attribution.",
59
+ ],
60
+ "examples": [
61
+ "applied-cli knowledge upsert --agent-id <uuid> --type escalation --question \"requests about palantir\" --answer \"I can help with basic questions first. Could you share more details?\" --guardrail \"If user repeats concern, escalate.\"",
62
+ "applied-cli knowledge upsert --agent-id <uuid> --type escalation --question \"legal notice\" --answer \"\"",
63
+ ],
64
+ },
65
+ "exact": {
66
+ "summary": "Template response for semantically equivalent user asks.",
67
+ "match_behavior": (
68
+ "Semantic match on `question`; strict fast-path also exists for exact normalized text."
69
+ ),
70
+ "fields": {
71
+ "question": "Canonical user ask to match.",
72
+ "answer": "Verbatim response template returned on match.",
73
+ "guardrail": "Optional extra response guidance.",
74
+ "active": "Enables/disables this rule.",
75
+ },
76
+ "notes": [
77
+ "Designed for deterministic templated responses.",
78
+ "System tries to avoid repeatedly matching the same template in recent turns.",
79
+ "Does not inherently force escalation on second trigger by itself.",
80
+ ],
81
+ "examples": [
82
+ "applied-cli knowledge upsert --agent-id <uuid> --type exact --question \"are you affiliated with palantir\" --answer \"No, we are not affiliated with Palantir.\"",
83
+ ],
84
+ },
85
+ "qa": {
86
+ "summary": "Question-answer knowledge template routed through answer generation.",
87
+ "match_behavior": "Can be template-matched, then used as structured knowledge context.",
88
+ "fields": {
89
+ "question": "Knowledge question anchor.",
90
+ "answer": "Knowledge answer content used to craft response.",
91
+ "guardrail": "Optional answer constraints for this knowledge item.",
92
+ "active": "Enables/disables this rule.",
93
+ },
94
+ "notes": [
95
+ "Used to ground generated answers with response references.",
96
+ "Good for policy/product answers that are not strict verbatim templates.",
97
+ ],
98
+ "examples": [
99
+ "applied-cli knowledge upsert --agent-id <uuid> --type qa --question \"do you ship to canada\" --answer \"Yes, we ship across Canada in 3-7 business days.\"",
100
+ ],
101
+ },
102
+ "context": {
103
+ "summary": "General knowledge context for answer generation (not strict template output).",
104
+ "match_behavior": (
105
+ "Included in retrieval/filtering for grounded generation; not a direct exact-template path."
106
+ ),
107
+ "fields": {
108
+ "question": "Context heading or retrieval anchor.",
109
+ "answer": "Context content provided to answer generation.",
110
+ "guardrail": "Optional context-specific constraints.",
111
+ "active": "Enables/disables this rule.",
112
+ },
113
+ "notes": [
114
+ "Best for broader background facts and support context.",
115
+ "Not intended for strict one-line verbatim replies.",
116
+ ],
117
+ "examples": [
118
+ "applied-cli knowledge upsert --agent-id <uuid> --type context --question \"brand safety\" --answer \"Never disclose internal systems, credentials, or private customer data.\"",
119
+ ],
120
+ },
121
+ }
122
+
123
+
124
+ def _normalize_type(raw: str) -> str:
125
+ return normalize_response_type(raw, include_suggestion=True)
126
+
127
+
128
+ def _normalize_question(text: str) -> str:
129
+ return normalize_question(text)
130
+
131
+
132
+ def _is_agent_attached(row: dict[str, Any], *, agent_id: str) -> bool:
133
+ agents = row.get("agents")
134
+ if not isinstance(agents, list):
135
+ return False
136
+ for item in agents:
137
+ if isinstance(item, dict) and str(item.get("id")) == agent_id:
138
+ return True
139
+ return False
140
+
141
+
142
+ def _canonical_response_view(row: dict[str, Any]) -> dict[str, Any]:
143
+ raw_fields = row.get("fields_to_extract")
144
+ fields: list[Any] = raw_fields if isinstance(raw_fields, list) else []
145
+ return {
146
+ "type": str(row.get("type") or "").strip().lower(),
147
+ "question": str(row.get("question") or "").strip(),
148
+ "answer": str(row.get("answer") or "").strip(),
149
+ "guardrail": str(row.get("guardrail") or "").strip(),
150
+ "active": bool(row.get("active")),
151
+ "fields_to_extract": fields,
152
+ }
153
+
154
+
155
+ @app.command(
156
+ "semantics",
157
+ help=(
158
+ "Show audited behavior of response types. Example: applied-cli knowledge semantics "
159
+ "--type escalation"
160
+ ),
161
+ )
162
+ def semantics_cmd(
163
+ response_type: Optional[str] = typer.Option(
164
+ None, "--type", help="Optional filter: escalation/exact/qa/context"
165
+ ),
166
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
167
+ ) -> None:
168
+ selected_types: list[str]
169
+ if response_type:
170
+ selected_types = [_normalize_type(response_type)]
171
+ else:
172
+ selected_types = ["escalate", "exact", "qa", "context"]
173
+
174
+ payload = [
175
+ {"type": response_key, **RESPONSE_SEMANTICS[response_key]}
176
+ for response_key in selected_types
177
+ ]
178
+
179
+ if output_json:
180
+ typer.echo(json.dumps(payload, indent=2, default=str))
181
+ return
182
+
183
+ for item in payload:
184
+ typer.echo(f"type={item['type']}")
185
+ typer.echo(f"- summary: {item['summary']}")
186
+ typer.echo(f"- match_behavior: {item['match_behavior']}")
187
+ typer.echo("- fields:")
188
+ fields = item.get("fields") or {}
189
+ if isinstance(fields, dict):
190
+ for field_name, field_desc in fields.items():
191
+ typer.echo(f" - {field_name}: {field_desc}")
192
+ notes = item.get("notes") or []
193
+ if isinstance(notes, list) and notes:
194
+ typer.echo("- notes:")
195
+ for note in notes:
196
+ typer.echo(f" - {note}")
197
+ examples = item.get("examples") or []
198
+ if isinstance(examples, list) and examples:
199
+ typer.echo("- examples:")
200
+ for example in examples:
201
+ typer.echo(f" - {example}")
202
+ typer.echo("")
203
+
204
+
205
+ @app.command(
206
+ "list",
207
+ help=(
208
+ "List response rules. Example: applied-cli knowledge list --agent-id <uuid> "
209
+ "--type qa --active true"
210
+ ),
211
+ )
212
+ def list_cmd(
213
+ agent_id: Optional[str] = typer.Option(
214
+ None, "--agent-id", "--agent", help="Filter by agent UUID."
215
+ ),
216
+ response_type: Optional[str] = typer.Option(None, "--type", help="Filter by response type."),
217
+ active: Optional[str] = typer.Option(None, "--active", help="Filter by active true/false."),
218
+ limit: int = typer.Option(100, "--limit"),
219
+ ordering: str = typer.Option("-updated_at", "--ordering"),
220
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
221
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
222
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
223
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
224
+ ) -> None:
225
+ if agent_id:
226
+ _validate_uuid(agent_id, field_name="agent-id")
227
+ normalized_type = _normalize_type(response_type) if response_type else None
228
+ parsed_active = _parse_optional_bool(active, field_name="active")
229
+ try:
230
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
231
+ base_url=base_url, shop_id=shop_id, api_token=api_token
232
+ )
233
+ rows = list_responses(
234
+ base_url=resolved_base_url,
235
+ shop_id=resolved_shop_id,
236
+ api_token=resolved_token,
237
+ agent_id=agent_id,
238
+ response_type=normalized_type,
239
+ active=parsed_active,
240
+ limit=limit,
241
+ ordering=ordering,
242
+ )
243
+ except APIError as exc:
244
+ typer.echo(render_api_error(exc, action="list responses"), err=True)
245
+ raise typer.Exit(code=1) from exc
246
+
247
+ if output_json:
248
+ typer.echo(json.dumps(rows, indent=2, default=str))
249
+ return
250
+ if not rows:
251
+ typer.echo("No results.")
252
+ return
253
+ for row in rows:
254
+ typer.echo(
255
+ f"id={row.get('id')} | type={row.get('type')} | active={row.get('active')} | created_at={row.get('created_at')} | question={str(row.get('question') or '')[:80]}"
256
+ )
257
+
258
+
259
+ @app.command(
260
+ "describe",
261
+ help="Describe one response rule by UUID. Example: applied-cli knowledge describe --response-id <uuid>",
262
+ )
263
+ @app.command(
264
+ "show",
265
+ help="Show one response rule by UUID. Example: applied-cli knowledge show --response-id <uuid>",
266
+ )
267
+ def show_cmd(
268
+ response_id: str = typer.Option(
269
+ ..., "--response-id", "--response", "--id", help="Target response UUID."
270
+ ),
271
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
272
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
273
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
274
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
275
+ ) -> None:
276
+ _validate_uuid(response_id, field_name="response-id")
277
+ try:
278
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
279
+ base_url=base_url, shop_id=shop_id, api_token=api_token
280
+ )
281
+ row = get_response(
282
+ base_url=resolved_base_url,
283
+ shop_id=resolved_shop_id,
284
+ api_token=resolved_token,
285
+ response_id=response_id,
286
+ )
287
+ except APIError as exc:
288
+ typer.echo(render_api_error(exc, action="show response"), err=True)
289
+ if exc.status_code == 404:
290
+ typer.echo(
291
+ "Hint: run `applied-cli knowledge list --agent-id <uuid>` to find a valid `response_id`.",
292
+ err=True,
293
+ )
294
+ raise typer.Exit(code=1) from exc
295
+
296
+ if output_json:
297
+ typer.echo(json.dumps(row, indent=2, default=str))
298
+ return
299
+ typer.echo(f"id={row.get('id')}")
300
+ typer.echo(f"type={row.get('type')}")
301
+ typer.echo(f"active={row.get('active')}")
302
+ typer.echo(f"created_at={row.get('created_at')}")
303
+ typer.echo(f"updated_at={row.get('updated_at')}")
304
+ typer.echo(f"question={row.get('question')}")
305
+ typer.echo(f"answer={row.get('answer')}")
306
+
307
+
308
+ @app.command(
309
+ "create",
310
+ help=(
311
+ "Create a response rule. Example: applied-cli knowledge create --agent-id <uuid> "
312
+ "--type qa --question 'Do you ship to Canada?' --answer 'Yes, we do.'"
313
+ ),
314
+ )
315
+ def create_cmd(
316
+ agent_id: str = typer.Option(..., "--agent-id", "--agent", help="Agent UUID."),
317
+ response_type: str = typer.Option(..., "--type", help="escalation/exact/qa/context"),
318
+ question: str = typer.Option(..., "--question"),
319
+ answer: str = typer.Option(..., "--answer"),
320
+ guardrail: Optional[str] = typer.Option(None, "--guardrail"),
321
+ active: bool = typer.Option(True, "--active/--inactive"),
322
+ fields_to_extract_json: Optional[str] = typer.Option(None, "--fields-to-extract-json"),
323
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
324
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
325
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
326
+ dry_run: bool = typer.Option(False, help="Preview payload without writes."),
327
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
328
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
329
+ ) -> None:
330
+ _validate_uuid(agent_id, field_name="agent-id")
331
+ normalized_type = _normalize_type(response_type)
332
+ fields_to_extract = _parse_json_array(
333
+ fields_to_extract_json, field_name="fields-to-extract-json"
334
+ )
335
+ payload: dict[str, Any] = {
336
+ "agent_ids": [agent_id],
337
+ "type": normalized_type,
338
+ "question": question.strip(),
339
+ "answer": answer.strip(),
340
+ "active": active,
341
+ }
342
+ if guardrail is not None:
343
+ payload["guardrail"] = guardrail
344
+ if fields_to_extract is not None:
345
+ payload["fields_to_extract"] = fields_to_extract
346
+ try:
347
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
348
+ base_url=base_url, shop_id=shop_id, api_token=api_token
349
+ )
350
+ show_target(
351
+ {
352
+ "base_url": resolved_base_url,
353
+ "shop_id": resolved_shop_id,
354
+ "agent_id": agent_id,
355
+ "type": normalized_type,
356
+ "dry_run": dry_run,
357
+ }
358
+ )
359
+ confirm_or_exit(yes=yes)
360
+ if dry_run:
361
+ emit_dry_run(payload={"payload": payload, "dry_run": True}, output_json=output_json)
362
+ created = create_response(
363
+ base_url=resolved_base_url,
364
+ shop_id=resolved_shop_id,
365
+ api_token=resolved_token,
366
+ payload=payload,
367
+ )
368
+ except APIError as exc:
369
+ typer.echo(render_api_error(exc, action="create response"), err=True)
370
+ raise typer.Exit(code=1) from exc
371
+ emit_success(
372
+ output_json=output_json,
373
+ payload=created,
374
+ fields={"response_id": created.get("id")},
375
+ )
376
+
377
+
378
+ @app.command(
379
+ "upsert",
380
+ help=(
381
+ "Create or update one rule idempotently. Example: applied-cli knowledge upsert "
382
+ "--agent-id <uuid> --type escalation --question 'I need a human' --answer 'Escalating now.'"
383
+ ),
384
+ )
385
+ def upsert_cmd(
386
+ agent_id: str = typer.Option(..., "--agent-id", "--agent", help="Agent UUID."),
387
+ response_type: str = typer.Option(..., "--type", help="escalation/exact/qa/context"),
388
+ question: str = typer.Option(..., "--question"),
389
+ answer: str = typer.Option(..., "--answer"),
390
+ guardrail: Optional[str] = typer.Option(None, "--guardrail"),
391
+ active: bool = typer.Option(True, "--active/--inactive"),
392
+ fields_to_extract_json: Optional[str] = typer.Option(None, "--fields-to-extract-json"),
393
+ dry_run: bool = typer.Option(False, help="Preview without writes."),
394
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
395
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
396
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
397
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
398
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
399
+ ) -> None:
400
+ _validate_uuid(agent_id, field_name="agent-id")
401
+ normalized_type = _normalize_type(response_type)
402
+ normalized_question = _normalize_question(question)
403
+ fields_to_extract = _parse_json_array(
404
+ fields_to_extract_json, field_name="fields-to-extract-json"
405
+ )
406
+ payload: dict[str, Any] = {
407
+ "agent_ids": [agent_id],
408
+ "type": normalized_type,
409
+ "question": question.strip(),
410
+ "answer": answer.strip(),
411
+ "active": active,
412
+ }
413
+ if guardrail is not None:
414
+ payload["guardrail"] = guardrail
415
+ if fields_to_extract is not None:
416
+ payload["fields_to_extract"] = fields_to_extract
417
+ try:
418
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
419
+ base_url=base_url, shop_id=shop_id, api_token=api_token
420
+ )
421
+ show_target(
422
+ {
423
+ "base_url": resolved_base_url,
424
+ "shop_id": resolved_shop_id,
425
+ "agent_id": agent_id,
426
+ "type": normalized_type,
427
+ "dry_run": dry_run,
428
+ }
429
+ )
430
+ confirm_or_exit(yes=yes)
431
+ candidates = list_responses(
432
+ base_url=resolved_base_url,
433
+ shop_id=resolved_shop_id,
434
+ api_token=resolved_token,
435
+ agent_id=agent_id,
436
+ response_type=normalized_type,
437
+ limit=500,
438
+ )
439
+ except APIError as exc:
440
+ typer.echo(render_api_error(exc, action="lookup response for upsert"), err=True)
441
+ raise typer.Exit(code=1) from exc
442
+
443
+ matched: Optional[dict[str, Any]] = None
444
+ for row in candidates:
445
+ if not _is_agent_attached(row, agent_id=agent_id):
446
+ continue
447
+ row_question = _normalize_question(str(row.get("question") or ""))
448
+ if row_question == normalized_question:
449
+ matched = row
450
+ break
451
+
452
+ created_ids: list[str] = []
453
+ updated_ids: list[str] = []
454
+ unchanged_ids: list[str] = []
455
+ action = "created"
456
+ try:
457
+ if matched is None:
458
+ if not dry_run:
459
+ created = create_response(
460
+ base_url=resolved_base_url,
461
+ shop_id=resolved_shop_id,
462
+ api_token=resolved_token,
463
+ payload=payload,
464
+ )
465
+ created_ids.append(str(created.get("id")))
466
+ else:
467
+ created_ids.append("(planned)")
468
+ action = "created"
469
+ else:
470
+ current_reduced = _canonical_response_view(matched)
471
+ target_reduced = _canonical_response_view(payload)
472
+ if current_reduced == target_reduced:
473
+ unchanged_ids.append(str(matched.get("id")))
474
+ action = "unchanged"
475
+ else:
476
+ if not dry_run:
477
+ updated = patch_response(
478
+ base_url=resolved_base_url,
479
+ shop_id=resolved_shop_id,
480
+ api_token=resolved_token,
481
+ response_id=str(matched.get("id")),
482
+ payload=payload,
483
+ )
484
+ updated_ids.append(str(updated.get("id")))
485
+ else:
486
+ updated_ids.append(str(matched.get("id")))
487
+ action = "updated"
488
+ except APIError as exc:
489
+ typer.echo(render_api_error(exc, action="write response upsert"), err=True)
490
+ if exc.status_code == 404:
491
+ typer.echo(
492
+ "Hint: if you meant to edit an existing rule, run `applied-cli knowledge list --agent-id <uuid>` and use that `response_id`.",
493
+ err=True,
494
+ )
495
+ raise typer.Exit(code=1) from exc
496
+
497
+ summary = {
498
+ "action": action,
499
+ "created_count": 0 if action != "created" else 1,
500
+ "updated_count": 0 if action != "updated" else 1,
501
+ "unchanged_count": 0 if action != "unchanged" else 1,
502
+ "created_ids": created_ids,
503
+ "updated_ids": updated_ids,
504
+ "unchanged_ids": unchanged_ids,
505
+ "dry_run": dry_run,
506
+ }
507
+ if output_json:
508
+ typer.echo(json.dumps(summary, indent=2, default=str))
509
+ else:
510
+ typer.echo(
511
+ f"action={summary['action']} | created={summary['created_count']} | updated={summary['updated_count']} | unchanged={summary['unchanged_count']}"
512
+ )
513
+
514
+
515
+ @app.command(
516
+ "update",
517
+ help=(
518
+ "Update an existing response rule. Example: applied-cli knowledge update "
519
+ "--response-id <uuid> --answer 'Updated answer text'"
520
+ ),
521
+ )
522
+ def update_cmd(
523
+ response_id: str = typer.Option(
524
+ ..., "--response-id", "--response", "--id", help="Response UUID."
525
+ ),
526
+ response_type: Optional[str] = typer.Option(None, "--type"),
527
+ question: Optional[str] = typer.Option(None, "--question"),
528
+ answer: Optional[str] = typer.Option(None, "--answer"),
529
+ guardrail: Optional[str] = typer.Option(None, "--guardrail"),
530
+ active: Optional[str] = typer.Option(None, "--active", help="Set active true/false."),
531
+ agent_id: Optional[str] = typer.Option(
532
+ None, "--agent-id", "--agent", help="Optional single-agent binding UUID."
533
+ ),
534
+ fields_to_extract_json: Optional[str] = typer.Option(None, "--fields-to-extract-json"),
535
+ base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
536
+ shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
537
+ api_token: Optional[str] = typer.Option(None, help="Applied API token."),
538
+ dry_run: bool = typer.Option(False, help="Preview payload without writes."),
539
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
540
+ output_json: bool = typer.Option(False, "--json", help="Emit JSON output."),
541
+ ) -> None:
542
+ _validate_uuid(response_id, field_name="response-id")
543
+ if agent_id:
544
+ _validate_uuid(agent_id, field_name="agent-id")
545
+ fields_to_extract = _parse_json_array(
546
+ fields_to_extract_json, field_name="fields-to-extract-json"
547
+ )
548
+ payload: dict[str, Any] = {}
549
+ if response_type is not None:
550
+ payload["type"] = _normalize_type(response_type)
551
+ if question is not None:
552
+ payload["question"] = question.strip()
553
+ if answer is not None:
554
+ payload["answer"] = answer.strip()
555
+ if guardrail is not None:
556
+ payload["guardrail"] = guardrail
557
+ parsed_active = _parse_optional_bool(active, field_name="active")
558
+ if parsed_active is not None:
559
+ payload["active"] = parsed_active
560
+ if agent_id is not None:
561
+ payload["agent_ids"] = [agent_id]
562
+ if fields_to_extract is not None:
563
+ payload["fields_to_extract"] = fields_to_extract
564
+ if not payload:
565
+ raise typer.BadParameter("No fields provided. Pass at least one update option.")
566
+ try:
567
+ resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
568
+ base_url=base_url, shop_id=shop_id, api_token=api_token
569
+ )
570
+ show_target(
571
+ {
572
+ "base_url": resolved_base_url,
573
+ "shop_id": resolved_shop_id,
574
+ "response_id": response_id,
575
+ "fields": ", ".join(sorted(payload.keys())),
576
+ "dry_run": dry_run,
577
+ }
578
+ )
579
+ confirm_or_exit(yes=yes)
580
+ if dry_run:
581
+ emit_dry_run(
582
+ payload={"response_id": response_id, "payload": payload, "dry_run": True},
583
+ output_json=output_json,
584
+ )
585
+ updated = patch_response(
586
+ base_url=resolved_base_url,
587
+ shop_id=resolved_shop_id,
588
+ api_token=resolved_token,
589
+ response_id=response_id,
590
+ payload=payload,
591
+ )
592
+ except APIError as exc:
593
+ typer.echo(render_api_error(exc, action="update response"), err=True)
594
+ if exc.status_code == 404:
595
+ typer.echo(
596
+ "Hint: run `applied-cli knowledge list --agent-id <uuid>` to find a valid `response_id`.",
597
+ err=True,
598
+ )
599
+ raise typer.Exit(code=1) from exc
600
+ emit_success(
601
+ output_json=output_json,
602
+ payload=updated,
603
+ fields={"response_id": updated.get("id")},
604
+ )