assistant-runtime-sdk 1.5.0__tar.gz → 1.7.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.5.0
3
+ Version: 1.7.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.5.0"
56
+ __version__ = "1.7.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
  # =========================================================================
@@ -1669,11 +1719,12 @@ class AsyncAssistantRuntimeClient(BaseAssistantRuntimeClient):
1669
1719
  category: Optional[str] = None,
1670
1720
  conversation_id: Optional[str] = None,
1671
1721
  environment: Optional[dict] = None,
1672
- attachment_ids: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
1673
- """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}."""
1674
1725
  endpoint, payload = self._prepare_create_ticket(
1675
1726
  user_id, subject, description, category, conversation_id, environment,
1676
- attachment_ids)
1727
+ attachment_ids, conversation_transcript)
1677
1728
  return await self._request_post_json(endpoint, payload)
1678
1729
 
1679
1730
  async def submit_feedback(self, user_id: str, rating: Optional[int] = None,
@@ -1686,6 +1737,18 @@ class AsyncAssistantRuntimeClient(BaseAssistantRuntimeClient):
1686
1737
  user_id, rating, comment, category, conversation_id, environment)
1687
1738
  return await self._request_post_json(endpoint, payload)
1688
1739
 
1740
+ async def download_ticket_attachment(self, user_id: str, ticket_id: str,
1741
+ file_url: str) -> tuple:
1742
+ """Download one attachment from a ticket the user owns.
1743
+
1744
+ Returns:
1745
+ (content_bytes, content_type, filename) tuple.
1746
+ """
1747
+ endpoint, params = self._prepare_download_ticket_attachment(
1748
+ user_id, ticket_id, file_url
1749
+ )
1750
+ return await self._request_get_raw(endpoint, params)
1751
+
1689
1752
  async def list_tickets(self, user_id: str, status: Optional[str] = None) -> Optional[Dict[str, Any]]:
1690
1753
  """List the user's support tickets. Returns {tickets: [...]}."""
1691
1754
  endpoint, payload = self._prepare_list_tickets(user_id, status)
@@ -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
  # =========================================================================
@@ -1802,7 +1859,8 @@ class BaseAssistantRuntimeClient:
1802
1859
  category: Optional[str] = None,
1803
1860
  conversation_id: Optional[str] = None,
1804
1861
  environment: Optional[dict] = None,
1805
- attachment_ids: Optional[List[str]] = None) -> tuple:
1862
+ attachment_ids: Optional[List[str]] = None,
1863
+ conversation_transcript: Optional[str] = None) -> tuple:
1806
1864
  """Returns (endpoint, payload)."""
1807
1865
  payload: Dict[str, Any] = {
1808
1866
  "tenant_id": self.tenant_id,
@@ -1818,6 +1876,8 @@ class BaseAssistantRuntimeClient:
1818
1876
  payload["environment"] = environment
1819
1877
  if attachment_ids:
1820
1878
  payload["attachment_ids"] = attachment_ids
1879
+ if conversation_transcript:
1880
+ payload["conversation_transcript"] = conversation_transcript
1821
1881
  return "support.create_ticket", payload
1822
1882
 
1823
1883
  def _prepare_submit_feedback(self, user_id: str, rating: Optional[int] = None,
@@ -1864,6 +1924,16 @@ class BaseAssistantRuntimeClient:
1864
1924
  "ticket_id": ticket_id,
1865
1925
  }
1866
1926
 
1927
+ def _prepare_download_ticket_attachment(self, user_id: str, ticket_id: str,
1928
+ file_url: str) -> tuple:
1929
+ """Returns (endpoint, params)."""
1930
+ return "support.download_ticket_attachment", {
1931
+ "tenant_id": self.tenant_id,
1932
+ "user_id": user_id,
1933
+ "ticket_id": ticket_id,
1934
+ "file_url": file_url,
1935
+ }
1936
+
1867
1937
  def _prepare_reply_to_ticket(self, user_id: str, ticket_id: str, message: str,
1868
1938
  attachment_ids: Optional[List[str]] = None) -> tuple:
1869
1939
  """Returns (endpoint, payload)."""
@@ -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
  # =========================================================================
@@ -2478,11 +2575,12 @@ class AssistantRuntimeClient(BaseAssistantRuntimeClient):
2478
2575
  category: Optional[str] = None,
2479
2576
  conversation_id: Optional[str] = None,
2480
2577
  environment: Optional[dict] = None,
2481
- attachment_ids: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
2482
- """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}."""
2483
2581
  endpoint, payload = self._prepare_create_ticket(
2484
2582
  user_id, subject, description, category, conversation_id, environment,
2485
- attachment_ids)
2583
+ attachment_ids, conversation_transcript)
2486
2584
  return self._request_post_json(endpoint, payload)
2487
2585
 
2488
2586
  def submit_feedback(self, user_id: str, rating: Optional[int] = None,
@@ -2495,6 +2593,21 @@ class AssistantRuntimeClient(BaseAssistantRuntimeClient):
2495
2593
  user_id, rating, comment, category, conversation_id, environment)
2496
2594
  return self._request_post_json(endpoint, payload)
2497
2595
 
2596
+ def download_ticket_attachment(self, user_id: str, ticket_id: str,
2597
+ file_url: str) -> tuple:
2598
+ """Download one attachment from a ticket the user owns.
2599
+
2600
+ Raw bytes rather than JSON, so a 10 MB screenshot doesn't get
2601
+ base64-inflated through two hops.
2602
+
2603
+ Returns:
2604
+ (content_bytes, content_type, filename) tuple.
2605
+ """
2606
+ endpoint, params = self._prepare_download_ticket_attachment(
2607
+ user_id, ticket_id, file_url
2608
+ )
2609
+ return self._request_get_raw(endpoint, params)
2610
+
2498
2611
  def list_tickets(self, user_id: str, status: Optional[str] = None) -> Optional[Dict[str, Any]]:
2499
2612
  """List the user's support tickets. Returns {tickets: [...]}."""
2500
2613
  endpoint, payload = self._prepare_list_tickets(user_id, status)
@@ -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.5.0"
3
+ version = "1.7.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"}