applied-cli 0.5.71__tar.gz → 0.5.72__tar.gz

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.
Files changed (29) hide show
  1. {applied_cli-0.5.71 → applied_cli-0.5.72}/PKG-INFO +11 -1
  2. {applied_cli-0.5.71 → applied_cli-0.5.72}/README.md +10 -0
  3. {applied_cli-0.5.71 → applied_cli-0.5.72}/applied_cli/cli.py +285 -2
  4. {applied_cli-0.5.71 → applied_cli-0.5.72}/applied_cli/client.py +34 -0
  5. {applied_cli-0.5.71 → applied_cli-0.5.72}/applied_cli/tools.py +850 -5
  6. {applied_cli-0.5.71 → applied_cli-0.5.72}/applied_cli.egg-info/PKG-INFO +11 -1
  7. {applied_cli-0.5.71 → applied_cli-0.5.72}/applied_cli.egg-info/SOURCES.txt +2 -1
  8. {applied_cli-0.5.71 → applied_cli-0.5.72}/pyproject.toml +1 -1
  9. applied_cli-0.5.72/tests/test_cli.py +564 -0
  10. {applied_cli-0.5.71 → applied_cli-0.5.72}/tests/test_client.py +148 -0
  11. applied_cli-0.5.72/tests/test_knowledge_content_tools.py +383 -0
  12. applied_cli-0.5.71/tests/test_cli.py +0 -96
  13. {applied_cli-0.5.71 → applied_cli-0.5.72}/applied_cli/__init__.py +0 -0
  14. {applied_cli-0.5.71 → applied_cli-0.5.72}/applied_cli/agent_scoped_flows.py +0 -0
  15. {applied_cli-0.5.71 → applied_cli-0.5.72}/applied_cli/conversation_lookup.py +0 -0
  16. {applied_cli-0.5.71 → applied_cli-0.5.72}/applied_cli/conversations.py +0 -0
  17. {applied_cli-0.5.71 → applied_cli-0.5.72}/applied_cli/credentials.py +0 -0
  18. {applied_cli-0.5.71 → applied_cli-0.5.72}/applied_cli/flow_helpers.py +0 -0
  19. {applied_cli-0.5.71 → applied_cli-0.5.72}/applied_cli/formatters.py +0 -0
  20. {applied_cli-0.5.71 → applied_cli-0.5.72}/applied_cli.egg-info/dependency_links.txt +0 -0
  21. {applied_cli-0.5.71 → applied_cli-0.5.72}/applied_cli.egg-info/entry_points.txt +0 -0
  22. {applied_cli-0.5.71 → applied_cli-0.5.72}/applied_cli.egg-info/requires.txt +0 -0
  23. {applied_cli-0.5.71 → applied_cli-0.5.72}/applied_cli.egg-info/top_level.txt +0 -0
  24. {applied_cli-0.5.71 → applied_cli-0.5.72}/setup.cfg +0 -0
  25. {applied_cli-0.5.71 → applied_cli-0.5.72}/tests/test_agent_scoped_flows.py +0 -0
  26. {applied_cli-0.5.71 → applied_cli-0.5.72}/tests/test_audit_tools.py +0 -0
  27. {applied_cli-0.5.71 → applied_cli-0.5.72}/tests/test_benchmark_scenario_tools.py +0 -0
  28. {applied_cli-0.5.71 → applied_cli-0.5.72}/tests/test_conversation_tools.py +0 -0
  29. {applied_cli-0.5.71 → applied_cli-0.5.72}/tests/test_flow_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: applied-cli
3
- Version: 0.5.71
3
+ Version: 0.5.72
4
4
  Summary: CLI and shared client library for Applied Labs AI support agents
5
5
  Author: Applied Labs
6
6
  License-Expression: MIT
@@ -53,6 +53,16 @@ applied tickets --status open
53
53
 
54
54
  # Knowledge base
55
55
  applied knowledge --type qa --search "refund"
56
+ applied knowledge <id> --format json
57
+ applied knowledge --source "https://help.example.com" --format csv
58
+ applied knowledge-diff --source "https://help.example.com"
59
+ applied content list --source "https://help.example.com"
60
+ applied content-update <content_id> --text "Corrected source text"
61
+ applied content-resync <content_id> --wait
62
+ applied knowledge-pin <id>
63
+ applied knowledge-link <response_id> --content <content_id>
64
+ applied knowledge-protect <id>
65
+ applied knowledge-unprotect <id>
56
66
 
