assistant-runtime-sdk 1.7.0__tar.gz → 1.9.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assistant_runtime_sdk
3
- Version: 1.7.0
3
+ Version: 1.9.0
4
4
  Summary: Python SDK for FAC Cloud - the Assistant Runtime AI assistant backend
5
5
  Keywords: frappe,ai,assistant,llm,mcp,anthropic,openai,streaming
6
6
  Author: Paul Clinton
@@ -53,7 +53,7 @@ Standalone Functions:
53
53
 
54
54
  """
55
55
 
56
- __version__ = "1.7.0"
56
+ __version__ = "1.9.0"
57
57
  __author__ = "Paul Clinton"
58
58
  __license__ = "AGPL-3.0"
59
59
 
@@ -826,6 +826,14 @@ class AsyncAssistantRuntimeClient(BaseAssistantRuntimeClient):
826
826
  user_id, preference_id, status)
827
827
  return await self._request_post_json(endpoint, payload)
828
828
 
829
+ async def set_routing_preference_mode(
830
+ self, user_id: str, preference_id: str, rule_mode: str,
831
+ ) -> Optional[Dict[str, Any]]:
832
+ """Async version of AssistantRuntimeClient.set_routing_preference_mode."""
833
+ endpoint, payload = self._prepare_set_routing_preference_mode(
834
+ user_id, preference_id, rule_mode)
835
+ return await self._request_post_json(endpoint, payload)
836
+
829
837
  async def delete_routing_preference(
830
838
  self, user_id: str, preference_id: str,
831
839
  ) -> Optional[Dict[str, Any]]:
@@ -1333,7 +1341,7 @@ class AsyncAssistantRuntimeClient(BaseAssistantRuntimeClient):
1333
1341
  user_id: str,
1334
1342
  server_name: str,
1335
1343
  endpoint_url: str,
1336
- transport_type: str = "SSE",
1344
+ transport_type: str = "HTTP",
1337
1345
  auth_type: str = "OAuth",
1338
1346
  oauth_client_id: Optional[str] = None,
1339
1347
  oauth_client_secret: Optional[str] = None,
@@ -1344,12 +1352,14 @@ class AsyncAssistantRuntimeClient(BaseAssistantRuntimeClient):
1344
1352
  api_key_header: str = "Authorization",
1345
1353
  allowed_tools: Optional[list] = None,
1346
1354
  blocked_tools: Optional[list] = None,
1355
+ managed: bool = False,
1347
1356
  ) -> Dict[str, Any]:
1348
1357
  """Async version of AssistantRuntimeClient.add_user_mcp_server."""
1349
1358
  endpoint, params = self._prepare_add_user_mcp_server(
1350
1359
  user_id, server_name, endpoint_url, transport_type, auth_type,
1351
1360
  oauth_client_id, oauth_client_secret, access_token, refresh_token,
1352
1361
  token_expires_in, api_key, api_key_header, allowed_tools, blocked_tools,
1362
+ managed,
1353
1363
  )
1354
1364
  return await self._request_post_form(endpoint, params)
1355
1365
 
@@ -1359,7 +1369,7 @@ class AsyncAssistantRuntimeClient(BaseAssistantRuntimeClient):
1359
1369
  try:
1360
1370
  return await self._request_get(endpoint, params)
1361
1371
  except Exception as e:
1362
- return {"user_id": user_id, "mcp_servers": [], "error": str(e)}
1372
+ return {"user_id": user_id, "mcp_servers": [], "error": str(e), "_ar_unreachable": True}
1363
1373
 
1364
1374
  async def update_mcp_server_tokens(
1365
1375
  self,
@@ -1378,6 +1388,64 @@ class AsyncAssistantRuntimeClient(BaseAssistantRuntimeClient):
1378
1388
  endpoint, params = self._prepare_remove_user_mcp_server(user_id, server_name)
1379
1389
  return await self._request_delete(endpoint, params)
1380
1390
 
1391
+ async def enable_mcp_server(self, user_id: str, server_name: str, enabled: bool = True) -> Dict[str, Any]:
1392
+ """Async version of AssistantRuntimeClient.enable_mcp_server."""
1393
+ endpoint, params = self._prepare_enable_mcp_server(user_id, server_name, enabled)
1394
+ return await self._request_post_form(endpoint, params)
1395
+
1396
+ async def test_mcp_server(self, user_id: str, server_name: str) -> Dict[str, Any]:
1397
+ """Async version of AssistantRuntimeClient.test_mcp_server."""
1398
+ endpoint, params = self._prepare_test_mcp_server(user_id, server_name)
1399
+ return await self._request_post_form(endpoint, params)
1400
+
1401
+ async def set_mcp_server_tools(
1402
+ self,
1403
+ user_id: str,
1404
+ server_name: str,
1405
+ allowed_tools: Optional[list] = None,
1406
+ blocked_tools: Optional[list] = None,
1407
+ ) -> Dict[str, Any]:
1408
+ """Async version of AssistantRuntimeClient.set_mcp_server_tools."""
1409
+ endpoint, params = self._prepare_set_mcp_server_tools(
1410
+ user_id, server_name, allowed_tools, blocked_tools
1411
+ )
1412
+ return await self._request_post_form(endpoint, params)
1413
+
1414
+ async def begin_mcp_connect(
1415
+ self,
1416
+ endpoint_url: str,
1417
+ user_id: str,
1418
+ client_id: Optional[str] = None,
1419
+ client_secret: Optional[str] = None,
1420
+ ) -> Dict[str, Any]:
1421
+ """Async version of AssistantRuntimeClient.begin_mcp_connect."""
1422
+ endpoint, params = self._prepare_begin_mcp_connect(
1423
+ user_id, endpoint_url, client_id, client_secret
1424
+ )
1425
+ return await self._request_post_form(endpoint, params)
1426
+
1427
+ async def get_mcp_connect_session(self, handle: str, user_id: str) -> Dict[str, Any]:
1428
+ """Async version of AssistantRuntimeClient.get_mcp_connect_session."""
1429
+ endpoint, params = self._prepare_get_mcp_connect_session(user_id, handle)
1430
+ return await self._request_get(endpoint, params)
1431
+
1432
+ async def commit_mcp_connect(
1433
+ self, handle: str, server_name: str, user_id: str
1434
+ ) -> Dict[str, Any]:
1435
+ """Async version of AssistantRuntimeClient.commit_mcp_connect."""
1436
+ endpoint, params = self._prepare_commit_mcp_connect(user_id, handle, server_name)
1437
+ return await self._request_post_form(endpoint, params)
1438
+
1439
+ async def abandon_mcp_connect(self, handle: str, user_id: str) -> Dict[str, Any]:
1440
+ """Async version of AssistantRuntimeClient.abandon_mcp_connect."""
1441
+ endpoint, params = self._prepare_abandon_mcp_connect(user_id, handle)
1442
+ return await self._request_post_form(endpoint, params)
1443
+
1444
+ async def begin_mcp_reauth(self, server_name: str, user_id: str) -> Dict[str, Any]:
1445
+ """Async version of AssistantRuntimeClient.begin_mcp_reauth."""
1446
+ endpoint, params = self._prepare_begin_mcp_reauth(user_id, server_name)
1447
+ return await self._request_post_form(endpoint, params)
1448
+
1381
1449
  async def list_users(
1382
1450
  self,
1383
1451
  status: Optional[str] = None,
@@ -883,6 +883,16 @@ class BaseAssistantRuntimeClient:
883
883
  "status": status,
884
884
  }
885
885
 
886
+ def _prepare_set_routing_preference_mode(
887
+ self, user_id: str, preference_id: str, rule_mode: str,
888
+ ) -> tuple:
889
+ return "routing_preferences.set_routing_preference_mode", {
890
+ "tenant_id": self.tenant_id,
891
+ "user_id": user_id,
892
+ "preference_id": preference_id,
893
+ "rule_mode": rule_mode,
894
+ }
895
+
886
896
  def _prepare_delete_routing_preference(
887
897
  self, user_id: str, preference_id: str,
888
898
  ) -> tuple:
@@ -1427,7 +1437,7 @@ class BaseAssistantRuntimeClient:
1427
1437
 
1428
1438
  def _prepare_add_user_mcp_server(
1429
1439
  self, user_id: str, server_name: str, endpoint_url: str,
1430
- transport_type: str = "SSE", auth_type: str = "OAuth",
1440
+ transport_type: str = "HTTP", auth_type: str = "OAuth",
1431
1441
  oauth_client_id: Optional[str] = None,
1432
1442
  oauth_client_secret: Optional[str] = None,
1433
1443
  access_token: Optional[str] = None,
@@ -1437,6 +1447,7 @@ class BaseAssistantRuntimeClient:
1437
1447
  api_key_header: str = "Authorization",
1438
1448
  allowed_tools: Optional[list] = None,
1439
1449
  blocked_tools: Optional[list] = None,
1450
+ managed: bool = False,
1440
1451
  ) -> tuple:
1441
1452
  params: Dict[str, Any] = {
1442
1453
  "tenant_id": self.tenant_id,
@@ -1464,6 +1475,7 @@ class BaseAssistantRuntimeClient:
1464
1475
  params["allowed_tools"] = json.dumps(allowed_tools)
1465
1476
  if blocked_tools:
1466
1477
  params["blocked_tools"] = json.dumps(blocked_tools)
1478
+ params["managed"] = "1" if managed else "0"
1467
1479
  return "users.add_user_mcp_server", params
1468
1480
 
1469
1481
  def _prepare_get_user_mcp_servers(self, user_id: str) -> tuple:
@@ -1494,6 +1506,94 @@ class BaseAssistantRuntimeClient:
1494
1506
  "server_name": server_name,
1495
1507
  }
1496
1508
 
1509
+ def _prepare_enable_mcp_server(
1510
+ self, user_id: str, server_name: str, enabled: bool = True
1511
+ ) -> tuple:
1512
+ return "users.enable_mcp_server", {
1513
+ "tenant_id": self.tenant_id,
1514
+ "user_id": str(user_id),
1515
+ "server_name": str(server_name),
1516
+ "enabled": "1" if enabled else "0",
1517
+ }
1518
+
1519
+ def _prepare_test_mcp_server(self, user_id: str, server_name: str) -> tuple:
1520
+ return "users.test_mcp_server", {
1521
+ "tenant_id": self.tenant_id,
1522
+ "user_id": str(user_id),
1523
+ "server_name": str(server_name),
1524
+ }
1525
+
1526
+ def _prepare_set_mcp_server_tools(
1527
+ self, user_id: str, server_name: str,
1528
+ allowed_tools: Optional[list] = None,
1529
+ blocked_tools: Optional[list] = None,
1530
+ ) -> tuple:
1531
+ params: Dict[str, Any] = {
1532
+ "tenant_id": self.tenant_id,
1533
+ "user_id": user_id,
1534
+ "server_name": server_name,
1535
+ }
1536
+ if allowed_tools is not None:
1537
+ params["allowed_tools"] = json.dumps(allowed_tools)
1538
+ if blocked_tools is not None:
1539
+ params["blocked_tools"] = json.dumps(blocked_tools)
1540
+ return "users.set_mcp_server_tools", params
1541
+
1542
+ # -------------------------------------------------------------------------
1543
+ # MCP Connect wizard (assistant_runtime.api.mcp_oauth)
1544
+ # -------------------------------------------------------------------------
1545
+
1546
+ def _prepare_begin_mcp_connect(
1547
+ self, user_id: str, endpoint_url: str,
1548
+ client_id: Optional[str] = None,
1549
+ client_secret: Optional[str] = None,
1550
+ ) -> tuple:
1551
+ params: Dict[str, Any] = {
1552
+ "tenant_id": self.tenant_id,
1553
+ "user_id": str(user_id),
1554
+ "endpoint_url": str(endpoint_url),
1555
+ }
1556
+ # Omitted, not blank: a blank client_id on the wire makes AR record an
1557
+ # empty manual registration instead of taking the DCR path.
1558
+ if client_id:
1559
+ params["client_id"] = str(client_id)
1560
+ if client_secret:
1561
+ params["client_secret"] = str(client_secret)
1562
+ return "mcp_oauth.begin_mcp_connect", params
1563
+
1564
+ def _prepare_get_mcp_connect_session(self, user_id: str, handle: str) -> tuple:
1565
+ return "mcp_oauth.get_mcp_connect_session", {
1566
+ "tenant_id": self.tenant_id,
1567
+ "user_id": str(user_id),
1568
+ "handle": str(handle),
1569
+ }
1570
+
1571
+ def _prepare_commit_mcp_connect(
1572
+ self, user_id: str, handle: str, server_name: str
1573
+ ) -> tuple:
1574
+ return "mcp_oauth.commit_mcp_connect", {
1575
+ "tenant_id": self.tenant_id,
1576
+ "user_id": str(user_id),
1577
+ "handle": str(handle),
1578
+ "server_name": str(server_name),
1579
+ }
1580
+
1581
+ def _prepare_abandon_mcp_connect(self, user_id: str, handle: str) -> tuple:
1582
+ return "mcp_oauth.abandon_mcp_connect", {
1583
+ "tenant_id": self.tenant_id,
1584
+ "user_id": str(user_id),
1585
+ "handle": str(handle),
1586
+ }
1587
+
1588
+ def _prepare_begin_mcp_reauth(self, user_id: str, server_name: str) -> tuple:
1589
+ # No endpoint_url: AR reads it from the row named here, and commit
1590
+ # updates that row in place via the session's reauth_target.
1591
+ return "mcp_oauth.begin_mcp_reauth", {
1592
+ "tenant_id": self.tenant_id,
1593
+ "user_id": str(user_id),
1594
+ "server_name": str(server_name),
1595
+ }
1596
+
1497
1597
  def _prepare_list_users(
1498
1598
  self, status: Optional[str] = None, limit: int = 50,
1499
1599
  offset: int = 0, include_mcp_count: bool = True,
@@ -1189,6 +1189,28 @@ class AssistantRuntimeClient(BaseAssistantRuntimeClient):
1189
1189
  user_id, preference_id, status)
1190
1190
  return self._request_post_json(endpoint, payload)
1191
1191
 
1192
+ def set_routing_preference_mode(
1193
+ self, user_id: str, preference_id: str, rule_mode: str,
1194
+ ) -> Optional[Dict[str, Any]]:
1195
+ """Finish a rule's trial, or send it back to one.
1196
+
1197
+ Every rule carries its own mode, so this changes nothing about any
1198
+ other rule. Platform defaults are not reachable here — they are
1199
+ managed in AR Admin.
1200
+
1201
+ Args:
1202
+ user_id: User identifier
1203
+ preference_id: the rule
1204
+ rule_mode: "shadow" (matched and recorded, never applied) or
1205
+ "on" (applied)
1206
+
1207
+ Returns:
1208
+ {"preference_id": ..., "rule_mode": ...}
1209
+ """
1210
+ endpoint, payload = self._prepare_set_routing_preference_mode(
1211
+ user_id, preference_id, rule_mode)
1212
+ return self._request_post_json(endpoint, payload)
1213
+
1192
1214
  def delete_routing_preference(
1193
1215
  self, user_id: str, preference_id: str,
1194
1216
  ) -> Optional[Dict[str, Any]]:
@@ -1999,7 +2021,7 @@ class AssistantRuntimeClient(BaseAssistantRuntimeClient):
1999
2021
  user_id: str,
2000
2022
  server_name: str,
2001
2023
  endpoint_url: str,
2002
- transport_type: str = "SSE",
2024
+ transport_type: str = "HTTP",
2003
2025
  auth_type: str = "OAuth",
2004
2026
  oauth_client_id: Optional[str] = None,
2005
2027
  oauth_client_secret: Optional[str] = None,
@@ -2010,12 +2032,19 @@ class AssistantRuntimeClient(BaseAssistantRuntimeClient):
2010
2032
  api_key_header: str = "Authorization",
2011
2033
  allowed_tools: Optional[list] = None,
2012
2034
  blocked_tools: Optional[list] = None,
2035
+ managed: bool = False,
2013
2036
  ) -> Dict[str, Any]:
2014
- """Add or update an MCP server for a user."""
2037
+ """Add or update an MCP server for a user.
2038
+
2039
+ ``managed`` is sent as a hint only — AR derives the authoritative
2040
+ value server-side from whether server_name/endpoint_url match FAC's
2041
+ real registration shape, regardless of what is passed here.
2042
+ """
2015
2043
  endpoint, params = self._prepare_add_user_mcp_server(
2016
2044
  user_id, server_name, endpoint_url, transport_type, auth_type,
2017
2045
  oauth_client_id, oauth_client_secret, access_token, refresh_token,
2018
2046
  token_expires_in, api_key, api_key_header, allowed_tools, blocked_tools,
2047
+ managed,
2019
2048
  )
2020
2049
  return self._request_post_form(endpoint, params)
2021
2050
 
@@ -2025,7 +2054,7 @@ class AssistantRuntimeClient(BaseAssistantRuntimeClient):
2025
2054
  try:
2026
2055
  return self._request_get(endpoint, params)
2027
2056
  except Exception as e:
2028
- return {"user_id": user_id, "mcp_servers": [], "error": str(e)}
2057
+ return {"user_id": user_id, "mcp_servers": [], "error": str(e), "_ar_unreachable": True}
2029
2058
 
2030
2059
  def update_mcp_server_tokens(
2031
2060
  self,
@@ -2044,6 +2073,113 @@ class AssistantRuntimeClient(BaseAssistantRuntimeClient):
2044
2073
  endpoint, params = self._prepare_remove_user_mcp_server(user_id, server_name)
2045
2074
  return self._request_delete(endpoint, params)
2046
2075
 
2076
+ def enable_mcp_server(self, user_id: str, server_name: str, enabled: bool = True) -> Dict[str, Any]:
2077
+ """Enable or disable an MCP server without deleting it."""
2078
+ endpoint, params = self._prepare_enable_mcp_server(user_id, server_name, enabled)
2079
+ return self._request_post_form(endpoint, params)
2080
+
2081
+ def test_mcp_server(self, user_id: str, server_name: str) -> Dict[str, Any]:
2082
+ """Open a real authenticated MCP session and report what it exposes.
2083
+
2084
+ Unlike a reachability probe, this speaks MCP with the user's stored
2085
+ credentials and lists tools. Returns a result dict rather than
2086
+ raising: {"success": bool, "tool_count": int, "tools": [str], "error": str | None}.
2087
+ """
2088
+ endpoint, params = self._prepare_test_mcp_server(user_id, server_name)
2089
+ return self._request_post_form(endpoint, params)
2090
+
2091
+ def set_mcp_server_tools(
2092
+ self,
2093
+ user_id: str,
2094
+ server_name: str,
2095
+ allowed_tools: Optional[list] = None,
2096
+ blocked_tools: Optional[list] = None,
2097
+ ) -> Dict[str, Any]:
2098
+ """Replace the tool filters on one server, without touching enabled,
2099
+ status, or credentials.
2100
+
2101
+ Unlike ``add_user_mcp_server``, this is a targeted write, not an
2102
+ upsert — safe to call from a checkbox UI without risking a
2103
+ disabled server being silently re-enabled or its stored API key
2104
+ being dropped.
2105
+ """
2106
+ endpoint, params = self._prepare_set_mcp_server_tools(
2107
+ user_id, server_name, allowed_tools, blocked_tools
2108
+ )
2109
+ return self._request_post_form(endpoint, params)
2110
+
2111
+ def begin_mcp_connect(
2112
+ self,
2113
+ endpoint_url: str,
2114
+ user_id: str,
2115
+ client_id: Optional[str] = None,
2116
+ client_secret: Optional[str] = None,
2117
+ ) -> Dict[str, Any]:
2118
+ """Start a server-side MCP connect attempt and run the preflight.
2119
+
2120
+ Returns AR's connect-session payload:
2121
+ ``{"handle", "session", "preflight", "authorize_url", "redirect_uri"}``.
2122
+ ``preflight`` is ``run_preflight``'s dict — ``steps`` (each
2123
+ ``pass|fail|skipped``), ``server_info``, ``auth``. ``redirect_uri`` is
2124
+ the URI a user must register by hand when the authorization server has
2125
+ no Dynamic Client Registration.
2126
+
2127
+ ``client_id``/``client_secret`` are the no-DCR escape hatch: supply them
2128
+ to re-begin with credentials the user registered themselves.
2129
+ """
2130
+ endpoint, params = self._prepare_begin_mcp_connect(
2131
+ user_id, endpoint_url, client_id, client_secret
2132
+ )
2133
+ return self._request_post_form(endpoint, params)
2134
+
2135
+ def get_mcp_connect_session(self, handle: str, user_id: str) -> Dict[str, Any]:
2136
+ """Read an in-flight connect session by its short-lived handle.
2137
+
2138
+ Returns ``{"status", "endpoint_url", "preflight", "capabilities",
2139
+ "authorize_url", "error_message", "server_name",
2140
+ "reauth_server_name"}``. ``status`` is one of ``Preflight``,
2141
+ ``Awaiting Auth``, ``Authorized``, ``Committed``, ``Failed`` — Title
2142
+ Case with spaces, matching ``AR User MCP Server``'s own status Select.
2143
+ ``authorize_url`` is a real URL whenever one has been built, so a user
2144
+ who reloaded mid-flow can still click Connect. ``capabilities`` is
2145
+ populated only once the session reaches ``Authorized``; ``server_name``
2146
+ only once it reaches ``Committed``. ``reauth_server_name`` is the name
2147
+ of the row this session re-authorizes, or ``None`` on the ordinary add
2148
+ path — it is what lets a caller tell the two apart without carrying any
2149
+ state of its own across the authorization round trip.
2150
+ """
2151
+ endpoint, params = self._prepare_get_mcp_connect_session(user_id, handle)
2152
+ return self._request_get(endpoint, params)
2153
+
2154
+ def commit_mcp_connect(
2155
+ self, handle: str, server_name: str, user_id: str
2156
+ ) -> Dict[str, Any]:
2157
+ """Turn an authorized connect session into a real AR User MCP Server."""
2158
+ endpoint, params = self._prepare_commit_mcp_connect(user_id, handle, server_name)
2159
+ return self._request_post_form(endpoint, params)
2160
+
2161
+ def abandon_mcp_connect(self, handle: str, user_id: str) -> Dict[str, Any]:
2162
+ """Drop an in-flight connect session and the credentials escrowed in it."""
2163
+ endpoint, params = self._prepare_abandon_mcp_connect(user_id, handle)
2164
+ return self._request_post_form(endpoint, params)
2165
+
2166
+ def begin_mcp_reauth(self, server_name: str, user_id: str) -> Dict[str, Any]:
2167
+ """Re-authorize a connection that already exists.
2168
+
2169
+ Returns the same payload shape as :meth:`begin_mcp_connect`. AR opens
2170
+ the session with ``reauth_target`` pointing at the named
2171
+ ``AR User MCP Server`` row, so ``commit_mcp_connect`` updates that row's
2172
+ credentials in place and restores it to ``Active`` instead of inserting
2173
+ a second row — which is what made the duplicate-name guard fire on every
2174
+ reconnect before this endpoint existed.
2175
+
2176
+ This is the only correct entry point for reconnecting. Calling
2177
+ ``begin_mcp_connect`` and then committing under the existing name takes
2178
+ the add path and fails.
2179
+ """
2180
+ endpoint, params = self._prepare_begin_mcp_reauth(user_id, server_name)
2181
+ return self._request_post_form(endpoint, params)
2182
+
2047
2183
  def list_users(
2048
2184
  self,
2049
2185
  status: Optional[str] = None,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "assistant_runtime_sdk"
3
- version = "1.7.0"
3
+ version = "1.9.0"
4
4
  description = "Python SDK for FAC Cloud - the Assistant Runtime AI assistant backend"
5
5
  readme = "README.md"
6
6
  license = {text = "AGPL-3.0"}