assistant-runtime-sdk 1.4.0__tar.gz → 1.6.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.4.0
3
+ Version: 1.6.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.4.0"
56
+ __version__ = "1.6.0"
57
57
  __author__ = "Paul Clinton"
58
58
  __license__ = "AGPL-3.0"
59
59
 
@@ -118,6 +118,8 @@ from .types import (
118
118
  StreamStartData,
119
119
  StreamChunkData,
120
120
  StreamCompleteData,
121
+ RoutingReceiptData,
122
+ ModelSelectedData,
121
123
  ModelFallbackData,
122
124
  RateLimitedData,
123
125
  ToolCallStartData,
@@ -198,6 +200,8 @@ __all__ = [
198
200
  "StreamStartData",
199
201
  "StreamChunkData",
200
202
  "StreamCompleteData",
203
+ "RoutingReceiptData",
204
+ "ModelSelectedData",
201
205
  "ModelFallbackData",
202
206
  "RateLimitedData",
203
207
  "ToolCallStartData",
@@ -792,6 +792,56 @@ class AsyncAssistantRuntimeClient(BaseAssistantRuntimeClient):
792
792
  endpoint, payload = self._prepare_set_tool_preference(user_id, tool_name, preference)
793
793
  return await self._request_post_json(endpoint, payload)
794
794
 
795
+ # =========================================================================
796
+ # Routing Preference APIs
797
+ # =========================================================================
798
+
799
+ async def list_routing_preferences(self, user_id: str) -> Optional[Dict[str, Any]]:
800
+ """Async version of AssistantRuntimeClient.list_routing_preferences."""
801
+ endpoint, payload = self._prepare_list_routing_preferences(user_id)
802
+ return await self._request_post_json(endpoint, payload)
803
+
804
+ async def create_routing_preference(
805
+ self,
806
+ user_id: str,
807
+ scope: str,
808
+ match_kind: str,
809
+ match_value: str,
810
+ target_tier: str,
811
+ priority: int = None,
812
+ source_message_id: str = None,
813
+ origin: str = None,
814
+ ) -> Optional[Dict[str, Any]]:
815
+ """Async version of AssistantRuntimeClient.create_routing_preference."""
816
+ endpoint, payload = self._prepare_create_routing_preference(
817
+ user_id, scope, match_kind, match_value, target_tier,
818
+ priority, source_message_id, origin)
819
+ return await self._request_post_json(endpoint, payload)
820
+
821
+ async def set_routing_preference_status(
822
+ self, user_id: str, preference_id: str, status: str,
823
+ ) -> Optional[Dict[str, Any]]:
824
+ """Async version of AssistantRuntimeClient.set_routing_preference_status."""
825
+ endpoint, payload = self._prepare_set_routing_preference_status(
826
+ user_id, preference_id, status)
827
+ return await self._request_post_json(endpoint, payload)
828
+
829
+ async def delete_routing_preference(
830
+ self, user_id: str, preference_id: str,
831
+ ) -> Optional[Dict[str, Any]]:
832
+ """Async version of AssistantRuntimeClient.delete_routing_preference."""
833
+ endpoint, payload = self._prepare_delete_routing_preference(
834
+ user_id, preference_id)
835
+ return await self._request_post_json(endpoint, payload)
836
+
837
+ async def forecast_routing_preference(
838
+ self, user_id: str, match_kind: str, match_value: str, target_tier: str,
839
+ ) -> Optional[Dict[str, Any]]:
840
+ """Async version of AssistantRuntimeClient.forecast_routing_preference."""
841
+ endpoint, payload = self._prepare_forecast_routing_preference(
842
+ user_id, match_kind, match_value, target_tier)
843
+ return await self._request_post_json(endpoint, payload)
844
+
795
845
  # =========================================================================
796
846
  # Billing & Subscription APIs
797
847
  # =========================================================================
@@ -1489,10 +1539,11 @@ class AsyncAssistantRuntimeClient(BaseAssistantRuntimeClient):
1489
1539
  input_data: Dict[str, Any],
1490
1540
  user_id: str,
1491
1541
  trigger_id: str,
1542
+ workflow_docname: Optional[str] = None,
1492
1543
  ) -> Optional[Dict[str, Any]]:
1493
1544
  """Async version of AssistantRuntimeClient.execute_workflow_from_event."""
1494
1545
  endpoint, payload = self._prepare_execute_workflow_from_event(
1495
- workflow_name, input_data, user_id, trigger_id,
1546
+ workflow_name, input_data, user_id, trigger_id, workflow_docname,
1496
1547
  )
1497
1548
  return await self._request_post_json(endpoint, payload, api_base=self.workflows_api_base)
1498
1549
 
@@ -1668,11 +1719,12 @@ class AsyncAssistantRuntimeClient(BaseAssistantRuntimeClient):
1668
1719
  category: Optional[str] = None,
1669
1720
  conversation_id: Optional[str] = None,
1670
1721
  environment: Optional[dict] = None,
1671
- attachment_ids: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
1672
- """Raise a support ticket. Returns {ticket_id, portal_link}."""
1722
+ attachment_ids: Optional[List[str]] = None,
1723
+ conversation_transcript: Optional[str] = None) -> Optional[Dict[str, Any]]:
1724
+ """Raise a support ticket. Returns {ticket_id}."""
1673
1725
  endpoint, payload = self._prepare_create_ticket(
1674
1726
  user_id, subject, description, category, conversation_id, environment,
1675
- attachment_ids)
1727
+ attachment_ids, conversation_transcript)
1676
1728
  return await self._request_post_json(endpoint, payload)