57
67
  # Taxonomy
58
68
  applied taxonomy --type topics
@@ -28,6 +28,16 @@ applied tickets --status open
28
28
 
29
29
  # Knowledge base
30
30
  applied knowledge --type qa --search "refund"
31
+ applied knowledge <id> --format json
32
+ applied knowledge --source "https://help.example.com" --format csv
33
+ applied knowledge-diff --source "https://help.example.com"
34
+ applied content list --source "https://help.example.com"
35
+ applied content-update <content_id> --text "Corrected source text"
36
+ applied content-resync <content_id> --wait
37
+ applied knowledge-pin <id>
38
+ applied knowledge-link <response_id> --content <content_id>
39
+ applied knowledge-protect <id>
40
+ applied knowledge-unprotect <id>
31
41
 
32
42
  # Taxonomy
33
43
  applied taxonomy --type topics
@@ -22,6 +22,8 @@ app = typer.Typer(
22
22
  help="Applied Labs CLI - manage your AI agents and conversations.",
23
23
  no_args_is_help=True,
24
24
  )
25
+ content_app = typer.Typer(help="Inspect synced content items.")
26
+ app.add_typer(content_app, name="content")
25
27
 
26
28
  DEFAULT_BASE_URL = "https://api.appliedlabs.ai"
27
29
  DEFAULT_CLIENT_URL = "https://appliedlabs.ai"
