assistant-runtime-sdk 1.0.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,1989 @@
1
+ # Assistant Runtime SDK - Asynchronous Client
2
+ # Copyright (C) 2025 Paul Clinton
3
+ # AGPL-3.0 License
4
+
5
+ """
6
+ Asynchronous Assistant Runtime client using aiohttp.
7
+
8
+ Requires the 'async' extra: pip install assistant_runtime_sdk[async]
9
+ """
10
+
11
+ import os
12
+ from typing import AsyncGenerator, List, Optional, Dict, Any
13
+
14
+ try:
15
+ import aiohttp
16
+ except ImportError:
17
+ aiohttp = None # type: ignore
18
+
19
+ from .base import BaseAssistantRuntimeClient
20
+ from .exceptions import (
21
+ ARAPIError,
22
+ ARAuthenticationError,
23
+ ARTimeoutError,
24
+ ARConnectionError,
25
+ ARConfigurationError,
26
+ )
27
+
28
+
29
+ class AsyncAssistantRuntimeClient(BaseAssistantRuntimeClient):
30
+ """
31
+ Asynchronous Assistant Runtime client using aiohttp.
32
+
33
+ Use as an async context manager for proper resource management.
34
+
35
+ Example:
36
+ >>> async with AsyncAssistantRuntimeClient("tenant-id", "secret") as client:
37
+ ... models = await client.list_available_models()
38
+ ... async for event in client.stream_chat("session-1", "Hello", "user@example.com"):
39
+ ... print(event)
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ tenant_id: str,
45
+ tenant_secret: str,
46
+ ar_url: str = BaseAssistantRuntimeClient.DEFAULT_AR_URL,
47
+ logger=None,
48
+ timeout: float = BaseAssistantRuntimeClient.DEFAULT_TIMEOUT,
49
+ session: Optional["aiohttp.ClientSession"] = None,
50
+ billing_api_base: Optional[str] = None,
51
+ memory_api_base: Optional[str] = None,
52
+ workflows_api_base: Optional[str] = None,
53
+ marketplace_api_base: Optional[str] = None,
54
+ voice_api_base: Optional[str] = None,
55
+ site_url: Optional[str] = None,
56
+ ):
57
+ """
58
+ Initialize async Assistant Runtime client.
59
+
60
+ Args:
61
+ tenant_id: Unique tenant identifier from Assistant Runtime
62
+ tenant_secret: HMAC secret for request signing
63
+ ar_url: Base URL of Assistant Runtime server
64
+ logger: Optional logger instance
65
+ timeout: Default request timeout in seconds
66
+ session: Optional aiohttp.ClientSession to reuse
67
+ billing_api_base: Override URL for billing API endpoints
68
+ memory_api_base: Override URL for memory/onboarding/document API endpoints
69
+ workflows_api_base: Override URL for workflow API endpoints
70
+ marketplace_api_base: Override URL for marketplace API endpoints
71
+ voice_api_base: Override URL for voice/transcription API endpoints
72
+ site_url: This installation's canonical URL (e.g. ``"https://mysite.example.com"``).
73
+ Automatically included in signed payloads for origin binding (Phase 2).
74
+ """
75
+ if aiohttp is None:
76
+ raise ImportError(
77
+ "aiohttp is required for AsyncAssistantRuntimeClient. "
78
+ "Install it with: pip install assistant_runtime_sdk[async]"
79
+ )
80
+
81
+ super().__init__(
82
+ tenant_id, tenant_secret, ar_url, logger, timeout,
83
+ billing_api_base, memory_api_base, workflows_api_base,
84
+ marketplace_api_base, voice_api_base, site_url,
85
+ )
86
+ self._session = session
87
+ self._owns_session = session is None
88
+
89
+ async def __aenter__(self) -> "AsyncAssistantRuntimeClient":
90
+ """Enter async context - create session if needed."""
91
+ if self._owns_session:
92
+ timeout = aiohttp.ClientTimeout(total=self.timeout)
93
+ self._session = aiohttp.ClientSession(timeout=timeout)
94
+ return self
95
+
96
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
97
+ """Exit async context - close session if we own it."""
98
+ if self._owns_session and self._session:
99
+ await self._session.close()
100
+ self._session = None
101
+
102
+ def _ensure_session(self) -> "aiohttp.ClientSession":
103
+ """Ensure we have an active session."""
104
+ if self._session is None:
105
+ raise ARConfigurationError(
106
+ "No active session. Use AsyncAssistantRuntimeClient as a context manager: "
107
+ "async with AsyncAssistantRuntimeClient(...) as client:"
108
+ )
109
+ return self._session
110
+
111
+ # =========================================================================
112
+ # Internal Request Methods
113
+ # =========================================================================
114
+
115
+ async def _handle_error_response(
116
+ self, response: "aiohttp.ClientResponse", endpoint: str, method: str,
117
+ ) -> None:
118
+ """Check response status and raise appropriate exceptions.
119
+
120
+ Must be called BEFORE raise_for_status() so we can read the body
121
+ and raise specific exception types (e.g. ARAuthenticationError for 401).
122
+ """
123
+ if response.status < 400:
124
+ return
125
+
126
+ # Read error body for better messages. The full payload is
127
+ # forwarded to ARAPIError.response_data so callers can read
128
+ # any extra keys AR set (e.g. tenant_owner_user_id).
129
+ msg = None
130
+ data: Optional[Dict[str, Any]] = None
131
+ try:
132
+ data = await response.json()
133
+ msg = self._extract_error_from_data(data)
134
+ except Exception:
135
+ pass
136
+
137
+ if response.status == 401:
138
+ raise ARAuthenticationError(msg or "Authentication failed")
139
+
140
+ raise ARAPIError(
141
+ msg or f"{method} {endpoint} failed with status {response.status}",
142
+ status_code=response.status,
143
+ response_data=data,
144
+ )
145
+
146
+ async def _request_get(
147
+ self,
148
+ endpoint: str,
149
+ params: Dict[str, Any],
150
+ timeout: Optional[float] = None,
151
+ api_base: Optional[str] = None,
152
+ ) -> Optional[Dict[str, Any]]:
153
+ """Make authenticated async GET request."""
154
+ session = self._ensure_session()
155
+ url = f"{api_base}.{endpoint}" if api_base else self._build_endpoint_url(endpoint)
156
+ params = self._with_site_url(params)
157
+ headers = self._get_headers(params, for_query_string=True)
158
+
159
+ try:
160
+ timeout_obj = aiohttp.ClientTimeout(total=timeout or self.timeout)
161
+ async with session.get(url, params=params, headers=headers, timeout=timeout_obj) as response:
162
+ await self._handle_error_response(response, endpoint, "GET")
163
+ data = await response.json()
164
+ return data.get("message", data)
165
+ except (ARAuthenticationError, ARAPIError):
166
+ raise
167
+ except aiohttp.ServerTimeoutError as e:
168
+ self._log_error(f"GET {endpoint} timeout: {e}")
169
+ raise ARTimeoutError(f"Request to {endpoint} timed out") from e
170
+ except aiohttp.ClientConnectorError as e:
171
+ self._log_error(f"GET {endpoint} connection error: {e}")
172
+ raise ARConnectionError(f"Failed to connect to {endpoint}") from e
173
+ except aiohttp.ClientError as e:
174
+ self._log_error(f"GET {endpoint} error: {e}")
175
+ raise ARAPIError(str(e)) from e
176
+
177
+ async def _request_get_raw(
178
+ self,
179
+ endpoint: str,
180
+ params: Dict[str, Any],
181
+ timeout: Optional[float] = None,
182
+ api_base: Optional[str] = None,
183
+ ) -> tuple:
184
+ """Make authenticated async GET request expecting raw binary response.
185
+
186
+ Returns:
187
+ (content_bytes, content_type, filename) tuple
188
+ """
189
+ session = self._ensure_session()
190
+ url = f"{api_base}.{endpoint}" if api_base else self._build_endpoint_url(endpoint)
191
+ params = self._with_site_url(params)
192
+ headers = self._get_headers(params, for_query_string=True)
193
+
194
+ try:
195
+ timeout_obj = aiohttp.ClientTimeout(total=timeout or self.timeout)
196
+ async with session.get(url, params=params, headers=headers, timeout=timeout_obj) as response:
197
+ await self._handle_error_response(response, endpoint, "GET raw")
198
+ content = await response.read()
199
+
200
+ content_type = response.headers.get("Content-Type", "application/octet-stream")
201
+ filename = ""
202
+ cd = response.headers.get("Content-Disposition", "")
203
+ if "filename=" in cd:
204
+ parts = cd.split("filename=")
205
+ if len(parts) > 1:
206
+ filename = parts[1].strip().strip('"')
207
+
208
+ return content, content_type, filename
209
+ except (ARAuthenticationError, ARAPIError):
210
+ raise
211
+ except aiohttp.ServerTimeoutError as e:
212
+ self._log_error(f"GET raw {endpoint} timeout: {e}")
213
+ raise ARTimeoutError(f"Request to {endpoint} timed out") from e
214
+ except aiohttp.ClientConnectorError as e:
215
+ self._log_error(f"GET raw {endpoint} connection error: {e}")
216
+ raise ARConnectionError(f"Failed to connect to {endpoint}") from e
217
+ except aiohttp.ClientError as e:
218
+ self._log_error(f"GET raw {endpoint} error: {e}")
219
+ raise ARAPIError(str(e)) from e
220
+
221
+ async def _request_post_json(
222
+ self,
223
+ endpoint: str,
224
+ payload: Dict[str, Any],
225
+ timeout: Optional[float] = None,
226
+ api_base: Optional[str] = None,
227
+ ) -> Optional[Dict[str, Any]]:
228
+ """Make authenticated async POST request with JSON body."""
229
+ session = self._ensure_session()
230
+ url = f"{api_base}.{endpoint}" if api_base else self._build_endpoint_url(endpoint)
231
+ payload = self._with_site_url(payload)
232
+ headers = {
233
+ **self._get_headers(payload, for_query_string=False),
234
+ "Content-Type": "application/json",
235
+ }
236
+
237
+ try:
238
+ timeout_obj = aiohttp.ClientTimeout(total=timeout or self.timeout)
239
+ async with session.post(url, json=payload, headers=headers, timeout=timeout_obj) as response:
240
+ await self._handle_error_response(response, endpoint, "POST")
241
+ data = await response.json()
242
+ return data.get("message", data)
243
+ except (ARAuthenticationError, ARAPIError):
244
+ raise
245
+ except aiohttp.ServerTimeoutError as e:
246
+ self._log_error(f"POST {endpoint} timeout: {e}")
247
+ raise ARTimeoutError(f"Request to {endpoint} timed out") from e
248
+ except aiohttp.ClientConnectorError as e:
249
+ self._log_error(f"POST {endpoint} connection error: {e}")
250
+ raise ARConnectionError(f"Failed to connect to {endpoint}") from e
251
+ except aiohttp.ClientError as e:
252
+ self._log_error(f"POST {endpoint} error: {e}")
253
+ raise ARAPIError(str(e)) from e
254
+
255
+ async def _request_post_form(
256
+ self,
257
+ endpoint: str,
258
+ params: Dict[str, Any],
259
+ timeout: Optional[float] = None,
260
+ api_base: Optional[str] = None,
261
+ ) -> Optional[Dict[str, Any]]:
262
+ """Make authenticated async POST request with form-urlencoded body."""
263
+ session = self._ensure_session()
264
+ url = f"{api_base}.{endpoint}" if api_base else self._build_endpoint_url(endpoint)
265
+ params = self._with_site_url(params)
266
+ headers = {
267
+ **self._get_headers(params, for_query_string=True),
268
+ "Content-Type": "application/x-www-form-urlencoded",
269
+ }
270
+
271
+ try:
272
+ timeout_obj = aiohttp.ClientTimeout(total=timeout or self.timeout)
273
+ async with session.post(url, data=params, headers=headers, timeout=timeout_obj) as response:
274
+ await self._handle_error_response(response, endpoint, "POST form")
275
+ data = await response.json()
276
+ return data.get("message", data)
277
+ except (ARAuthenticationError, ARAPIError):
278
+ raise
279
+ except aiohttp.ServerTimeoutError as e:
280
+ self._log_error(f"POST form {endpoint} timeout: {e}")
281
+ raise ARTimeoutError(f"Request to {endpoint} timed out") from e
282
+ except aiohttp.ClientConnectorError as e:
283
+ self._log_error(f"POST form {endpoint} connection error: {e}")
284
+ raise ARConnectionError(f"Failed to connect to {endpoint}") from e
285
+ except aiohttp.ClientError as e:
286
+ self._log_error(f"POST form {endpoint} error: {e}")
287
+ raise ARAPIError(str(e)) from e
288
+
289
+ async def _request_post_multipart(
290
+ self,
291
+ endpoint: str,
292
+ params: Dict[str, Any],
293
+ file_field: str,
294
+ file_name: str,
295
+ file_data: bytes,
296
+ content_type: str = "application/octet-stream",
297
+ timeout: Optional[float] = None,
298
+ api_base: Optional[str] = None,
299
+ ) -> Optional[Dict[str, Any]]:
300
+ """Make authenticated async POST request with multipart form data including a file."""
301
+ session = self._ensure_session()
302
+ url = f"{api_base}.{endpoint}" if api_base else self._build_endpoint_url(endpoint)
303
+ # Sign only non-file form fields — Frappe's form_dict excludes file parts
304
+ params = self._with_site_url(params)
305
+ headers = self._get_headers(params, for_query_string=True)
306
+ # Do NOT set Content-Type — aiohttp sets multipart boundary automatically
307
+
308
+ form_data = aiohttp.FormData()
309
+ for key, value in params.items():
310
+ form_data.add_field(key, str(value))
311
+ form_data.add_field(
312
+ file_field, file_data,
313
+ filename=file_name,
314
+ content_type=content_type,
315
+ )
316
+
317
+ try:
318
+ timeout_obj = aiohttp.ClientTimeout(total=timeout or self.timeout)
319
+ async with session.post(url, data=form_data, headers=headers, timeout=timeout_obj) as response:
320
+ await self._handle_error_response(response, endpoint, "POST multipart")
321
+ data = await response.json()
322
+ return data.get("message", data)
323
+ except (ARAuthenticationError, ARAPIError):
324
+ raise
325
+ except aiohttp.ServerTimeoutError as e:
326
+ self._log_error(f"POST multipart {endpoint} timeout: {e}")
327
+ raise ARTimeoutError(f"Request to {endpoint} timed out") from e
328
+ except aiohttp.ClientConnectorError as e:
329
+ self._log_error(f"POST multipart {endpoint} connection error: {e}")
330
+ raise ARConnectionError(f"Failed to connect to {endpoint}") from e
331
+ except aiohttp.ClientError as e:
332
+ self._log_error(f"POST multipart {endpoint} error: {e}")
333
+ raise ARAPIError(str(e)) from e
334
+
335
+ async def _request_delete(
336
+ self,
337
+ endpoint: str,
338
+ params: Dict[str, Any],
339
+ timeout: Optional[float] = None,
340
+ api_base: Optional[str] = None,
341
+ ) -> Optional[Dict[str, Any]]:
342
+ """Make authenticated async DELETE request."""
343
+ session = self._ensure_session()
344
+ url = f"{api_base}.{endpoint}" if api_base else self._build_endpoint_url(endpoint)
345
+ params = self._with_site_url(params)
346
+ headers = self._get_headers(params, for_query_string=True)
347
+
348
+ try:
349
+ timeout_obj = aiohttp.ClientTimeout(total=timeout or self.timeout)
350
+ async with session.delete(url, params=params, headers=headers, timeout=timeout_obj) as response:
351
+ await self._handle_error_response(response, endpoint, "DELETE")
352
+ data = await response.json()
353
+ return data.get("message", data)
354
+ except (ARAuthenticationError, ARAPIError):
355
+ raise
356
+ except aiohttp.ServerTimeoutError as e:
357
+ self._log_error(f"DELETE {endpoint} timeout: {e}")
358
+ raise ARTimeoutError(f"Request to {endpoint} timed out") from e
359
+ except aiohttp.ClientConnectorError as e:
360
+ self._log_error(f"DELETE {endpoint} connection error: {e}")
361
+ raise ARConnectionError(f"Failed to connect to {endpoint}") from e
362
+ except aiohttp.ClientError as e:
363
+ self._log_error(f"DELETE {endpoint} error: {e}")
364
+ raise ARAPIError(str(e)) from e
365
+
366
+ # =========================================================================
367
+ # Streaming API
368
+ # =========================================================================
369
+
370
+ async def stream_chat(
371
+ self,
372
+ session_id: str,
373
+ message: Optional[str],
374
+ user_id: str,
375
+ context: Optional[Dict[str, Any]] = None,
376
+ model_id: Optional[str] = None,
377
+ attachments: Optional[List[Dict[str, Any]]] = None,
378
+ system_prompt_addendum: Optional[str] = None,
379
+ client_type: Optional[str] = None,
380
+ interrupt_response: Optional[List[Dict[str, str]]] = None,
381
+ message_id: Optional[str] = None,
382
+ session_state: Optional[Dict[str, Any]] = None,
383
+ continue_from_message_id: Optional[str] = None,
384
+ *,
385
+ web_search: Optional[bool] = None,
386
+ thinking_enabled: Optional[bool] = None,
387
+ ) -> AsyncGenerator[Dict[str, Any], None]:
388
+ """Async version of AssistantRuntimeClient.stream_chat.
389
+
390
+ On ``stream_complete``, ``event['session_state']`` is the updated signed
391
+ blob — store it and pass it as ``session_state`` on the next turn.
392
+ """
393
+ session = self._ensure_session()
394
+ payload = self._prepare_stream_payload(
395
+ session_id, message, user_id, context, model_id, attachments,
396
+ system_prompt_addendum, client_type=client_type,
397
+ interrupt_response=interrupt_response,
398
+ message_id=message_id,
399
+ session_state=session_state,
400
+ continue_from_message_id=continue_from_message_id,
401
+ web_search=web_search,
402
+ thinking_enabled=thinking_enabled,
403
+ )
404
+ url = self._build_endpoint_url("streaming.stream_chat")
405
+ payload = self._with_site_url(payload)
406
+ headers = self._get_stream_headers(payload, for_json_body=True)
407
+
408
+ try:
409
+ timeout = aiohttp.ClientTimeout(
410
+ connect=self.STREAM_CONNECT_TIMEOUT,
411
+ total=self.STREAM_READ_TIMEOUT,
412
+ )
413
+ async with session.post(url, json=payload, headers=headers, timeout=timeout) as response:
414
+ response.raise_for_status()
415
+
416
+ current_event = None
417
+
418
+ async for line in response.content:
419
+ if not line:
420
+ continue
421
+
422
+ line_str = line.decode("utf-8").strip()
423
+ if not line_str:
424
+ continue
425
+
426
+ parsed = self._parse_sse_line(line_str)
427
+ if not parsed:
428
+ continue
429
+
430
+ if parsed["type"] == "heartbeat":
431
+ yield {"event": "heartbeat", "data": {}}
432
+ continue
433
+ if parsed["type"] == "event_name":
434
+ current_event = parsed["value"]
435
+ elif parsed["type"] == "data":
436
+ yield {"event": current_event or "unknown", "data": parsed["value"]}
437
+ current_event = None
438
+
439
+ except aiohttp.ServerTimeoutError:
440
+ yield {
441
+ "event": "stream_error",
442
+ "data": {"error": "Connection timeout", "error_code": "TIMEOUT"},
443
+ }
444
+ except aiohttp.ClientError as e:
445
+ yield {
446
+ "event": "stream_error",
447
+ "data": {"error": str(e), "error_code": "REQUEST_ERROR"},
448
+ }
449
+
450
+ # =========================================================================
451
+ # Tenant APIs
452
+ # =========================================================================
453
+
454
+ async def get_tenant_info(self) -> Optional[Dict[str, Any]]:
455
+ """Async version of AssistantRuntimeClient.get_tenant_info."""
456
+ endpoint, params = self._prepare_get_tenant_info()
457
+ return await self._request_get(endpoint, params)
458
+
459
+ async def accept_terms(self, terms_version: str, accepted_by: str) -> Dict[str, Any]:
460
+ """Async version of AssistantRuntimeClient.accept_terms."""
461
+ endpoint, payload = self._prepare_accept_terms(terms_version, accepted_by)
462
+ return await self._request_post_json(endpoint, payload)
463
+
464
+ # =========================================================================
465
+ # Model APIs
466
+ # =========================================================================
467
+
468
+ async def list_available_models(self, timeout: Optional[float] = None) -> Optional[Dict[str, Any]]:
469
+ """Async version of AssistantRuntimeClient.list_available_models."""
470
+ endpoint, params = self._prepare_list_available_models()
471
+ return await self._request_get(endpoint, params, timeout=timeout)
472
+
473
+ async def get_available_models(self) -> Optional[Dict[str, Any]]:
474
+ """Async version of AssistantRuntimeClient.get_available_models."""
475
+ endpoint, params = self._prepare_get_available_models()
476
+ return await self._request_get(endpoint, params)
477
+
478
+ async def set_preferred_model(self, model_id: str) -> bool:
479
+ """Async version of AssistantRuntimeClient.set_preferred_model."""
480
+ endpoint, payload = self._prepare_set_preferred_model(model_id)
481
+ result = await self._request_post_json(endpoint, payload)
482
+ return result.get("success", False) if result else False
483
+
484
+ # =========================================================================
485
+ # Origin Binding APIs (Phase 2)
486
+ # =========================================================================
487
+
488
+ async def request_rebind(self, new_site_url: str) -> Dict[str, Any]:
489
+ """Initiate a tenant rebind. Returns {"rebind_token": str, "expires_at": str}.
490
+
491
+ When the site URL of an installation changes, call this to begin the
492
+ rebind flow. AR sends a confirmation email to the tenant owner. After
493
+ the owner clicks the link, call the module-level helper
494
+ ``get_rotated_secret(ar_url, rebind_token)`` in the sync client with
495
+ the token from the link to retrieve the new ``tenant_secret``.
496
+ """
497
+ return await self._request_post_json(
498
+ "request_rebind",
499
+ {"tenant_id": self.tenant_id, "new_site_url": new_site_url},
500
+ )
501
+
502
+ async def claim_tenant_email(self, owner_email: str) -> Dict[str, Any]:
503
+ """Bootstrap an owner_email on a legacy tenant (one without a verified
504
+ owner_email on file). AR sends a verification email; the email is only
505
+ persisted to the tenant record after the owner clicks the link.
506
+ """
507
+ return await self._request_post_json(
508
+ "claim_tenant_email",
509
+ {"tenant_id": self.tenant_id, "owner_email": owner_email},
510
+ )
511
+
512
+ async def diagnose_registration(self) -> Dict[str, Any]:
513
+ """Diagnose registration issues. Returns one of:
514
+
515
+ - ``{"status": "ok", "bound_host": ..., "last_verification": ...}``
516
+ - ``{"status": "origin_mismatch", "bound_host": ..., "claimed_host": ...,
517
+ "remediation": "rebind", "rebind_url": ...}``
518
+ """
519
+ return await self._request_post_json(
520
+ "diagnose_registration",
521
+ {"tenant_id": self.tenant_id},
522
+ )
523
+
524
+ # =========================================================================
525
+ # Prompt APIs
526
+ # =========================================================================
527
+
528
+ async def list_prompts(self, user_id: str, cursor: Optional[str] = None, timeout: Optional[float] = None) -> Optional[Dict[str, Any]]:
529
+ """Async version of AssistantRuntimeClient.list_prompts."""
530
+ endpoint, params = self._prepare_list_prompts(user_id, cursor)
531
+ return await self._request_get(endpoint, params, timeout=timeout)
532
+
533
+ async def get_prompt(
534
+ self,
535
+ prompt_name: str,
536
+ user_id: str,
537
+ arguments: Optional[Dict[str, Any]] = None,
538
+ ) -> Optional[Dict[str, Any]]:
539
+ """Async version of AssistantRuntimeClient.get_prompt."""
540
+ endpoint, payload = self._prepare_get_prompt(prompt_name, user_id, arguments)
541
+ return await self._request_post_json(endpoint, payload)
542
+
543
+ # =========================================================================
544
+ # Suggestion APIs
545
+ # =========================================================================
546
+
547
+ async def get_suggestions(
548
+ self,
549
+ user_id: str,
550
+ context: Optional[Dict[str, Any]] = None,
551
+ limit: int = 8,
552
+ timeout: Optional[float] = None,
553
+ ) -> Optional[Dict[str, Any]]:
554
+ """Async version of AssistantRuntimeClient.get_suggestions."""
555
+ endpoint, params = self._prepare_get_suggestions(user_id, context, limit)
556
+ return await self._request_get(endpoint, params, timeout=timeout)
557
+
558
+ async def generate_curated_suggestions(
559
+ self,
560
+ user_id: str,
561
+ signals: Optional[Dict[str, Any]] = None,
562
+ timeout: Optional[float] = None,
563
+ ) -> Optional[Dict[str, Any]]:
564
+ """Generate LLM-curated welcome suggestions from usage signals.
565
+
566
+ Costs one Economy-tier completion on AR (rate-limited server-side to
567
+ one generation per user per 20h). Returns
568
+ {"suggestions": [...], "generated_at": ...} or an empty-suggestions
569
+ dict with a reason/rate_limited flag.
570
+ """
571
+ endpoint, payload = self._prepare_generate_curated_suggestions(user_id, signals)
572
+ return await self._request_post_json(endpoint, payload, timeout=timeout)
573
+
574
+ # =========================================================================
575
+ # Onboarding APIs
576
+ # =========================================================================
577
+
578
+ async def get_onboarding_status(self, user_id: str) -> Optional[Dict[str, Any]]:
579
+ """Async version of AssistantRuntimeClient.get_onboarding_status."""
580
+ endpoint, params = self._prepare_get_onboarding_status(user_id)
581
+ return await self._request_get(endpoint, params, api_base=self.memory_api_base)
582
+
583
+ async def complete_onboarding(
584
+ self,
585
+ user_id: str,
586
+ ) -> Optional[Dict[str, Any]]:
587
+ """Async version of AssistantRuntimeClient.complete_onboarding."""
588
+ endpoint, payload = self._prepare_complete_onboarding(user_id)
589
+ return await self._request_post_json(endpoint, payload, api_base=self.memory_api_base)
590
+
591
+ # =========================================================================
592
+ # Document APIs (RAG)
593
+ # =========================================================================
594
+
595
+ async def upload_document(
596
+ self,
597
+ file_path: Optional[str] = None,
598
+ file_data: Optional[bytes] = None,
599
+ file_name: Optional[str] = None,
600
+ content_type: Optional[str] = None,
601
+ user_id: Optional[str] = None,
602
+ visibility: Optional[str] = None,
603
+ shared_with: Optional[List[str]] = None,
604
+ ) -> Optional[Dict[str, Any]]:
605
+ """Async version of AssistantRuntimeClient.upload_document."""
606
+ endpoint, params, f_field, f_name, f_data, c_type = self._prepare_upload_document(
607
+ file_path, file_data, file_name, content_type,
608
+ user_id=user_id, visibility=visibility, shared_with=shared_with,
609
+ )
610
+ return await self._request_post_multipart(
611
+ endpoint, params=params, file_field=f_field,
612
+ file_name=f_name, file_data=f_data, content_type=c_type,
613
+ timeout=120.0, api_base=self.memory_api_base,
614
+ )
615
+
616
+ async def list_documents(
617
+ self, limit: int = 50, offset: int = 0, user_id: Optional[str] = None
618
+ ) -> Optional[Dict[str, Any]]:
619
+ """Async version of AssistantRuntimeClient.list_documents."""
620
+ endpoint, params = self._prepare_list_documents(limit, offset, user_id=user_id)
621
+ return await self._request_get(endpoint, params, api_base=self.memory_api_base)
622
+
623
+ async def get_document(self, document_id: str) -> Optional[Dict[str, Any]]:
624
+ """Async version of AssistantRuntimeClient.get_document."""
625
+ endpoint, params = self._prepare_get_document(document_id)
626
+ return await self._request_get(endpoint, params, api_base=self.memory_api_base)
627
+
628
+ async def list_chunks(
629
+ self,
630
+ document_id: str,
631
+ user_id: Optional[str] = None,
632
+ search: Optional[str] = None,
633
+ limit: int = 50,
634
+ offset: int = 0,
635
+ ) -> Optional[Dict[str, Any]]:
636
+ """Async version of AssistantRuntimeClient.list_chunks."""
637
+ endpoint, params = self._prepare_list_chunks(
638
+ document_id, user_id=user_id, search=search, limit=limit, offset=offset
639
+ )
640
+ return await self._request_get(endpoint, params, api_base=self.memory_api_base)
641
+
642
+ async def get_document_content(
643
+ self, document_id: str, user_id: Optional[str] = None
644
+ ) -> tuple:
645
+ """Async version of AssistantRuntimeClient.get_document_content."""
646
+ endpoint, params = self._prepare_get_document_content(document_id, user_id=user_id)
647
+ return await self._request_get_raw(endpoint, params, api_base=self.memory_api_base)
648
+
649
+ async def delete_document(
650
+ self, document_id: str, user_id: Optional[str] = None
651
+ ) -> Optional[Dict[str, Any]]:
652
+ """Async version of AssistantRuntimeClient.delete_document."""
653
+ endpoint, payload = self._prepare_delete_document(document_id, user_id=user_id)
654
+ return await self._request_post_json(endpoint, payload, api_base=self.memory_api_base)
655
+
656
+ async def update_document_access(
657
+ self,
658
+ document_id: str,
659
+ user_id: str,
660
+ visibility: Optional[str] = None,
661
+ add_users: Optional[List[str]] = None,
662
+ remove_users: Optional[List[str]] = None,
663
+ ) -> Optional[Dict[str, Any]]:
664
+ """Async version of AssistantRuntimeClient.update_document_access."""
665
+ endpoint, payload = self._prepare_update_document_access(
666
+ document_id, user_id,
667
+ visibility=visibility, add_users=add_users, remove_users=remove_users,
668
+ )
669
+ return await self._request_post_json(endpoint, payload, api_base=self.memory_api_base)
670
+
671
+ async def get_storage_info(self) -> Optional[Dict[str, Any]]:
672
+ """Async version of AssistantRuntimeClient.get_storage_info."""
673
+ endpoint, params = self._prepare_get_storage_info()
674
+ return await self._request_get(endpoint, params, api_base=self.memory_api_base)
675
+
676
+ # =========================================================================
677
+ # Memory APIs (User Memory Viewer)
678
+ # =========================================================================
679
+
680
+ async def list_memories(
681
+ self,
682
+ user_id: Optional[str] = None,
683
+ memory_type: Optional[str] = None,
684
+ limit: int = 50,
685
+ offset: int = 0,
686
+ ) -> Optional[Dict[str, Any]]:
687
+ """Async version of AssistantRuntimeClient.list_memories."""
688
+ endpoint, params = self._prepare_list_memories(user_id, memory_type, limit, offset)
689
+ return await self._request_get(endpoint, params, api_base=self.memory_api_base)
690
+
691
+ async def delete_memory(self, user_id: str, memory_id: str) -> Optional[Dict[str, Any]]:
692
+ """Async version of AssistantRuntimeClient.delete_memory."""
693
+ endpoint, payload = self._prepare_delete_memory(user_id, memory_id)
694
+ return await self._request_post_json(endpoint, payload, api_base=self.memory_api_base)
695
+
696
+ async def delete_all_memories(self, user_id: str) -> Optional[Dict[str, Any]]:
697
+ """Async version of AssistantRuntimeClient.delete_all_memories."""
698
+ endpoint, payload = self._prepare_delete_all_memories(user_id)
699
+ return await self._request_post_json(endpoint, payload, api_base=self.memory_api_base)
700
+
701
+ async def update_memory(self, user_id: str, memory_id: str, content: str) -> Optional[Dict[str, Any]]:
702
+ """Async version of AssistantRuntimeClient.update_memory."""
703
+ endpoint, payload = self._prepare_update_memory(user_id, memory_id, content)
704
+ return await self._request_post_json(endpoint, payload, api_base=self.memory_api_base)
705
+
706
+ async def get_memory_stats(self, user_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
707
+ """Async version of AssistantRuntimeClient.get_memory_stats."""
708
+ endpoint, params = self._prepare_get_memory_stats(user_id)
709
+ return await self._request_get(endpoint, params, api_base=self.memory_api_base)
710
+
711
+ async def get_memory_summary(
712
+ self,
713
+ user_id: str,
714
+ force: bool = False,
715
+ timeout: Optional[float] = None,
716
+ ) -> Optional[Dict[str, Any]]:
717
+ """Async get AI-generated narrative summary of user's memories."""
718
+ endpoint, params = self._prepare_get_memory_summary(user_id, force)
719
+ return await self._request_get(endpoint, params, timeout=timeout, api_base=self.memory_api_base)
720
+
721
+ # =========================================================================
722
+ # Shared Knowledge APIs
723
+ # =========================================================================
724
+
725
+ async def get_shared_knowledge(self) -> Optional[Dict[str, Any]]:
726
+ """Async version of AssistantRuntimeClient.get_shared_knowledge."""
727
+ endpoint, params = self._prepare_get_shared_knowledge()
728
+ return await self._request_get(endpoint, params, api_base=self.memory_api_base)
729
+
730
+ async def update_shared_knowledge(self, content: str) -> Optional[Dict[str, Any]]:
731
+ """Async version of AssistantRuntimeClient.update_shared_knowledge."""
732
+ endpoint, payload = self._prepare_update_shared_knowledge(content)
733
+ return await self._request_post_json(endpoint, payload, api_base=self.memory_api_base)
734
+
735
+ async def share_memory_to_knowledge(self, user_id: str, memory_id: str) -> Optional[Dict[str, Any]]:
736
+ """Async version of AssistantRuntimeClient.share_memory_to_knowledge."""
737
+ endpoint, payload = self._prepare_share_memory_to_knowledge(user_id, memory_id)
738
+ return await self._request_post_json(endpoint, payload, api_base=self.memory_api_base)
739
+
740
+ # =========================================================================
741
+ # Resource APIs (Skills/Documentation)
742
+ # =========================================================================
743
+
744
+ async def list_resources(
745
+ self,
746
+ user_id: str,
747
+ server: Optional[str] = None,
748
+ ) -> Optional[Dict[str, Any]]:
749
+ """Async version of AssistantRuntimeClient.list_resources."""
750
+ params = self._prepare_resource_params(user_id, server=server)
751
+ return await self._request_get("resources.list_resources", params)
752
+
753
+ async def read_resource(self, user_id: str, uri: str) -> Optional[Dict[str, Any]]:
754
+ """Async version of AssistantRuntimeClient.read_resource."""
755
+ params = self._prepare_resource_params(user_id, uri=uri)
756
+ return await self._request_post_json("resources.read_resource", params)
757
+
758
+ # =========================================================================
759
+ # Tool APIs
760
+ # =========================================================================
761
+
762
+ async def list_tools(
763
+ self,
764
+ user_id: str,
765
+ server: Optional[str] = None,
766
+ ) -> Optional[Dict[str, Any]]:
767
+ """Async version of AssistantRuntimeClient.list_tools."""
768
+ endpoint, params = self._prepare_list_tools(user_id, server)
769
+ return await self._request_get(endpoint, params)
770
+
771
+ # =========================================================================
772
+ # Tool Preference APIs (per-user approval settings)
773
+ # =========================================================================
774
+
775
+ async def list_tool_preferences(self, user_id: str) -> Optional[Dict[str, Any]]:
776
+ """Async version of AssistantRuntimeClient.list_tool_preferences."""
777
+ endpoint, params = self._prepare_list_tool_preferences(user_id)
778
+ return await self._request_get(endpoint, params)
779
+
780
+ async def set_tool_preference(
781
+ self,
782
+ user_id: str,
783
+ tool_name: str,
784
+ preference: str,
785
+ ) -> Optional[Dict[str, Any]]:
786
+ """Async version of AssistantRuntimeClient.set_tool_preference."""
787
+ endpoint, payload = self._prepare_set_tool_preference(user_id, tool_name, preference)
788
+ return await self._request_post_json(endpoint, payload)
789
+
790
+ # =========================================================================
791
+ # Billing & Subscription APIs
792
+ # =========================================================================
793
+
794
+ async def check_billing_available(self) -> bool:
795
+ """Async version of AssistantRuntimeClient.check_billing_available."""
796
+ session = self._ensure_session()
797
+ try:
798
+ url = f"{self.api_base}.get_capabilities"
799
+ timeout_obj = aiohttp.ClientTimeout(total=self.timeout)
800
+ async with session.get(url, timeout=timeout_obj) as response:
801
+ response.raise_for_status()
802
+ data = await response.json()
803
+ result = data.get("message", data)
804
+ self._billing_available = result.get("billing_enabled", False)
805
+ except (aiohttp.ClientError, Exception):
806
+ self._billing_available = False
807
+ return self._billing_available
808
+
809
+ async def get_plan_comparison(self) -> Optional[Dict[str, Any]]:
810
+ """Async version of AssistantRuntimeClient.get_plan_comparison."""
811
+ self._require_billing()
812
+ session = self._ensure_session()
813
+ url = self._build_billing_endpoint_url("get_plan_comparison")
814
+ try:
815
+ timeout_obj = aiohttp.ClientTimeout(total=self.timeout)
816
+ async with session.get(url, timeout=timeout_obj) as response:
817
+ response.raise_for_status()
818
+ data = await response.json()
819
+ return data.get("message", data)
820
+ except (aiohttp.ClientError, Exception) as e:
821
+ self._log_error(f"get_plan_comparison error: {e}")
822
+ return None
823
+
824
+ async def get_recommended_gateway(self) -> Optional[Dict[str, Any]]:
825
+ """Async version of AssistantRuntimeClient.get_recommended_gateway."""
826
+ endpoint, params = self._prepare_get_recommended_gateway()
827
+ return await self._request_get(endpoint, params, api_base=self.billing_api_base)
828
+
829
+ async def get_available_gateways(self) -> Optional[Dict[str, Any]]:
830
+ """Async version of AssistantRuntimeClient.get_available_gateways."""
831
+ endpoint, params = self._prepare_get_available_gateways()
832
+ return await self._request_get(endpoint, params, api_base=self.billing_api_base)
833
+
834
+ async def preview_plan_pricing(
835
+ self, plan: str, billing_cycle: str = "monthly",
836
+ ) -> Optional[Dict[str, Any]]:
837
+ """Async version of AssistantRuntimeClient.preview_plan_pricing."""
838
+ endpoint, params = self._prepare_preview_plan_pricing(plan, billing_cycle)
839
+ return await self._request_get(endpoint, params, api_base=self.billing_api_base)
840
+
841
+ async def add_user_seat(self) -> Optional[Dict[str, Any]]:
842
+ """Async version of AssistantRuntimeClient.add_user_seat."""
843
+ endpoint, params = self._prepare_add_user_seat()
844
+ return await self._request_get(endpoint, params, api_base=self.billing_api_base)
845
+
846
+ async def remove_user_seat(self) -> Optional[Dict[str, Any]]:
847
+ """Async version of AssistantRuntimeClient.remove_user_seat."""
848
+ endpoint, params = self._prepare_remove_user_seat()
849
+ return await self._request_get(endpoint, params, api_base=self.billing_api_base)
850
+
851
+ async def preview_seat_charge(self) -> Optional[Dict[str, Any]]:
852
+ """Async version of AssistantRuntimeClient.preview_seat_charge."""
853
+ endpoint, params = self._prepare_preview_seat_charge()
854
+ return await self._request_get(endpoint, params, api_base=self.billing_api_base)
855
+
856
+ async def download_invoice_pdf(self, ar_invoice_name: str) -> tuple:
857
+ """Async version of AssistantRuntimeClient.download_invoice_pdf."""
858
+ endpoint, params = self._prepare_download_invoice_pdf(ar_invoice_name)
859
+ return await self._request_get_raw(
860
+ endpoint, params, api_base=self.billing_api_base,
861
+ )
862
+
863
+ async def initiate_checkout(
864
+ self,
865
+ plan: str,
866
+ billing_cycle: str = "monthly",
867
+ gateway: Optional[str] = None,
868
+ billing_name: Optional[str] = None,
869
+ billing_email: Optional[str] = None,
870
+ ) -> Optional[Dict[str, Any]]:
871
+ """Async version of AssistantRuntimeClient.initiate_checkout."""
872
+ endpoint, payload = self._prepare_initiate_checkout(plan, billing_cycle, gateway, billing_name, billing_email)
873
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
874
+
875
+ async def verify_checkout(self, session_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
876
+ """Async version of AssistantRuntimeClient.verify_checkout."""
877
+ endpoint, payload = self._prepare_verify_checkout(session_id)
878
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
879
+
880
+ async def verify_razorpay_payment(
881
+ self,
882
+ razorpay_payment_id: str,
883
+ razorpay_subscription_id: str,
884
+ razorpay_signature: str,
885
+ ) -> Optional[Dict[str, Any]]:
886
+ """Async version of AssistantRuntimeClient.verify_razorpay_payment."""
887
+ endpoint, payload = self._prepare_verify_razorpay_payment(razorpay_payment_id, razorpay_subscription_id, razorpay_signature)
888
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
889
+
890
+ async def verify_razorpay_credit_payment(
891
+ self,
892
+ razorpay_payment_id: str,
893
+ razorpay_order_id: str,
894
+ razorpay_signature: str,
895
+ ) -> Optional[Dict[str, Any]]:
896
+ """Async version of AssistantRuntimeClient.verify_razorpay_credit_payment."""
897
+ endpoint, payload = self._prepare_verify_razorpay_credit_payment(razorpay_payment_id, razorpay_order_id, razorpay_signature)
898
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
899
+
900
+ async def verify_seat_payment(
901
+ self,
902
+ razorpay_payment_id: str,
903
+ razorpay_order_id: str,
904
+ razorpay_signature: str,
905
+ ) -> Optional[Dict[str, Any]]:
906
+ """Async version of AssistantRuntimeClient.verify_seat_payment."""
907
+ endpoint, payload = self._prepare_verify_seat_payment(
908
+ razorpay_payment_id, razorpay_order_id, razorpay_signature
909
+ )
910
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
911
+
912
+ async def get_token_analytics(self, days: int = 30, user_id: str = None) -> Optional[Dict[str, Any]]:
913
+ """Async version of AssistantRuntimeClient.get_token_analytics."""
914
+ endpoint, payload = self._prepare_get_token_analytics(days, user_id)
915
+ return await self._request_post_json(endpoint, payload)
916
+
917
+ async def get_conversation_analytics(
918
+ self, user_id: str = None, days: int = 30, limit: int = 50, offset: int = 0,
919
+ ) -> Optional[Dict[str, Any]]:
920
+ """Async version of AssistantRuntimeClient.get_conversation_analytics."""
921
+ endpoint, payload = self._prepare_get_conversation_analytics(user_id, days, limit, offset)
922
+ return await self._request_post_json(endpoint, payload)
923
+
924
+ async def get_message_credits(self, conversation_id: str, user_id: str = None) -> Optional[Dict[str, Any]]:
925
+ """Async version of AssistantRuntimeClient.get_message_credits."""
926
+ endpoint, payload = self._prepare_get_message_credits(conversation_id, user_id)
927
+ return await self._request_post_json(endpoint, payload)
928
+
929
+ async def get_usage_dashboard(self) -> Optional[Dict[str, Any]]:
930
+ """Async version of AssistantRuntimeClient.get_usage_dashboard."""
931
+ endpoint, payload = self._prepare_get_usage_dashboard()
932
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
933
+
934
+ async def get_usage_history(self, days: int = 30) -> Optional[Dict[str, Any]]:
935
+ """Async version of AssistantRuntimeClient.get_usage_history."""
936
+ endpoint, payload = self._prepare_get_usage_history(days)
937
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
938
+
939
+ async def get_invoices(self, limit: int = 10) -> Optional[Dict[str, Any]]:
940
+ """Async version of AssistantRuntimeClient.get_invoices."""
941
+ endpoint, payload = self._prepare_get_invoices(limit)
942
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
943
+
944
+ async def get_upcoming_invoice(self) -> Optional[Dict[str, Any]]:
945
+ """Async version of AssistantRuntimeClient.get_upcoming_invoice."""
946
+ endpoint, payload = self._prepare_get_upcoming_invoice()
947
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
948
+
949
+ async def get_payment_methods(self) -> Optional[Dict[str, Any]]:
950
+ """Async version of AssistantRuntimeClient.get_payment_methods."""
951
+ endpoint, payload = self._prepare_get_payment_methods()
952
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
953
+
954
+ async def upgrade_plan(
955
+ self,
956
+ new_plan: str,
957
+ billing_cycle: str = "monthly",
958
+ gateway: Optional[str] = None,
959
+ billing_name: Optional[str] = None,
960
+ billing_email: Optional[str] = None,
961
+ promo_code: Optional[str] = None,
962
+ payment_method: Optional[str] = None,
963
+ ) -> Optional[Dict[str, Any]]:
964
+ """Async version of AssistantRuntimeClient.upgrade_plan."""
965
+ endpoint, payload = self._prepare_upgrade_plan(
966
+ new_plan, billing_cycle, gateway, billing_name, billing_email, promo_code,
967
+ payment_method=payment_method,
968
+ )
969
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
970
+
971
+ async def reauthorize_mandate(
972
+ self,
973
+ billing_name: Optional[str] = None,
974
+ payment_method: Optional[str] = None,
975
+ ) -> Optional[Dict[str, Any]]:
976
+ """Async version of AssistantRuntimeClient.reauthorize_mandate."""
977
+ endpoint, payload = self._prepare_reauthorize_mandate(
978
+ billing_name=billing_name,
979
+ payment_method=payment_method,
980
+ )
981
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
982
+
983
+ async def validate_promo_code(
984
+ self,
985
+ promo_code: str,
986
+ plan: Optional[str] = None,
987
+ timeout: Optional[float] = None,
988
+ ) -> Optional[Dict[str, Any]]:
989
+ """Async validate a promo code for this tenant."""
990
+ endpoint, payload = self._prepare_validate_promo_code(promo_code, plan)
991
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base, timeout=timeout)
992
+
993
+ async def downgrade_to_free(self) -> Optional[Dict[str, Any]]:
994
+ """Async version of AssistantRuntimeClient.downgrade_to_free."""
995
+ endpoint, payload = self._prepare_downgrade_to_free()
996
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
997
+
998
+ async def cancel_scheduled_change(self) -> Optional[Dict[str, Any]]:
999
+ """Async version of AssistantRuntimeClient.cancel_scheduled_change."""
1000
+ endpoint, payload = self._prepare_cancel_scheduled_change()
1001
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
1002
+
1003
+ async def cancel_subscription(self, cancel_immediately: bool = False) -> Optional[Dict[str, Any]]:
1004
+ """Async version of AssistantRuntimeClient.cancel_subscription."""
1005
+ endpoint, payload = self._prepare_cancel_subscription(cancel_immediately)
1006
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
1007
+
1008
+ async def reactivate_subscription(self) -> Optional[Dict[str, Any]]:
1009
+ """Async version of AssistantRuntimeClient.reactivate_subscription."""
1010
+ endpoint, payload = self._prepare_reactivate_subscription()
1011
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
1012
+
1013
+ async def pause_subscription(self) -> Optional[Dict[str, Any]]:
1014
+ """Async version of AssistantRuntimeClient.pause_subscription."""
1015
+ endpoint, payload = self._prepare_pause_subscription()
1016
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
1017
+
1018
+ async def resume_subscription(self) -> Optional[Dict[str, Any]]:
1019
+ """Async version of AssistantRuntimeClient.resume_subscription."""
1020
+ endpoint, payload = self._prepare_resume_subscription()
1021
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
1022
+
1023
+ async def update_payment_method(self) -> Optional[Dict[str, Any]]:
1024
+ """Async version of AssistantRuntimeClient.update_payment_method."""
1025
+ endpoint, payload = self._prepare_update_payment_method()
1026
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
1027
+
1028
+ async def get_subscription_status(self) -> Optional[Dict[str, Any]]:
1029
+ """Async version of AssistantRuntimeClient.get_subscription_status."""
1030
+ endpoint, params = self._prepare_get_subscription_status()
1031
+ return await self._request_get(endpoint, params, api_base=self.billing_api_base)
1032
+
1033
+ async def get_billing_history(self, limit: int = 20) -> Optional[Dict[str, Any]]:
1034
+ """Async version of AssistantRuntimeClient.get_billing_history."""
1035
+ endpoint, params = self._prepare_get_billing_history(limit)
1036
+ return await self._request_get(endpoint, params, api_base=self.billing_api_base)
1037
+
1038
+ async def get_billing_details(self) -> Optional[Dict[str, Any]]:
1039
+ """Async version of AssistantRuntimeClient.get_billing_details."""
1040
+ endpoint, params = self._prepare_get_billing_details()
1041
+ return await self._request_get(endpoint, params, api_base=self.billing_api_base)
1042
+
1043
+ async def save_billing_details(self, **billing_fields: Any) -> Optional[Dict[str, Any]]:
1044
+ """Async version of AssistantRuntimeClient.save_billing_details."""
1045
+ endpoint, payload = self._prepare_save_billing_details(billing_fields)
1046
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
1047
+
1048
+ # =========================================================================
1049
+ # Prepaid Credit APIs
1050
+ # =========================================================================
1051
+
1052
+ async def get_credit_balance(self) -> Optional[Dict[str, Any]]:
1053
+ """Async version of AssistantRuntimeClient.get_credit_balance."""
1054
+ endpoint, params = self._prepare_get_credit_balance()
1055
+ return await self._request_get(endpoint, params, api_base=self.billing_api_base)
1056
+
1057
+ async def purchase_credits(
1058
+ self, credit_amount: int, gateway: str = None
1059
+ ) -> Optional[Dict[str, Any]]:
1060
+ """Async version of AssistantRuntimeClient.purchase_credits."""
1061
+ endpoint, payload = self._prepare_purchase_credits(credit_amount, gateway)
1062
+ return await self._request_post_json(endpoint, payload, api_base=self.billing_api_base)
1063
+
1064
+ async def get_expiring_credits(self) -> Optional[Dict[str, Any]]:
1065
+ """Async version of AssistantRuntimeClient.get_expiring_credits."""
1066
+ endpoint, params = self._prepare_get_expiring_credits()
1067
+ return await self._request_get(endpoint, params, api_base=self.billing_api_base)
1068
+
1069
+ async def get_consumption_breakdown(self, days: int = 30) -> Optional[Dict[str, Any]]:
1070
+ """Async version of AssistantRuntimeClient.get_consumption_breakdown."""
1071
+ endpoint, params = self._prepare_get_consumption_breakdown(days)
1072
+ return await self._request_get(endpoint, params, api_base=self.billing_api_base)
1073
+
1074
+ # =========================================================================
1075
+ # Conversation APIs
1076
+ # =========================================================================
1077
+
1078
+ async def list_conversations(
1079
+ self,
1080
+ user_id: Optional[str] = None,
1081
+ limit: int = 50,
1082
+ offset: int = 0,
1083
+ include_deleted: bool = False,
1084
+ from_date: Optional[str] = None,
1085
+ to_date: Optional[str] = None,
1086
+ ) -> Optional[Dict[str, Any]]:
1087
+ """Async version of AssistantRuntimeClient.list_conversations."""
1088
+ endpoint, params = self._prepare_list_conversations(user_id, limit, offset, include_deleted, from_date, to_date)
1089
+ return await self._request_get(endpoint, params)
1090
+
1091
+ async def get_conversation(self, conversation_id: str) -> Optional[Dict[str, Any]]:
1092
+ """Async version of AssistantRuntimeClient.get_conversation."""
1093
+ endpoint, params = self._prepare_get_conversation(conversation_id)
1094
+ return await self._request_get(endpoint, params)
1095
+
1096
+ async def get_messages(
1097
+ self,
1098
+ conversation_id: str,
1099
+ limit: int = 100,
1100
+ offset: int = 0,
1101
+ include_deleted: bool = False,
1102
+ ) -> Optional[Dict[str, Any]]:
1103
+ """Async version of AssistantRuntimeClient.get_messages."""
1104
+ endpoint, params = self._prepare_get_messages(conversation_id, limit, offset, include_deleted)
1105
+ return await self._request_get(endpoint, params)
1106
+
1107
+ async def create_message(
1108
+ self,
1109
+ conversation_id: str,
1110
+ message_id: str,
1111
+ role: str,
1112
+ content: str,
1113
+ user_id: Optional[str] = None,
1114
+ tokens_used: int = 0,
1115
+ context: Optional[Dict[str, Any]] = None,
1116
+ ) -> Optional[Dict[str, Any]]:
1117
+ """Async version of AssistantRuntimeClient.create_message."""
1118
+ endpoint, payload = self._prepare_create_message(conversation_id, message_id, role, content, user_id, tokens_used, context)
1119
+ return await self._request_post_json(endpoint, payload)
1120
+
1121
+ async def update_conversation(
1122
+ self,
1123
+ conversation_id: str,
1124
+ title: Optional[str] = None,
1125
+ user_id: Optional[str] = None,
1126
+ ) -> Optional[Dict[str, Any]]:
1127
+ """Async version of AssistantRuntimeClient.update_conversation."""
1128
+ endpoint, payload = self._prepare_update_conversation(conversation_id, title, user_id)
1129
+ return await self._request_post_json(endpoint, payload)
1130
+
1131
+ async def delete_conversation(self, conversation_id: str, hard_delete: bool = False) -> Optional[Dict[str, Any]]:
1132
+ """Delete a conversation. Use hard_delete=True for permanent GDPR erasure."""
1133
+ endpoint, payload = self._prepare_delete_conversation(conversation_id, hard_delete)
1134
+ return await self._request_post_json(endpoint, payload)
1135
+
1136
+ async def delete_message(self, conversation_id: str, message_id: str) -> Optional[Dict[str, Any]]:
1137
+ """Async version of AssistantRuntimeClient.delete_message."""
1138
+ endpoint, payload = self._prepare_delete_message(conversation_id, message_id)
1139
+ return await self._request_post_json(endpoint, payload)
1140
+
1141
+ async def get_sync_stats(self) -> Optional[Dict[str, Any]]:
1142
+ """Async version of AssistantRuntimeClient.get_sync_stats."""
1143
+ endpoint, params = self._prepare_get_sync_stats()
1144
+ return await self._request_get(endpoint, params)
1145
+
1146
+ # =========================================================================
1147
+ # Streaming Events APIs (Historical)
1148
+ # =========================================================================
1149
+
1150
+ async def get_message_events(
1151
+ self,
1152
+ conversation_id: str,
1153
+ message_id: Optional[str] = None,
1154
+ event_types: Optional[list] = None,
1155
+ limit: int = 100,
1156
+ offset: int = 0,
1157
+ ) -> Optional[Dict[str, Any]]:
1158
+ """Async version of AssistantRuntimeClient.get_message_events."""
1159
+ endpoint, params = self._prepare_get_message_events(conversation_id, message_id, event_types, limit, offset)
1160
+ return await self._request_get(endpoint, params)
1161
+
1162
+ async def get_tool_execution_stats(
1163
+ self,
1164
+ conversation_id: Optional[str] = None,
1165
+ from_date: Optional[str] = None,
1166
+ to_date: Optional[str] = None,
1167
+ ) -> Optional[Dict[str, Any]]:
1168
+ """Async version of AssistantRuntimeClient.get_tool_execution_stats."""
1169
+ endpoint, params = self._prepare_get_tool_execution_stats(conversation_id, from_date, to_date)
1170
+ return await self._request_get(endpoint, params)
1171
+
1172
+ # =========================================================================
1173
+ # User & MCP Server APIs
1174
+ # =========================================================================
1175
+
1176
+ async def register_user(
1177
+ self,
1178
+ user_id: str,
1179
+ display_name: Optional[str] = None,
1180
+ custom_instructions: Optional[str] = None,
1181
+ locale: Optional[str] = None,
1182
+ timezone: Optional[str] = None,
1183
+ user_role: Optional[str] = None,
1184
+ email: Optional[str] = None,
1185
+ registered_by: Optional[str] = None,
1186
+ ) -> Dict[str, Any]:
1187
+ """Async version of AssistantRuntimeClient.register_user."""
1188
+ endpoint, params = self._prepare_register_user(
1189
+ user_id, display_name, custom_instructions,
1190
+ locale=locale, timezone=timezone, user_role=user_role, email=email,
1191
+ registered_by=registered_by,
1192
+ )
1193
+ return await self._request_post_form(endpoint, params)
1194
+
1195
+ async def invite_user(
1196
+ self,
1197
+ user_id: str,
1198
+ user_role: Optional[str] = None,
1199
+ invited_by: Optional[str] = None,
1200
+ ) -> Dict[str, Any]:
1201
+ """Async version of AssistantRuntimeClient.invite_user."""
1202
+ endpoint, params = self._prepare_invite_user(user_id, user_role, invited_by)
1203
+ return await self._request_post_form(endpoint, params)
1204
+
1205
+ async def revoke_invite(
1206
+ self,
1207
+ user_id: str,
1208
+ revoked_by: Optional[str] = None,
1209
+ ) -> Dict[str, Any]:
1210
+ """Async version of AssistantRuntimeClient.revoke_invite."""
1211
+ endpoint, params = self._prepare_revoke_invite(user_id, revoked_by)
1212
+ return await self._request_post_form(endpoint, params)
1213
+
1214
+ async def resend_invite(
1215
+ self,
1216
+ user_id: str,
1217
+ resent_by: Optional[str] = None,
1218
+ ) -> Dict[str, Any]:
1219
+ """Async version of AssistantRuntimeClient.resend_invite."""
1220
+ endpoint, params = self._prepare_resend_invite(user_id, resent_by)
1221
+ return await self._request_post_form(endpoint, params)
1222
+
1223
+ async def list_invites(self) -> Dict[str, Any]:
1224
+ """Async version of AssistantRuntimeClient.list_invites."""
1225
+ endpoint, params = self._prepare_list_invites()
1226
+ return await self._request_get(endpoint, params)
1227
+
1228
+ async def get_member_audit_log(self, limit: int = 100, offset: int = 0) -> Dict[str, Any]:
1229
+ """Async version of AssistantRuntimeClient.get_member_audit_log."""
1230
+ endpoint, params = self._prepare_get_member_audit_log(limit, offset)
1231
+ return await self._request_get(endpoint, params)
1232
+
1233
+ async def get_user(self, user_id: str) -> Optional[Dict[str, Any]]:
1234
+ """Async version of AssistantRuntimeClient.get_user."""
1235
+ endpoint, params = self._prepare_get_user(user_id)
1236
+ return await self._request_get(endpoint, params)
1237
+
1238
+ async def get_user_auth_status(self, user_id: str) -> Dict[str, Any]:
1239
+ """Async version of AssistantRuntimeClient.get_user_auth_status.
1240
+
1241
+ See sync client docstring for the ``_ar_unreachable`` contract.
1242
+ """
1243
+ endpoint, params = self._prepare_get_user_auth_status(user_id)
1244
+ try:
1245
+ return await self._request_get(endpoint, params)
1246
+ except Exception as e:
1247
+ return {
1248
+ "_ar_unreachable": True,
1249
+ "error": str(e),
1250
+ }
1251
+
1252
+ async def add_user_mcp_server(
1253
+ self,
1254
+ user_id: str,
1255
+ server_name: str,
1256
+ endpoint_url: str,
1257
+ transport_type: str = "SSE",
1258
+ auth_type: str = "OAuth",
1259
+ oauth_client_id: Optional[str] = None,
1260
+ oauth_client_secret: Optional[str] = None,
1261
+ access_token: Optional[str] = None,
1262
+ refresh_token: Optional[str] = None,
1263
+ token_expires_in: int = 3600,
1264
+ api_key: Optional[str] = None,
1265
+ api_key_header: str = "Authorization",
1266
+ allowed_tools: Optional[list] = None,
1267
+ blocked_tools: Optional[list] = None,
1268
+ ) -> Dict[str, Any]:
1269
+ """Async version of AssistantRuntimeClient.add_user_mcp_server."""
1270
+ endpoint, params = self._prepare_add_user_mcp_server(
1271
+ user_id, server_name, endpoint_url, transport_type, auth_type,
1272
+ oauth_client_id, oauth_client_secret, access_token, refresh_token,
1273
+ token_expires_in, api_key, api_key_header, allowed_tools, blocked_tools,
1274
+ )
1275
+ return await self._request_post_form(endpoint, params)
1276
+
1277
+ async def get_user_mcp_servers(self, user_id: str) -> Dict[str, Any]:
1278
+ """Async version of AssistantRuntimeClient.get_user_mcp_servers."""
1279
+ endpoint, params = self._prepare_get_user_mcp_servers(user_id)
1280
+ try:
1281
+ return await self._request_get(endpoint, params)
1282
+ except Exception as e:
1283
+ return {"user_id": user_id, "mcp_servers": [], "error": str(e)}
1284
+
1285
+ async def update_mcp_server_tokens(
1286
+ self,
1287
+ user_id: str,
1288
+ server_name: str,
1289
+ access_token: str,
1290
+ refresh_token: Optional[str] = None,
1291
+ token_expires_in: int = 3600,
1292
+ ) -> Dict[str, Any]:
1293
+ """Async version of AssistantRuntimeClient.update_mcp_server_tokens."""
1294
+ endpoint, params = self._prepare_update_mcp_server_tokens(user_id, server_name, access_token, refresh_token, token_expires_in)
1295
+ return await self._request_post_form(endpoint, params)
1296
+
1297
+ async def remove_user_mcp_server(self, user_id: str, server_name: str) -> Dict[str, Any]:
1298
+ """Async version of AssistantRuntimeClient.remove_user_mcp_server."""
1299
+ endpoint, params = self._prepare_remove_user_mcp_server(user_id, server_name)
1300
+ return await self._request_delete(endpoint, params)
1301
+
1302
+ async def list_users(
1303
+ self,
1304
+ status: Optional[str] = None,
1305
+ limit: int = 50,
1306
+ offset: int = 0,
1307
+ include_mcp_count: bool = True,
1308
+ ) -> Dict[str, Any]:
1309
+ """Async version of AssistantRuntimeClient.list_users."""
1310
+ endpoint, params = self._prepare_list_users(status, limit, offset, include_mcp_count)
1311
+ return await self._request_get(endpoint, params)
1312
+
1313
+ async def update_user(
1314
+ self,
1315
+ user_id: str,
1316
+ display_name: Optional[str] = None,
1317
+ custom_instructions: Optional[str] = None,
1318
+ locale: Optional[str] = None,
1319
+ timezone: Optional[str] = None,
1320
+ user_role: Optional[str] = None,
1321
+ email: Optional[str] = None,
1322
+ job_title: Optional[str] = None,
1323
+ department: Optional[str] = None,
1324
+ about: Optional[str] = None,
1325
+ ) -> Dict[str, Any]:
1326
+ """Async version of AssistantRuntimeClient.update_user."""
1327
+ endpoint, params = self._prepare_update_user(
1328
+ user_id, display_name, custom_instructions,
1329
+ locale=locale, timezone=timezone, user_role=user_role, email=email,
1330
+ job_title=job_title, department=department, about=about,
1331
+ )
1332
+ return await self._request_post_form(endpoint, params)
1333
+
1334
+ async def deregister_user(self, user_id: str) -> Dict[str, Any]:
1335
+ """Async version of AssistantRuntimeClient.deregister_user."""
1336
+ endpoint, params = self._prepare_deregister_user(user_id)
1337
+ return await self._request_post_form(endpoint, params)
1338
+
1339
+ async def get_user_limit_status(self) -> Dict[str, Any]:
1340
+ """Async version of AssistantRuntimeClient.get_user_limit_status."""
1341
+ endpoint, params = self._prepare_get_user_limit_status()
1342
+ return await self._request_get(endpoint, params)
1343
+
1344
+ async def suspend_user(self, user_id: str) -> Dict[str, Any]:
1345
+ """Async version of AssistantRuntimeClient.suspend_user."""
1346
+ endpoint, params = self._prepare_suspend_user(user_id)
1347
+ return await self._request_post_form(endpoint, params)
1348
+
1349
+ async def revoke_user(self, user_id: str) -> Dict[str, Any]:
1350
+ """Async version of AssistantRuntimeClient.revoke_user."""
1351
+ endpoint, params = self._prepare_revoke_user(user_id)
1352
+ return await self._request_post_form(endpoint, params)
1353
+
1354
+ async def set_user_credit_limit(
1355
+ self,
1356
+ user_id: str,
1357
+ monthly_credit_limit: float = 0,
1358
+ acted_by: Optional[str] = None,
1359
+ ) -> Dict[str, Any]:
1360
+ """Async version: Set per-user monthly credit limit."""
1361
+ endpoint = "users.set_user_credit_limit"
1362
+ params = {
1363
+ "tenant_id": self.tenant_id,
1364
+ "user_id": user_id,
1365
+ "monthly_credit_limit": str(monthly_credit_limit),
1366
+ }
1367
+ if acted_by:
1368
+ params["acted_by"] = acted_by
1369
+ return await self._request_post_form(endpoint, params)
1370
+
1371
+ async def get_my_credit_status(self, user_id: str) -> Dict[str, Any]:
1372
+ """Async version: Get credit usage status for a specific user."""
1373
+ endpoint = "users.get_my_credit_status"
1374
+ params = {
1375
+ "tenant_id": self.tenant_id,
1376
+ "user_id": user_id,
1377
+ }
1378
+ return await self._request_get(endpoint, params)
1379
+
1380
+ # =========================================================================
1381
+ # Workflow APIs
1382
+ # =========================================================================
1383
+
1384
+ async def create_workflow(
1385
+ self,
1386
+ workflow_name: str,
1387
+ graph_json: Optional[str] = None,
1388
+ description: str = "",
1389
+ default_model_id: Optional[str] = None,
1390
+ default_user_id: Optional[str] = None,
1391
+ error_strategy: str = "fail_fast",
1392
+ timeout_seconds: int = 600,
1393
+ ) -> Optional[Dict[str, Any]]:
1394
+ """Async version of AssistantRuntimeClient.create_workflow."""
1395
+ endpoint, payload = self._prepare_create_workflow(
1396
+ workflow_name, graph_json, description, default_model_id,
1397
+ default_user_id, error_strategy, timeout_seconds,
1398
+ )
1399
+ return await self._request_post_json(endpoint, payload, api_base=self.workflows_api_base)
1400
+
1401
+ async def get_workflow(
1402
+ self,
1403
+ name: Optional[str] = None,
1404
+ workflow_name: Optional[str] = None,
1405
+ ) -> Optional[Dict[str, Any]]:
1406
+ """Async version of AssistantRuntimeClient.get_workflow."""
1407
+ endpoint, params = self._prepare_get_workflow(name, workflow_name)
1408
+ return await self._request_get(endpoint, params, api_base=self.workflows_api_base)
1409
+
1410
+ async def update_workflow(
1411
+ self,
1412
+ name: str,
1413
+ graph_json: Optional[str] = None,
1414
+ workflow_name: Optional[str] = None,
1415
+ description: Optional[str] = None,
1416
+ status: Optional[str] = None,
1417
+ default_model_id: Optional[str] = None,
1418
+ default_user_id: Optional[str] = None,
1419
+ error_strategy: Optional[str] = None,
1420
+ timeout_seconds: Optional[int] = None,
1421
+ max_node_executions: Optional[int] = None,
1422
+ max_retries: Optional[int] = None,
1423
+ ) -> Optional[Dict[str, Any]]:
1424
+ """Async version of AssistantRuntimeClient.update_workflow."""
1425
+ endpoint, payload = self._prepare_update_workflow(
1426
+ name, graph_json, workflow_name, description, status,
1427
+ default_model_id, default_user_id, error_strategy,
1428
+ timeout_seconds, max_node_executions, max_retries,
1429
+ )
1430
+ return await self._request_post_json(endpoint, payload, api_base=self.workflows_api_base)
1431
+
1432
+ async def delete_workflow(self, name: str) -> Optional[Dict[str, Any]]:
1433
+ """Async version of AssistantRuntimeClient.delete_workflow."""
1434
+ endpoint, payload = self._prepare_delete_workflow(name)
1435
+ return await self._request_post_json(endpoint, payload, api_base=self.workflows_api_base)
1436
+
1437
+ async def list_workflows(
1438
+ self,
1439
+ status: Optional[str] = None,
1440
+ page: int = 0,
1441
+ page_size: int = 20,
1442
+ ) -> Optional[Dict[str, Any]]:
1443
+ """Async version of AssistantRuntimeClient.list_workflows."""
1444
+ endpoint, params = self._prepare_list_workflows(status, page, page_size)
1445
+ return await self._request_get(endpoint, params, api_base=self.workflows_api_base)
1446
+
1447
+ async def execute_workflow(
1448
+ self,
1449
+ name: str,
1450
+ input_data: Optional[str] = None,
1451
+ user_id: Optional[str] = None,
1452
+ ) -> Optional[Dict[str, Any]]:
1453
+ """Async version of AssistantRuntimeClient.execute_workflow."""
1454
+ endpoint, payload = self._prepare_execute_workflow(name, input_data, user_id)
1455
+ return await self._request_post_json(endpoint, payload, api_base=self.workflows_api_base)
1456
+
1457
+ async def execute_workflow_from_event(
1458
+ self,
1459
+ workflow_name: str,
1460
+ input_data: Dict[str, Any],
1461
+ user_id: str,
1462
+ trigger_id: str,
1463
+ ) -> Optional[Dict[str, Any]]:
1464
+ """Async version of AssistantRuntimeClient.execute_workflow_from_event."""
1465
+ endpoint, payload = self._prepare_execute_workflow_from_event(
1466
+ workflow_name, input_data, user_id, trigger_id,
1467
+ )
1468
+ return await self._request_post_json(endpoint, payload, api_base=self.workflows_api_base)
1469
+
1470
+ async def cancel_workflow_run(self, run_name: str) -> Optional[Dict[str, Any]]:
1471
+ """Async version of AssistantRuntimeClient.cancel_workflow_run."""
1472
+ endpoint, payload = self._prepare_cancel_workflow_run(run_name)
1473
+ return await self._request_post_json(endpoint, payload, api_base=self.workflows_api_base)
1474
+
1475
+ async def get_workflow_run(self, run_name: str) -> Optional[Dict[str, Any]]:
1476
+ """Async version of AssistantRuntimeClient.get_workflow_run."""
1477
+ endpoint, params = self._prepare_get_workflow_run(run_name)
1478
+ return await self._request_get(endpoint, params, api_base=self.workflows_api_base)
1479
+
1480
+ async def list_workflow_runs(
1481
+ self,
1482
+ workflow_name: Optional[str] = None,
1483
+ status: Optional[str] = None,
1484
+ page: int = 0,
1485
+ page_size: int = 20,
1486
+ ) -> Optional[Dict[str, Any]]:
1487
+ """Async version of AssistantRuntimeClient.list_workflow_runs."""
1488
+ endpoint, params = self._prepare_list_workflow_runs(workflow_name, status, page, page_size)
1489
+ return await self._request_get(endpoint, params, api_base=self.workflows_api_base)
1490
+
1491
+ async def get_workflow_audit_summary(
1492
+ self,
1493
+ workflow_id: str,
1494
+ window: str = "last_7_days",
1495
+ ) -> Optional[Dict[str, Any]]:
1496
+ """Async version of AssistantRuntimeClient.get_workflow_audit_summary."""
1497
+ endpoint, params = self._prepare_get_workflow_audit_summary(workflow_id, window)
1498
+ return await self._request_get(endpoint, params, api_base=self.workflows_api_base)
1499
+
1500
+ async def set_workflow_schedule(
1501
+ self,
1502
+ name: str,
1503
+ cron_expression: str,
1504
+ timezone: str = "UTC",
1505
+ enabled: bool = True,
1506
+ default_input: Optional[str] = None,
1507
+ ) -> Optional[Dict[str, Any]]:
1508
+ """Async version of AssistantRuntimeClient.set_workflow_schedule."""
1509
+ endpoint, payload = self._prepare_set_workflow_schedule(name, cron_expression, timezone, enabled, default_input)
1510
+ return await self._request_post_json(endpoint, payload, api_base=self.workflows_api_base)
1511
+
1512
+ async def validate_workflow_graph(self, graph_json: str) -> Optional[Dict[str, Any]]:
1513
+ """Async version of AssistantRuntimeClient.validate_workflow_graph."""
1514
+ endpoint, payload = self._prepare_validate_workflow_graph(graph_json)
1515
+ return await self._request_post_json(endpoint, payload, api_base=self.workflows_api_base)
1516
+
1517
+ async def test_workflow_node(
1518
+ self,
1519
+ node_json: str,
1520
+ input_text: str = "Test input",
1521
+ default_model_id: Optional[str] = None,
1522
+ default_user_id: Optional[str] = None,
1523
+ ) -> Optional[Dict[str, Any]]:
1524
+ """Async version of AssistantRuntimeClient.test_workflow_node."""
1525
+ endpoint, payload = self._prepare_test_workflow_node(node_json, input_text, default_model_id, default_user_id)
1526
+ return await self._request_post_json(endpoint, payload, timeout=120.0, api_base=self.workflows_api_base)
1527
+
1528
+ async def resolve_workflow_tools(
1529
+ self,
1530
+ user_id: str,
1531
+ tool_directives: List[Dict[str, Any]],
1532
+ ) -> Optional[Dict[str, Any]]:
1533
+ """Async version of AssistantRuntimeClient.resolve_workflow_tools."""
1534
+ endpoint, payload = self._prepare_resolve_workflow_tools(user_id, tool_directives)
1535
+ return await self._request_post_json(endpoint, payload, api_base=self.workflows_api_base)
1536
+
1537
+ async def run_workflow_node(
1538
+ self,
1539
+ name: str,
1540
+ node_id: str,
1541
+ input_text: str = "Test input",
1542
+ user_id: Optional[str] = None,
1543
+ ) -> Optional[Dict[str, Any]]:
1544
+ """Async version of AssistantRuntimeClient.run_workflow_node."""
1545
+ endpoint, payload = self._prepare_run_workflow_node(name, node_id, input_text, user_id)
1546
+ return await self._request_post_json(endpoint, payload, api_base=self.workflows_api_base, timeout=120.0)
1547
+
1548
+ # --- Workflow Templates ---
1549
+
1550
+ async def export_workflow(
1551
+ self,
1552
+ name: str,
1553
+ template_name: Optional[str] = None,
1554
+ category: str = "General",
1555
+ save_as_template: bool = False,
1556
+ is_public: bool = False,
1557
+ ) -> Optional[Dict[str, Any]]:
1558
+ """Async version of AssistantRuntimeClient.export_workflow."""
1559
+ endpoint, payload = self._prepare_export_workflow(name, template_name, category, save_as_template, is_public)
1560
+ return await self._request_post_json(endpoint, payload, api_base=self.workflows_api_base)
1561
+
1562
+ # `list_templates`, `get_template`, `import_template`, `update_template`,
1563
+ # and `delete_template` removed in chunk 4 — use the async marketplace
1564
+ # methods (`list_listings(listing_type="Workflow")`, `get_listing`,
1565
+ # `import_listing`, `update_listing`, `delete_listing`) instead.
1566
+
1567
+ async def upload_listing_from_json(
1568
+ self,
1569
+ file_path: str,
1570
+ user_id: str,
1571
+ is_public: bool = False,
1572
+ is_published: bool = True,
1573
+ plan_tier: Optional[str] = None,
1574
+ timeout: Optional[float] = None,
1575
+ ) -> Optional[Dict[str, Any]]:
1576
+ """Async upload an ``ar_workflow_template_v1`` JSON file + create a listing."""
1577
+ payload: Dict[str, Any] = {
1578
+ "tenant_id": self.tenant_id,
1579
+ "user_id": user_id,
1580
+ "is_public": "1" if is_public else "0",
1581
+ "is_published": "1" if is_published else "0",
1582
+ }
1583
+ if plan_tier:
1584
+ payload["plan_tier"] = plan_tier
1585
+ url = f"{self.marketplace_api_base}.publishing.upload_listing_from_json"
1586
+ payload = self._with_site_url(payload)
1587
+ headers = self._get_headers(payload, for_query_string=False)
1588
+ timeout_val = timeout or self.timeout
1589
+
1590
+ data = aiohttp.FormData()
1591
+ for k, v in payload.items():
1592
+ data.add_field(k, str(v))
1593
+ with open(file_path, "rb") as f:
1594
+ data.add_field(
1595
+ "file", f.read(),
1596
+ filename=os.path.basename(file_path),
1597
+ content_type="application/json",
1598
+ )
1599
+
1600
+ async with self.session.post(
1601
+ url, data=data, headers=headers,
1602
+ timeout=aiohttp.ClientTimeout(total=timeout_val),
1603
+ ) as resp:
1604
+ resp.raise_for_status()
1605
+ result = await resp.json()
1606
+ return result.get("message", result)
1607
+
1608
+ # `rate_template` removed in chunk 4 — use `rate_listing` instead.
1609
+ # `download_template` removed in chunk 5 — use `download_listing_as_json` instead.
1610
+
1611
+ # --- GDPR / Privacy ---
1612
+
1613
+ async def export_user_data(self, user_id: str) -> Optional[Dict[str, Any]]:
1614
+ """Export all user data as structured JSON (GDPR Article 20)."""
1615
+ endpoint, payload = self._prepare_export_user_data(user_id)
1616
+ return await self._request_post_json(endpoint, payload)
1617
+
1618
+ async def erase_user_data(self, user_id: str) -> Optional[Dict[str, Any]]:
1619
+ """Permanently delete all user data (GDPR Article 17)."""
1620
+ endpoint, payload = self._prepare_erase_user_data(user_id)
1621
+ return await self._request_post_json(endpoint, payload)
1622
+
1623
+ async def rectify_user_data(self, user_id: str, updates: dict) -> Optional[Dict[str, Any]]:
1624
+ """Update user's personal data (GDPR Article 16)."""
1625
+ endpoint, payload = self._prepare_rectify_user_data(user_id, updates)
1626
+ return await self._request_post_json(endpoint, payload)
1627
+
1628
+ async def restrict_user_processing(self, user_id: str, restrict: bool = True) -> Optional[Dict[str, Any]]:
1629
+ """Restrict or unrestrict data processing for a user (GDPR Article 18)."""
1630
+ endpoint, payload = self._prepare_restrict_user_processing(user_id, restrict)
1631
+ return await self._request_post_json(endpoint, payload)
1632
+
1633
+ async def update_user_consent(self, user_id: str, consent_type: str, granted: bool = True) -> Optional[Dict[str, Any]]:
1634
+ """Update user consent for a specific processing activity."""
1635
+ endpoint, payload = self._prepare_update_user_consent(user_id, consent_type, granted)
1636
+ return await self._request_post_json(endpoint, payload)
1637
+
1638
+ async def create_ticket(self, user_id: str, subject: str, description: str,
1639
+ category: Optional[str] = None,
1640
+ conversation_id: Optional[str] = None,
1641
+ environment: Optional[dict] = None,
1642
+ attachment_ids: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
1643
+ """Raise a support ticket. Returns {ticket_id, portal_link}."""
1644
+ endpoint, payload = self._prepare_create_ticket(
1645
+ user_id, subject, description, category, conversation_id, environment,
1646
+ attachment_ids)
1647
+ return await self._request_post_json(endpoint, payload)
1648
+
1649
+ async def submit_feedback(self, user_id: str, rating: Optional[int] = None,
1650
+ comment: Optional[str] = None,
1651
+ category: Optional[str] = None,
1652
+ conversation_id: Optional[str] = None,
1653
+ environment: Optional[dict] = None) -> Optional[Dict[str, Any]]:
1654
+ """Submit product/service feedback. Returns {feedback_id}."""
1655
+ endpoint, payload = self._prepare_submit_feedback(
1656
+ user_id, rating, comment, category, conversation_id, environment)
1657
+ return await self._request_post_json(endpoint, payload)
1658
+
1659
+ async def list_tickets(self, user_id: str, status: Optional[str] = None) -> Optional[Dict[str, Any]]:
1660
+ """List the user's support tickets. Returns {tickets: [...]}."""
1661
+ endpoint, payload = self._prepare_list_tickets(user_id, status)
1662
+ return await self._request_post_json(endpoint, payload)
1663
+
1664
+ async def get_ticket_thread(self, user_id: str, ticket_id: str) -> Optional[Dict[str, Any]]:
1665
+ """Get a ticket's full conversation thread. Returns {ticket, messages: [...]}."""
1666
+ endpoint, payload = self._prepare_get_ticket_thread(user_id, ticket_id)
1667
+ return await self._request_post_json(endpoint, payload)
1668
+
1669
+ async def reply_to_ticket(self, user_id: str, ticket_id: str, message: str,
1670
+ attachment_ids: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
1671
+ """Post a reply to a ticket. Returns {message_id}."""
1672
+ endpoint, payload = self._prepare_reply_to_ticket(user_id, ticket_id, message, attachment_ids)
1673
+ return await self._request_post_json(endpoint, payload)
1674
+
1675
+ async def upload_ticket_attachment(
1676
+ self,
1677
+ user_id: str,
1678
+ file_name: str,
1679
+ file_data: bytes,
1680
+ content_type: Optional[str] = None,
1681
+ ) -> Optional[Dict[str, Any]]:
1682
+ """Async version of AssistantRuntimeClient.upload_ticket_attachment."""
1683
+ endpoint, params, f_field, f_name, f_data, c_type = self._prepare_upload_ticket_attachment(
1684
+ user_id, file_name, file_data, content_type,
1685
+ )
1686
+ return await self._request_post_multipart(
1687
+ endpoint, params=params, file_field=f_field,
1688
+ file_name=f_name, file_data=f_data, content_type=c_type,
1689
+ timeout=120.0,
1690
+ )
1691
+
1692
+ async def get_tenant_privacy_config(self) -> Optional[Dict[str, Any]]:
1693
+ """Get tenant's privacy policy configuration."""
1694
+ endpoint, payload = self._prepare_get_tenant_privacy_config()
1695
+ return await self._request_post_json(endpoint, payload)
1696
+
1697
+ async def update_tenant_privacy_config(self, config: dict) -> Optional[Dict[str, Any]]:
1698
+ """Update tenant's privacy policy configuration."""
1699
+ endpoint, payload = self._prepare_update_tenant_privacy_config(config)
1700
+ return await self._request_post_json(endpoint, payload)
1701
+
1702
+ # --- Heartbeat & Notifications ---
1703
+
1704
+ async def heartbeat(
1705
+ self,
1706
+ faco_version: Optional[str] = None,
1707
+ fac_version: Optional[str] = None,
1708
+ frappe_version: Optional[str] = None,
1709
+ erpnext_version: Optional[str] = None,
1710
+ python_version: Optional[str] = None,
1711
+ copilot_version: Optional[str] = None,
1712
+ timeout: Optional[float] = None,
1713
+ ) -> Optional[Dict[str, Any]]:
1714
+ """Async send version/health info and receive pending notifications."""
1715
+ endpoint, payload = self._prepare_heartbeat(
1716
+ faco_version, fac_version, frappe_version, erpnext_version,
1717
+ python_version, copilot_version,
1718
+ )
1719
+ return await self._request_post_json(endpoint, payload, timeout=timeout)
1720
+
1721
+ async def get_notifications(self, user_id: str, timeout: Optional[float] = None) -> Optional[Dict[str, Any]]:
1722
+ """Async fetch pending notifications for a user."""
1723
+ endpoint, payload = self._prepare_get_notifications(user_id)
1724
+ return await self._request_post_json(endpoint, payload, timeout=timeout)
1725
+
1726
+ async def dismiss_notification(
1727
+ self,
1728
+ notification_id: str,
1729
+ user_id: str,
1730
+ timeout: Optional[float] = None,
1731
+ ) -> Optional[Dict[str, Any]]:
1732
+ """Async record that a user has dismissed a notification."""
1733
+ endpoint, payload = self._prepare_dismiss_notification(notification_id, user_id)
1734
+ return await self._request_post_json(endpoint, payload, timeout=timeout)
1735
+
1736
+ # -------------------------------------------------------------------
1737
+ # Marketplace API — async, routes via marketplace_api_base
1738
+ # -------------------------------------------------------------------
1739
+
1740
+ async def list_listings(
1741
+ self,
1742
+ listing_type: Optional[str] = None,
1743
+ category: Optional[str] = None,
1744
+ search: Optional[str] = None,
1745
+ featured_only: bool = False,
1746
+ min_rating: Optional[float] = None,
1747
+ plan_tier: Optional[str] = None,
1748
+ sort_by: Optional[str] = None,
1749
+ page: int = 0,
1750
+ page_size: int = 20,
1751
+ user_id: Optional[str] = None,
1752
+ timeout: Optional[float] = None,
1753
+ ) -> Optional[Dict[str, Any]]:
1754
+ endpoint, params = self._prepare_list_listings(
1755
+ listing_type, category, search, featured_only, min_rating,
1756
+ plan_tier, sort_by, page, page_size, user_id,
1757
+ )
1758
+ return await self._request_get(endpoint, params, api_base=self.marketplace_api_base, timeout=timeout)
1759
+
1760
+ async def get_listing(
1761
+ self, name: str, user_id: Optional[str] = None,
1762
+ include_source: bool = True,
1763
+ ) -> Optional[Dict[str, Any]]:
1764
+ endpoint, params = self._prepare_get_listing(name, user_id, include_source)
1765
+ return await self._request_get(endpoint, params, api_base=self.marketplace_api_base)
1766
+
1767
+ async def import_listing(
1768
+ self,
1769
+ user_id: str,
1770
+ name: str,
1771
+ new_title: Optional[str] = None,
1772
+ variables: Optional[str] = None,
1773
+ default_model_id: Optional[str] = None,
1774
+ ) -> Optional[Dict[str, Any]]:
1775
+ endpoint, payload = self._prepare_import_listing(
1776
+ user_id, name, new_title, variables, default_model_id,
1777
+ )
1778
+ return await self._request_post_json(endpoint, payload, api_base=self.marketplace_api_base)
1779
+
1780
+ async def update_listing(
1781
+ self,
1782
+ name: str,
1783
+ title: Optional[str] = None,
1784
+ short_description: Optional[str] = None,
1785
+ description: Optional[str] = None,
1786
+ category: Optional[str] = None,
1787
+ tags: Optional[str] = None,
1788
+ icon: Optional[str] = None,
1789
+ is_public: Optional[bool] = None,
1790
+ is_published: Optional[bool] = None,
1791
+ plan_tier: Optional[str] = None,
1792
+ ) -> Optional[Dict[str, Any]]:
1793
+ endpoint, payload = self._prepare_update_listing(
1794
+ name, title, short_description, description, category, tags,
1795
+ icon, is_public, is_published, plan_tier,
1796
+ )
1797
+ return await self._request_post_json(endpoint, payload, api_base=self.marketplace_api_base)
1798
+
1799
+ async def delete_listing(self, name: str) -> Optional[Dict[str, Any]]:
1800
+ endpoint, payload = self._prepare_delete_listing(name)
1801
+ return await self._request_post_json(endpoint, payload, api_base=self.marketplace_api_base)
1802
+
1803
+ async def rate_listing(
1804
+ self, user_id: str, listing: str, rating: int,
1805
+ review: Optional[str] = None,
1806
+ ) -> Optional[Dict[str, Any]]:
1807
+ endpoint, payload = self._prepare_rate_listing(user_id, listing, rating, review)
1808
+ return await self._request_post_json(endpoint, payload, api_base=self.marketplace_api_base)
1809
+
1810
+ async def report_listing(
1811
+ self, user_id: str, listing: str, reason: str,
1812
+ details: Optional[str] = None,
1813
+ ) -> Optional[Dict[str, Any]]:
1814
+ endpoint, payload = self._prepare_report_listing(user_id, listing, reason, details)
1815
+ return await self._request_post_json(endpoint, payload, api_base=self.marketplace_api_base)
1816
+
1817
+ async def list_pending_reviews(
1818
+ self, page: int = 0, page_size: int = 20,
1819
+ ) -> Optional[Dict[str, Any]]:
1820
+ endpoint, params = self._prepare_list_pending_reviews(page, page_size)
1821
+ return await self._request_get(endpoint, params, api_base=self.marketplace_api_base)
1822
+
1823
+ async def approve_listing(
1824
+ self, listing: str, notes: Optional[str] = None,
1825
+ ) -> Optional[Dict[str, Any]]:
1826
+ endpoint, payload = self._prepare_approve_listing(listing, notes)
1827
+ return await self._request_post_json(endpoint, payload, api_base=self.marketplace_api_base)
1828
+
1829
+ async def reject_listing(
1830
+ self, listing: str, notes: Optional[str] = None,
1831
+ ) -> Optional[Dict[str, Any]]:
1832
+ endpoint, payload = self._prepare_reject_listing(listing, notes)
1833
+ return await self._request_post_json(endpoint, payload, api_base=self.marketplace_api_base)
1834
+
1835
+ async def get_creator_stats(
1836
+ self, user_id: Optional[str] = None,
1837
+ ) -> Optional[Dict[str, Any]]:
1838
+ endpoint, params = self._prepare_get_creator_stats(user_id)
1839
+ return await self._request_get(endpoint, params, api_base=self.marketplace_api_base)
1840
+
1841
+ async def list_my_listings(
1842
+ self,
1843
+ user_id: Optional[str] = None,
1844
+ listing_type: Optional[str] = None,
1845
+ page: int = 0,
1846
+ page_size: int = 20,
1847
+ ) -> Optional[Dict[str, Any]]:
1848
+ endpoint, params = self._prepare_list_my_listings(user_id, listing_type, page, page_size)
1849
+ return await self._request_get(endpoint, params, api_base=self.marketplace_api_base)
1850
+
1851
+ # -------------------------------------------------------------------
1852
+ # Marketplace publishing + downloads + version checks (chunk 5)
1853
+ # -------------------------------------------------------------------
1854
+
1855
+ async def publish_workflow(
1856
+ self,
1857
+ user_id: str,
1858
+ workflow_name: str,
1859
+ template_name: Optional[str] = None,
1860
+ category: str = "General",
1861
+ short_description: Optional[str] = None,
1862
+ description: Optional[str] = None,
1863
+ tags: Optional[str] = None,
1864
+ is_public: bool = False,
1865
+ plan_tier: Optional[str] = None,
1866
+ ) -> Optional[Dict[str, Any]]:
1867
+ endpoint, payload = self._prepare_publish_workflow(
1868
+ user_id, workflow_name, template_name, category, short_description,
1869
+ description, tags, is_public, plan_tier,
1870
+ )
1871
+ return await self._request_post_json(endpoint, payload, api_base=self.marketplace_api_base)
1872
+
1873
+ async def download_listing_as_json(self, name: str) -> Optional[Dict[str, Any]]:
1874
+ endpoint, params = self._prepare_download_listing_as_json(name)
1875
+ return await self._request_get(endpoint, params, api_base=self.marketplace_api_base)
1876
+
1877
+ async def check_workflow_update(self, name: str) -> Optional[Dict[str, Any]]:
1878
+ endpoint, params = self._prepare_check_workflow_update(name)
1879
+ return await self._request_get(endpoint, params, api_base=self.marketplace_api_base)
1880
+
1881
+ async def check_all_workflow_updates(self) -> Optional[Dict[str, Any]]:
1882
+ endpoint, params = self._prepare_check_all_workflow_updates()
1883
+ return await self._request_get(endpoint, params, api_base=self.marketplace_api_base)
1884
+
1885
+ # -------------------------------------------------------------------
1886
+ # Tenant Packs API — admin-grade pack management for the calling tenant
1887
+ # -------------------------------------------------------------------
1888
+
1889
+ async def list_packs(self) -> Optional[Dict[str, Any]]:
1890
+ """Return every active pack with eligibility + enablement annotations."""
1891
+ endpoint, params = self._prepare_list_packs()
1892
+ return await self._request_get(endpoint, params, api_base=self.marketplace_api_base)
1893
+
1894
+ async def get_pack_contents(self, pack_id: str) -> Optional[Dict[str, Any]]:
1895
+ """Return the named prompts + skills of a pack the tenant owns."""
1896
+ endpoint, params = self._prepare_get_pack_contents(pack_id)
1897
+ return await self._request_get(endpoint, params, api_base=self.marketplace_api_base)
1898
+
1899
+ async def set_industry(
1900
+ self,
1901
+ industry: Optional[str],
1902
+ auto_enable: bool = True,
1903
+ ) -> Optional[Dict[str, Any]]:
1904
+ """Set the calling tenant's industry and optionally auto-enable its pack."""
1905
+ endpoint, payload = self._prepare_set_industry(industry, auto_enable)
1906
+ return await self._request_post_json(endpoint, payload, api_base=self.marketplace_api_base)
1907
+
1908
+ async def set_pack_enabled(
1909
+ self,
1910
+ pack_id: str,
1911
+ enabled: bool,
1912
+ source: str = "User",
1913
+ ) -> Optional[Dict[str, Any]]:
1914
+ """Toggle a pack on or off for the calling tenant."""
1915
+ endpoint, payload = self._prepare_set_pack_enabled(pack_id, enabled, source)
1916
+ return await self._request_post_json(endpoint, payload, api_base=self.marketplace_api_base)
1917
+
1918
+ async def get_recommended_pack(
1919
+ self, user_id: Optional[str] = None,
1920
+ ) -> Optional[Dict[str, Any]]:
1921
+ """Return at most one pack to surface in the onboarding recommendation toast."""
1922
+ endpoint, params = self._prepare_get_recommended_pack(user_id)
1923
+ return await self._request_get(endpoint, params, api_base=self.marketplace_api_base)
1924
+
1925
+ async def dismiss_pack_recommendation(self, user_id: str) -> Optional[Dict[str, Any]]:
1926
+ """Mark the recommendation toast as dismissed for the given user."""
1927
+ endpoint, payload = self._prepare_dismiss_pack_recommendation(user_id)
1928
+ return await self._request_post_json(endpoint, payload, api_base=self.marketplace_api_base)
1929
+
1930
+ # =========================================================================
1931
+ # HITL APIs
1932
+ # =========================================================================
1933
+
1934
+ async def get_pending_interrupt(self, session_id: str, user_id: str) -> Optional[Dict[str, Any]]:
1935
+ """Async version of AssistantRuntimeClient.get_pending_interrupt."""
1936
+ endpoint, params = self._prepare_get_pending_interrupt(session_id, user_id)
1937
+ return await self._request_get(endpoint, params, api_base=self.api_base)
1938
+
1939
+ async def cancel_session(self, session_id: str) -> Optional[Dict[str, Any]]:
1940
+ """Async version of AssistantRuntimeClient.cancel_session."""
1941
+ endpoint, payload = self._prepare_cancel_session(session_id)
1942
+ return await self._request_post_json(endpoint, payload, api_base=self.api_base)
1943
+
1944
+ # =========================================================================
1945
+ # Voice API
1946
+ # =========================================================================
1947
+
1948
+ async def transcribe_audio(
1949
+ self,
1950
+ audio_bytes: bytes,
1951
+ mime_type: str = "audio/webm",
1952
+ user_id: Optional[str] = None,
1953
+ duration_ms: int = 0,
1954
+ language: Optional[str] = None,
1955
+ ) -> Optional[Dict[str, Any]]:
1956
+ """
1957
+ Async transcribe an audio blob via AR's voice endpoint.
1958
+
1959
+ Args:
1960
+ audio_bytes: Raw audio bytes.
1961
+ mime_type: MIME type (default: audio/webm).
1962
+ user_id: User identifier for per-user billing attribution.
1963
+ duration_ms: Client-reported recording length (advisory only;
1964
+ billing uses the provider's authoritative duration).
1965
+ language: ISO-639-1 hint ("en", "es"…). Strongly recommended —
1966
+ without it Whisper auto-detects and frequently hallucinates
1967
+ outro phrases ("Thank you for watching") on silent clips.
1968
+
1969
+ Returns:
1970
+ {"text": str, "duration_seconds": float, "credits_consumed": float}
1971
+ """
1972
+ params: Dict[str, Any] = {
1973
+ "tenant_id": self.tenant_id,
1974
+ "duration_ms": duration_ms,
1975
+ }
1976
+ if user_id:
1977
+ params["user_id"] = user_id
1978
+ if language:
1979
+ params["language"] = language
1980
+ return await self._request_post_multipart(
1981
+ endpoint="transcribe_v1",
1982
+ params=params,
1983
+ file_field="audio",
1984
+ file_name="audio.webm",
1985
+ file_data=audio_bytes,
1986
+ content_type=mime_type,
1987
+ api_base=self.voice_api_base,
1988
+ )
1989
+