1677
1729
 
1678
1730
  async def submit_feedback(self, user_id: str, rating: Optional[int] = None,
@@ -846,6 +846,63 @@ class BaseAssistantRuntimeClient:
846
846
  "preference": preference,
847
847
  }
848
848
 
849
+ # =========================================================================
850
+ # Prepare Methods — Routing Preferences
851
+ # =========================================================================
852
+
853
+ def _prepare_list_routing_preferences(self, user_id: str) -> tuple:
854
+ return "routing_preferences.list_routing_preferences", {
855
+ "tenant_id": self.tenant_id,
856
+ "user_id": user_id,
857
+ }
858
+
859
+ def _prepare_create_routing_preference(
860
+ self, user_id: str, scope: str, match_kind: str, match_value: str,
861
+ target_tier: str, priority: int = None, source_message_id: str = None,
862
+ origin: str = None,
863
+ ) -> tuple:
864
+ return "routing_preferences.create_routing_preference", {
865
+ "tenant_id": self.tenant_id,
866
+ "user_id": user_id,
867
+ "scope": scope,
868
+ "match_kind": match_kind,
869
+ "match_value": match_value,
870
+ "target_tier": target_tier,
871
+ "priority": priority,
872
+ "source_message_id": source_message_id,
873
+ "origin": origin,
874
+ }
875
+
876
+ def _prepare_set_routing_preference_status(
877
+ self, user_id: str, preference_id: str, status: str,
878
+ ) -> tuple:
879
+ return "routing_preferences.set_routing_preference_status", {
880
+ "tenant_id": self.tenant_id,
881
+ "user_id": user_id,
882
+ "preference_id": preference_id,
883
+ "status": status,
884
+ }
885
+
886
+ def _prepare_delete_routing_preference(
887
+ self, user_id: str, preference_id: str,
888
+ ) -> tuple:
889
+ return "routing_preferences.delete_routing_preference", {
890
+ "tenant_id": self.tenant_id,
891
+ "user_id": user_id,
892
+ "preference_id": preference_id,
893
+ }
894
+
895
+ def _prepare_forecast_routing_preference(
896
+ self, user_id: str, match_kind: str, match_value: str, target_tier: str,
897
+ ) -> tuple:
898
+ return "routing_preferences.forecast_routing_preference", {
899
+ "tenant_id": self.tenant_id,
900
+ "user_id": user_id,
901
+ "match_kind": match_kind,
902
+ "match_value": match_value,
903
+ "target_tier": target_tier,
904
+ }
905
+
849
906
  # =========================================================================
850
907
  # Prepare Methods — Billing
851
908
  # =========================================================================
@@ -1617,14 +1674,20 @@ class BaseAssistantRuntimeClient:
1617
1674
  input_data: Dict[str, Any],
1618
1675
  user_id: str,
1619
1676
  trigger_id: str,
1677
+ workflow_docname: Optional[str] = None,
1620
1678
  ) -> tuple:
1621
- return "workflows.execute_from_event", {
1679
+ payload = {
1622
1680
  "tenant_id": self.tenant_id,
1623
1681
  "workflow_name": workflow_name,
1624
1682
  "input_data": input_data,
1625
1683
  "user_id": user_id,
1626
1684
  "trigger_id": trigger_id,
1627
1685
  }
1686
+ # The docname is the stable binding; workflow_name is a mutable display
1687
+ # label a rename can invalidate. Both travel so an older AR keeps working.
1688
+ if workflow_docname:
1689
+ payload["workflow_docname"] = workflow_docname
1690
+ return "workflows.execute_from_event", payload
1628
1691
 
1629
1692
  def _prepare_cancel_workflow_run(self, run_name: str) -> tuple:
1630
1693
  return "workflows.cancel_run", {
@@ -1796,7 +1859,8 @@ class BaseAssistantRuntimeClient:
1796
1859
  category: Optional[str] = None,
1797
1860
  conversation_id: Optional[str] = None,
1798
1861
  environment: Optional[dict] = None,
1799
- attachment_ids: Optional[List[str]] = None) -> tuple:
1862
+ attachment_ids: Optional[List[str]] = None,
1863
+ conversation_transcript: Optional[str] = None) -> tuple:
1800
1864
  """Returns (endpoint, payload)."""