@@ -938,27 +940,174 @@ def ticket_update(
938
940
 
939
941
  @app.command()
940
942
  def knowledge(
943
+ id: str = typer.Argument(None, help="Knowledge item ID to fetch"),
941
944
  type: str = typer.Option(
942
945
  None, "--type", "-t", help="Filter by type: qa, context, escalate, exact"
943
946
  ),
944
947
  search: str = typer.Option(None, "--search", "-s", help="Search query"),
948
+ linked_to: str = typer.Option(
949
+ None, "--linked-to", help="Filter by linked content ID"
950
+ ),
951
+ source: str = typer.Option(
952
+ None, "--source", help="Filter by linked content source URL/domain"
953
+ ),
954
+ has_content: bool | None = typer.Option(
955
+ None,
956
+ "--has-content/--no-content",
957
+ help="Filter content-linked vs standalone manual entries",
958
+ ),
945
959
  limit: int = typer.Option(100, "--limit", "-l", help="Results per page"),
946
960
  all_pages: bool = typer.Option(
947
961
  True, "--all/--no-all", help="Fetch all pages (default: True)"
948
962
  ),
963
+ full: bool = typer.Option(False, "--full", help="Include full audit fields in CSV"),
949
964
  format: str = typer.Option(
950
965
  "csv", "--format", "-f", help="Output format: csv or json"
951
966
  ),
967
+ shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
952
968
  ) -> None:
953
- """List knowledge base items."""
954
- client = get_client()
969
+ """List knowledge base items or fetch one item by ID."""
970
+ client = get_client(shop_id=shop_id)
971
+ if id:
972
+ item_format = "json" if format == "json" else "text"
973
+ result = asyncio.run(tools.knowledge_get(client, id, output_format=item_format))
974
+ typer.echo(result)
975
+ return
976
+
955
977
  result = asyncio.run(
956
978
  tools.knowledge_list(
957
979
  client,
958
980
  kb_type=type,
959
981
  search=search,
982
+ linked_to=linked_to,
983
+ source=source,
984
+ has_content=has_content,
960
985
  limit=limit,
961
986
  fetch_all=all_pages,
987
+ full=full,
988
+ output_format=format,
989
+ )
990
+ )
991
+ typer.echo(result)
992
+
993
+
994
+ @app.command("knowledge-diff")
995
+ def knowledge_diff(
996
+ source: str = typer.Option(..., "--source", help="Source URL/domain to audit"),
997
+ limit: int = typer.Option(100, "--limit", "-l", help="Results per API page"),
998
+ format: str = typer.Option(
999
+ "text", "--format", "-f", help="Output format: text or json"
1000
+ ),
1001
+ shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
1002
+ ) -> None:
1003
+ """Diff content-linked knowledge against current source pages."""
1004
+ client = get_client(shop_id=shop_id)
1005
+ result = asyncio.run(
1006
+ tools.knowledge_diff(
1007
+ client,
1008
+ source=source,
1009
+ limit=limit,
1010
+ output_format=format,
1011
+ )
1012
+ )
1013
+ typer.echo(result)
1014
+
1015
+
1016
+ # -----------------------------------------------------------------------------
1017
+ # Content commands
1018
+ # -----------------------------------------------------------------------------
1019
+
1020
+
1021
+ @content_app.command("list")
1022
+ def content_list_cmd(
1023
+ type: str = typer.Option(
1024
+ None, "--type", "-t", help="Filter by type: Article, Product, Document, Site"
1025
+ ),
1026
+ status: str = typer.Option(None, "--status", "-s", help="Filter by status"),
1027
+ search: str = typer.Option(None, "--search", help="Search query"),
1028
+ source: str = typer.Option(None, "--source", help="Filter by source URL/domain"),
1029
+ limit: int = typer.Option(100, "--limit", "-l", help="Results per page"),
1030
+ all_pages: bool = typer.Option(
1031
+ True, "--all/--no-all", help="Fetch all pages (default: True)"
1032
+ ),
1033
+ full: bool = typer.Option(False, "--full", help="Include sync audit fields in CSV"),
1034
+ format: str = typer.Option(
1035
+ "csv", "--format", "-f", help="Output format: csv or json"
1036
+ ),
1037
+ shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
1038
+ ) -> None:
1039
+ """List content items imported into the knowledge system."""
1040
+ client = get_client(shop_id=shop_id)
1041
+ result = asyncio.run(
1042
+ tools.content_list(
1043
+ client,
1044
+ content_type=type,
1045
+ status=status,
1046
+ search=search,
1047
+ source=source,
1048
+ limit=limit,
1049
+ fetch_all=all_pages,
1050
+ full=full,
1051
+ output_format=format,
1052
+ )
1053
+ )
1054
+ typer.echo(result)
1055
+
1056
+
1057
+ @app.command("content-resync")
1058
+ def content_resync(
1059
+ id: str = typer.Argument(..., help="Content item ID"),
1060
+ wait: bool = typer.Option(
1061
+ False, "--wait", help="Poll until the content item is no longer syncing"
1062
+ ),
1063
+ poll_interval: float = typer.Option(
1064
+ 2.0, "--poll-interval", help="Seconds between --wait polls"
1065
+ ),
1066
+ timeout: float = typer.Option(
1067
+ 300.0, "--timeout", help="Maximum seconds to wait for sync completion"
1068
+ ),
1069
+ format: str = typer.Option(
1070
+ "text", "--format", "-f", help="Output format: text or json"
1071
+ ),
1072
+ shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
1073
+ ) -> None:
1074
+ """Trigger a resync for one content item."""
1075
+ client = get_client(shop_id=shop_id)
1076
+ result = asyncio.run(
1077
+ tools.content_resync(
1078
+ client,
1079
+ id,
1080
+ wait=wait,
1081
+ poll_interval=poll_interval,
1082
+ timeout=timeout,
1083
+ output_format=format,
1084
+ )
1085
+ )
1086
+ typer.echo(result)
1087
+
1088
+
1089
+ @app.command("content-update")
1090
+ def content_update(
1091
+ id: str = typer.Argument(..., help="Content item ID"),
1092
+ text: str = typer.Option(None, "--text", help="Override source text"),
1093
+ title: str = typer.Option(None, "--title", help="Content title"),
1094
+ description: str = typer.Option(None, "--description", help="Content description"),
1095
+ status: str = typer.Option(None, "--status", help="Content status"),
1096
+ format: str = typer.Option(
1097
+ "text", "--format", "-f", help="Output format: text or json"
1098
+ ),
1099
+ shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
1100
+ ) -> None:
1101
+ """Update a content item before resyncing linked responses."""
1102
+ client = get_client(shop_id=shop_id)
1103
+ result = asyncio.run(
1104
+ tools.content_update(
1105
+ client,
1106
+ id,
1107
+ text=text,
1108
+ title=title,
1109
+ description=description,
1110
+ status=status,
962
1111
  output_format=format,
963
1112
  )
964
1113
  )
@@ -1923,6 +2072,9 @@ def knowledge_create(
1923
2072
  ),
1924
2073
  question: str = typer.Option(..., "--question", "-q", help="Trigger text / question"),
1925
2074
  answer: str = typer.Option(..., "--answer", "-a", help="Response text / answer"),
2075
+ chunk_text: str = typer.Option(
2076
+ None, "--chunk-text", help="Raw retrieval chunk text"
2077
+ ),
1926
2078
  guardrail: str = typer.Option("", "--guardrail", help="Guardrail instructions"),
1927
2079
  active: bool = typer.Option(True, "--active/--inactive", help="Whether the item is active"),
1928
2080
  agent_ids: str = typer.Option(
@@ -1941,6 +2093,7 @@ def knowledge_create(
1941
2093
  kb_type=kb_type,
1942
2094
  question=question,
1943
2095
  answer=answer,
2096
+ chunk_text=chunk_text,
1944
2097
  guardrail=guardrail,
1945
2098
  active=active,
1946
2099
  agent_ids=parsed_agent_ids,
@@ -1963,6 +2116,11 @@ def knowledge_update(
1963
2116
  ),
1964
2117
  label_id: str = typer.Option(None, "--label-id", help="Topic label UUID"),
1965
2118
  flow_id: str = typer.Option(None, "--flow-id", help="Flow UUID to associate with"),
2119
+ pin: bool = typer.Option(
2120
+ False,
2121
+ "--pin",
2122
+ help="Mark the response as manually managed while updating",
2123
+ ),
1966
2124
  shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
1967
2125
  ) -> None:
1968
2126
  """Update a knowledge base item."""
@@ -1982,6 +2140,95 @@ def knowledge_update(
1982
2140
  agent_ids=parsed_agent_ids,
1983
2141
  label_id=label_id,
1984
2142
  flow_id=flow_id,
2143
+ pin=pin,
2144
+ )
2145
+ )
2146
+ typer.echo(result)
2147
+
2148
+
2149
+ @app.command("knowledge-pin")
2150
+ def knowledge_pin(
2151
+ id: str = typer.Argument(..., help="Knowledge item ID"),
2152
+ unpin: bool = typer.Option(False, "--unpin", help="Remove the pin/manual flag"),
2153
+ format: str = typer.Option(
2154
+ "text", "--format", "-f", help="Output format: text or json"
2155
+ ),
2156
+ shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
2157
+ ) -> None:
2158
+ """Pin a knowledge item so manual edits can survive content resyncs."""
2159
+ client = get_client(shop_id=shop_id)
2160
+ result = asyncio.run(
2161
+ tools.knowledge_pin(
2162
+ client,
2163
+ id,
2164
+ pinned=not unpin,
2165
+ output_format=format,
2166
+ )
2167
+ )
2168
+ typer.echo(result)
2169
+
2170
+
2171
+ @app.command("knowledge-link")
2172
+ def knowledge_link(
2173
+ id: str = typer.Argument(..., help="Knowledge item ID"),
2174
+ content_id: str = typer.Option(
2175
+ ..., "--content", help="Content item ID to link to"
2176
+ ),
2177
+ format: str = typer.Option(
2178
+ "text", "--format", "-f", help="Output format: text or json"
2179
+ ),
2180
+ shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
2181
+ ) -> None:
2182
+ """Link a knowledge item to a content item so sync can manage it."""
2183
+ client = get_client(shop_id=shop_id)
2184
+ result = asyncio.run(
2185
+ tools.knowledge_link(
2186
+ client,
2187
+ id,
2188
+ content_id=content_id,
2189
+ output_format=format,
2190
+ )
2191
+ )
2192
+ typer.echo(result)
2193
+
2194
+
2195
+ @app.command("knowledge-protect")
2196
+ def knowledge_protect(
2197
+ id: str = typer.Argument(..., help="Knowledge item ID"),
2198
+ format: str = typer.Option(
2199
+ "text", "--format", "-f", help="Output format: text or json"
2200
+ ),
2201
+ shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
2202
+ ) -> None:
2203
+ """Disable autogeneration on the linked content for a knowledge item."""
2204
+ client = get_client(shop_id=shop_id)
2205
+ result = asyncio.run(
2206
+ tools.knowledge_protect(
2207
+ client,
2208
+ id,
2209
+ protected=True,
2210
+ output_format=format,
2211
+ )
2212
+ )
2213
+ typer.echo(result)
2214
+
2215
+
2216
+ @app.command("knowledge-unprotect")
2217
+ def knowledge_unprotect(
2218
+ id: str = typer.Argument(..., help="Knowledge item ID"),
2219
+ format: str = typer.Option(
2220
+ "text", "--format", "-f", help="Output format: text or json"
2221
+ ),
2222
+ shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
2223
+ ) -> None:
2224
+ """Re-enable autogeneration on the linked content for a knowledge item."""
2225
+ client = get_client(shop_id=shop_id)
2226
+ result = asyncio.run(
2227
+ tools.knowledge_protect(
2228
+ client,
2229
+ id,
2230
+ protected=False,
2231
+ output_format=format,
1985
2232
  )
1986
2233
  )
1987
2234
  typer.echo(result)
@@ -2226,6 +2473,42 @@ def product_update(
2226
2473
  typer.echo(result)
2227
2474
 
2228
2475
 
2476
+ @app.command("agent-deploy")
2477
+ def agent_deploy(
2478
+ agent_ids: list[str] = typer.Argument(..., help="One or more Agent IDs to deploy"),
2479
+ description: str = typer.Option("", "--description", "-d", help="Optional revision description"),
2480
+ shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
2481
+ ) -> None:
2482
+ """Deploy the current configuration for one or more agents as new live revisions.
2483
+
2484
+ Creates a new revision from each agent's current state and immediately sets it live,
2485
+ so new conversations use the latest changes.
2486
+
2487
+ Pass multiple agent IDs to deploy several agents at once:
2488
+ applied agent-deploy <id1> <id2> <id3> --description "what changed"
2489
+ """
2490
+ client = get_client(shop_id=shop_id)
2491
+
2492
+ async def _deploy_one(agent_id: str):
2493
+ return await client._request(
2494
+ "POST",
2495
+ "/v1/revisions/",
2496
+ body={"agent_id": agent_id, "deploy": True, "description": description},
2497
+ )
2498
+
2499
+ async def _deploy_all():
2500
+ import asyncio as _asyncio
2501
+ return await _asyncio.gather(*[_deploy_one(aid) for aid in agent_ids])
2502
+
2503
+ results = asyncio.run(_deploy_all())
2504
+ for result in results:
2505
+ version = result.get("version", "?")
2506
+ revision_id = result.get("id", "?")
2507
+ is_live = result.get("is_live", False)
2508
+ agent_id = result.get("agent_id", "?")
2509
+ typer.echo(f"Deployed agent {agent_id}: revision v{version} (id={revision_id}, is_live={is_live})")
2510
+
2511
+
2229
2512
  def main() -> None:
2230
2513
  """CLI entrypoint."""
2231
2514
  nested_exit_code = run_agent_scoped_flow_command(sys.argv[1:], get_client)
@@ -1152,6 +1152,9 @@ class AppliedClient:
1152
1152
  self,
1153
1153
  kb_type: str | None = None,
1154
1154
  search: str | None = None,
1155
+ content_id: str | None = None,
1156
+ source: str | None = None,
1157
+ has_content: bool | None = None,
1155
1158
  limit: int = 100,
1156
1159
  fetch_all: bool = False,
1157
1160
  ) -> list[dict]:
@@ -1160,6 +1163,9 @@ class AppliedClient:
1160
1163
  Args:
1161
1164
  kb_type: Filter by type
1162
1165
  search: Search query
1166
+ content_id: Filter by linked content ID
1167
+ source: Filter by linked content source URL
1168
+ has_content: Filter to content-linked or standalone items
1163
1169
  limit: Page size (default 100, max per request)
1164
1170
  fetch_all: If True, paginate through all results
1165
1171
  """
@@ -1168,6 +1174,12 @@ class AppliedClient:
1168
1174
  params["type"] = kb_type
1169
1175
  if search:
1170
1176
  params["search"] = search
1177
+ if content_id:
1178
+ params["content_id"] = content_id
1179
+ if source:
1180
+ params["source"] = source
1181
+ if has_content is not None:
1182
+ params["content__isnull"] = str(not has_content).lower()
1171
1183
 
1172
1184
  if not fetch_all:
1173
1185
  data = await self._request("GET", "/v1/responses/", params=params)
@@ -1223,6 +1235,7 @@ class AppliedClient:
1223
1235
  content_type: str | None = None,
1224
1236
  status: str | None = None,
1225
1237
  search: str | None = None,
1238
+ source: str | None = None,
1226
1239
  limit: int = 100,
1227
1240
  fetch_all: bool = False,
1228
1241
  ) -> list[dict]:
@@ -1232,6 +1245,7 @@ class AppliedClient:
1232
1245
  content_type: Filter by type - 'Article', 'Product', 'Document', 'Site'
1233
1246
  status: Filter by status - 'Published', 'Draft', 'Pending'
1234
1247
  search: Search query
1248
+ source: Filter by source URL/domain
1235
1249
  limit: Page size (default 100)
1236
1250
  fetch_all: If True, paginate through all results
1237
1251
  """
@@ -1242,6 +1256,8 @@ class AppliedClient:
1242
1256
  params["status"] = status
1243
1257
  if search:
1244
1258
  params["search"] = search
1259
+ if source:
1260
+ params["source"] = source
1245
1261
 
1246
1262
  if not fetch_all:
1247
1263
  data = await self._request("GET", "/v1/content/", params=params)
@@ -1344,6 +1360,10 @@ class AppliedClient:
1344
1360
  body=kwargs,
1345
1361
  )
1346
1362
 
1363
+ async def resync_content(self, content_id: str) -> dict:
1364
+ """Trigger a resync for a content item."""
1365
+ return await self._request("POST", f"/v1/content/{content_id}/resync/")
1366
+
1347
1367
  async def delete_content(self, content_id: str) -> None:
1348
1368
  """Delete a content item."""
1349
1369
  await self._request("DELETE", f"/v1/content/{content_id}/")
@@ -2382,6 +2402,7 @@ class AppliedClient:
2382
2402
  kb_type: str,
2383
2403
  question: str,
2384
2404
  answer: str,
2405
+ chunk_text: str | None = None,
2385
2406
  guardrail: str = "",
2386
2407
  active: bool = True,
2387
2408
  agent_ids: list[str] | None = None,
@@ -2394,6 +2415,7 @@ class AppliedClient:
2394
2415
  kb_type: Type - 'qa', 'exact', 'escalate', 'context', 'greeting', 'signature'
2395
2416
  question: Trigger text / question
2396
2417
  answer: Response text / answer
2418
+ chunk_text: Raw retrieval chunk text
2397
2419
  guardrail: Optional guardrail instructions
2398
2420
  active: Whether the item is active (default True)
2399
2421
  agent_ids: Optional list of agent UUIDs to assign to
@@ -2406,6 +2428,8 @@ class AppliedClient:
2406
2428
  "answer": answer,
2407
2429
  "active": active,
2408
2430
  }
2431
+ if chunk_text is not None:
2432
+ body["chunk_text"] = chunk_text
2409
2433
  if guardrail:
2410
2434
  body["guardrail"] = guardrail
2411
2435
  if agent_ids:
@@ -2426,6 +2450,16 @@ class AppliedClient:
2426
2450
  "PATCH", f"/v1/responses/{response_id}/", body=kwargs
2427
2451
  )
2428
2452
 
2453
+ async def pin_response(
2454
+ self,
2455
+ response_id: str,
2456
+ pinned: bool = True,
2457
+ *,
2458
+ field: str = "pinned",
2459
+ ) -> dict:
2460
+ """Mark a knowledge base response as pinned/manually managed."""
2461
+ return await self.update_response(response_id, **{field: pinned})
2462
+
2429
2463
  async def delete_response(self, response_id: str) -> None:
2430
2464
  """Delete a knowledge base item."""
2431
2465
  await self._request("DELETE", f"/v1/responses/{response_id}/")