1801
1865
  payload: Dict[str, Any] = {
1802
1866
  "tenant_id": self.tenant_id,
@@ -1812,6 +1876,8 @@ class BaseAssistantRuntimeClient:
1812
1876
  payload["environment"] = environment
1813
1877
  if attachment_ids:
1814
1878
  payload["attachment_ids"] = attachment_ids
1879
+ if conversation_transcript:
1880
+ payload["conversation_transcript"] = conversation_transcript
1815
1881
  return "support.create_ticket", payload
1816
1882
 
1817
1883
  def _prepare_submit_feedback(self, user_id: str, rating: Optional[int] = None,
@@ -1119,6 +1119,103 @@ class AssistantRuntimeClient(BaseAssistantRuntimeClient):
1119
1119
  endpoint, payload = self._prepare_set_tool_preference(user_id, tool_name, preference)
1120
1120
  return self._request_post_json(endpoint, payload)
1121
1121
 
1122
+ # =========================================================================
1123
+ # Routing Preference APIs
1124
+ # =========================================================================
1125
+ #
1126
+ # Every one of these is POST, reads included: a rule's match_value is text
1127
+ # the user wrote, and a GET would put it in the query string and from
1128
+ # there into access logs.
1129
+
1130
+ def list_routing_preferences(self, user_id: str) -> Optional[Dict[str, Any]]:
1131
+ """List this member's routing rules and their workspace's.
1132
+
1133
+ Args:
1134
+ user_id: User identifier
1135
+
1136
+ Returns:
1137
+ {"mine": [...], "team": [...], "mode": "off"|"shadow"|"on",
1138
+ "can_manage_team": bool}. A team rule's `match_value` is None
1139
+ unless the caller wrote it or administers the workspace.
1140
+ """
1141
+ endpoint, payload = self._prepare_list_routing_preferences(user_id)
1142
+ return self._request_post_json(endpoint, payload)
1143
+
1144
+ def create_routing_preference(
1145
+ self,
1146
+ user_id: str,
1147
+ scope: str,
1148
+ match_kind: str,
1149
+ match_value: str,
1150
+ target_tier: str,
1151
+ priority: int = None,
1152
+ source_message_id: str = None,
1153
+ origin: str = None,
1154
+ ) -> Optional[Dict[str, Any]]:
1155
+ """Create a routing rule. Workspace scope is admin-capable only.
1156
+
1157
+ Args:
1158
+ user_id: User identifier — the recorded author
1159
+ scope: "Tenant" (workspace) or "User" (personal, not yet open)
1160
+ match_kind: "doctype", "keyword" or "task_type"
1161
+ match_value: what to match on
1162
+ target_tier: "Economy", "Standard" or "Premium"
1163
+ priority: lower runs first; defaults to 100
1164
+ source_message_id: opaque id of the message this came from
1165
+ origin: "panel", "settings" or "admin"
1166
+
1167
+ Returns:
1168
+ {"preference_id": ..., "direction": ..., "status": ...}
1169
+ """
1170
+ endpoint, payload = self._prepare_create_routing_preference(
1171
+ user_id, scope, match_kind, match_value, target_tier,
1172
+ priority, source_message_id, origin)
1173
+ return self._request_post_json(endpoint, payload)
1174
+
1175
+ def set_routing_preference_status(
1176
+ self, user_id: str, preference_id: str, status: str,
1177
+ ) -> Optional[Dict[str, Any]]:
1178
+ """Suspend or re-enable a rule.
1179
+
1180
+ Args:
1181
+ user_id: User identifier
1182
+ preference_id: the rule
1183
+ status: "active" or "suspended"
1184
+
1185
+ Returns:
1186
+ {"preference_id": ..., "status": ...}
1187
+ """
1188
+ endpoint, payload = self._prepare_set_routing_preference_status(
1189
+ user_id, preference_id, status)
1190
+ return self._request_post_json(endpoint, payload)
1191
+
1192
+ def delete_routing_preference(
1193
+ self, user_id: str, preference_id: str,
1194
+ ) -> Optional[Dict[str, Any]]:
1195
+ """Remove a rule outright.
1196
+
1197
+ Returns:
1198
+ {"preference_id": ..., "deleted": True}
1199
+ """
1200
+ endpoint, payload = self._prepare_delete_routing_preference(
1201
+ user_id, preference_id)
1202
+ return self._request_post_json(endpoint, payload)
1203
+
1204
+ def forecast_routing_preference(
1205
+ self, user_id: str, match_kind: str, match_value: str, target_tier: str,
1206
+ ) -> Optional[Dict[str, Any]]:
1207
+ """What a rule would cost, before saving it.
1208
+
1209
+ Returns:
1210
+ {"direction": ..., "cost_multiplier": float|None,
1211
+ "rates_reviewed": bool, "requires_confirmation": bool,
1212
+ "can_manage_team": bool}. A null multiplier means the rates are
1213
+ not configured — it never means parity.
1214
+ """
1215
+ endpoint, payload = self._prepare_forecast_routing_preference(
1216
+ user_id, match_kind, match_value, target_tier)
1217
+ return self._request_post_json(endpoint, payload)
1218
+
1122
1219
  # =========================================================================
1123
1220
  # Billing & Subscription APIs
1124
1221
  # =========================================================================
@@ -2193,21 +2290,24 @@ class AssistantRuntimeClient(BaseAssistantRuntimeClient):
2193
2290
  input_data: Dict[str, Any],
2194
2291
  user_id: str,
2195
2292
  trigger_id: str,
2293
+ workflow_docname: Optional[str] = None,
2196
2294
  ) -> Optional[Dict[str, Any]]:
2197
2295
  """
2198
- Trigger workflow execution from a FACO-side doc event.
2296
+ Trigger workflow execution from a FAC-side doc event.
2199
2297
 
2200
2298
  Args:
2201
- workflow_name: AR Workflow.workflow_name (human identifier)
2299
+ workflow_name: AR Workflow.workflow_name (mutable display label)
2202
2300
  input_data: Structured payload {trigger, doc, changed_fields}
2203
2301
  user_id: User on the customer bench who saved the doc
2204
- trigger_id: FACO AR Workflow Trigger name (for traceability)
2302
+ trigger_id: FAC Workflow Trigger name (for traceability)
2303
+ workflow_docname: AR Workflow docname (WF-#####). Preferred — a
2304
+ rename cannot invalidate it. Falls back to workflow_name.
2205
2305
 
2206
2306
  Returns:
2207
2307
  {"status": "queued", "run_name": str, "run_id": str, ...}
2208
2308
  """
2209
2309
  endpoint, payload = self._prepare_execute_workflow_from_event(
2210
- workflow_name, input_data, user_id, trigger_id,
2310
+ workflow_name, input_data, user_id, trigger_id, workflow_docname,
2211
2311
  )
2212
2312
  return self._request_post_json(endpoint, payload, api_base=self.workflows_api_base)
2213
2313
 
@@ -2475,11 +2575,12 @@ class AssistantRuntimeClient(BaseAssistantRuntimeClient):
2475
2575
  category: Optional[str] = None,
2476
2576
  conversation_id: Optional[str] = None,
2477
2577
  environment: Optional[dict] = None,
2478
- attachment_ids: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
2479
- """Raise a support ticket. Returns {ticket_id, portal_link}."""
2578
+ attachment_ids: Optional[List[str]] = None,
2579
+ conversation_transcript: Optional[str] = None) -> Optional[Dict[str, Any]]:
2580
+ """Raise a support ticket. Returns {ticket_id}."""
2480
2581
  endpoint, payload = self._prepare_create_ticket(
2481
2582
  user_id, subject, description, category, conversation_id, environment,
2482
- attachment_ids)
2583
+ attachment_ids, conversation_transcript)
2483
2584
  return self._request_post_json(endpoint, payload)
2484
2585
 
2485
2586
  def submit_feedback(self, user_id: str, rating: Optional[int] = None,
@@ -18,10 +18,12 @@ class SSEEventType(str, Enum):
18
18
  STREAM_START = "stream_start"
19
19
  STREAM_CHUNK = "stream_chunk"
20
20
  STREAM_COMPLETE = "stream_complete"
21
+ STREAM_CANCELLED = "stream_cancelled"
21
22
  STREAM_ERROR = "stream_error"
22
23
 
23
24
  # AI response events
24
25
  THINKING = "thinking"
26
+ THINKING_COMPLETE = "thinking_complete"
25
27
 
26
28
  # Tool execution events
27
29
  TOOL_CALL_START = "tool_call_start"
@@ -32,7 +34,18 @@ class SSEEventType(str, Enum):
32
34
  TOOL_CANCELLED = "tool_cancelled"
33
35
 
34
36
  # Auto-model events
37
+ MODEL_SELECTED = "model_selected"
38
+ # Deprecated: AR has never emitted `model_fallback`. Kept because this is a
39
+ # published API; use MODEL_SELECTED.
35
40
  MODEL_FALLBACK = "model_fallback"
41
+ ROUTING_NOTICE = "routing_notice"
42
+
43
+ # Retrieval, planning and workflow events
44
+ SOURCES = "sources"
45
+ PLAN_COMPLETE = "plan_complete"
46
+ TASK_UPDATED = "task_updated"
47
+ WORKFLOW_CREATED = "workflow_created"
48
+ CONTEXT_SUMMARIZED = "context_summarized"
36
49
 
37
50
  # Rate limiting events
38
51
  RATE_LIMITED = "rate_limited"
@@ -77,6 +77,41 @@ class StreamChunkData(TypedDict):
77
77
  chunk_index: Optional[int]
78
78
 
79
79
 
80
+ class RoutingReceiptData(TypedDict):
81
+ """Why a given model answered a turn (`stream_complete.routing`).
82
+
83
+ Closed reason codes, never sentences — the client owns the wording. The
84
+ server cannot put a price, a credit rate or a review timestamp in here.
85
+ `preference` / `preference_source` are reserved; they stay null until
86
+ routing preferences ship.
87
+ """
88
+
89
+ v: int
90
+ mode: str # auto | explicit
91
+ incomplete: bool # true on the live emit, false at completion
92
+ selected_model: Optional[str]
93
+ selected_model_name: Optional[str] # resolved server-side for catalogue-less clients
94
+ selected_tier: Optional[str]
95
+ fallback_from: Optional[str]
96
+ fallback_from_name: Optional[str]
97
+ classification: Optional[dict] # complexity, task_type, source, floor_applied
98
+ floor: Optional[dict] # tier, reasons[]
99
+ ceiling: Optional[dict] # tier, source, reasons[]
100
+ bound_by: Optional[str] # floor | ceiling | neither
101
+ target_tier: Optional[str]
102
+ band: Optional[str] # only for an admin-capable viewer
103
+ band_disclosed: str # full | capacity_managed
104
+ shortlist_size: int
105
+ pick_reason: str
106
+ notices: List[str]
107
+ thinking: dict # requested, applied, effort, not_applied_reason
108
+ credits: dict # actual
109
+ cycles: int
110
+ also_ran: List[str]
111
+ preference: Optional[dict]
112
+ preference_source: str
113
+
114
+
80
115
  class StreamCompleteData(TypedDict):
81
116
  """Data from stream_complete event."""
82
117
 
@@ -86,16 +121,25 @@ class StreamCompleteData(TypedDict):
86
121
  model_id: str
87
122
  session_id: str
88
123
  message_id: str
124
+ credits_used: Optional[float]
125
+ model_breakdown: Optional[dict]
126
+ routing: Optional[RoutingReceiptData]
89
127
 
90
128
 
91
- class ModelFallbackData(TypedDict):
92
- """Data from model_fallback event (auto-mode)."""
129
+ class ModelSelectedData(TypedDict):
130
+ """Data from the `model_selected` event (auto-mode)."""
93
131
 
94
- original: str
132
+ original: Optional[str]
95
133
  selected: str
96
134
  provider: str
97
135
  tier: str
98
136
  fallback_attempted: bool
137
+ routing: Optional[RoutingReceiptData]
138
+
139
+
140
+ # Deprecated: named for a `model_fallback` event that has never existed on the
141
+ # wire. Kept as an alias because this is a published API.
142
+ ModelFallbackData = ModelSelectedData
99
143
 
100
144
 
101
145
  class RateLimitedData(TypedDict):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "assistant_runtime_sdk"
3
- version = "1.4.0"
3
+ version = "1.6.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"}