synapseone-sdk 0.7.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.
- synapsedx_sdk/__init__.py +11 -0
- synapseone_sdk/__init__.py +291 -0
- synapseone_sdk/_http.py +105 -0
- synapseone_sdk/ai.py +44 -0
- synapseone_sdk/base.py +744 -0
- synapseone_sdk/example.py +36 -0
- synapseone_sdk/faststore.py +358 -0
- synapseone_sdk/iam.py +314 -0
- synapseone_sdk/inference.py +74 -0
- synapseone_sdk/lds.py +188 -0
- synapseone_sdk/queue.py +233 -0
- synapseone_sdk/stream.py +167 -0
- synapseone_sdk/test_client.py +497 -0
- synapseone_sdk/test_services.py +806 -0
- synapseone_sdk/workflow.py +260 -0
- synapseone_sdk-0.7.0.dist-info/METADATA +141 -0
- synapseone_sdk-0.7.0.dist-info/RECORD +20 -0
- synapseone_sdk-0.7.0.dist-info/WHEEL +5 -0
- synapseone_sdk-0.7.0.dist-info/licenses/LICENSE +199 -0
- synapseone_sdk-0.7.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Copyright 2026 SynapseOne Contributors
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
"""Compatibility shim for the SynapseDX -> SynapseOne rebrand.
|
|
4
|
+
|
|
5
|
+
``synapsedx_sdk`` is deprecated; import ``synapseone_sdk`` instead. Everything
|
|
6
|
+
here is re-exported from the canonical package, including the legacy
|
|
7
|
+
``SynapseDXClient`` / ``SynapseDXError`` aliases.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from synapseone_sdk import * # noqa: F401,F403
|
|
11
|
+
from synapseone_sdk import __all__ as __all__ # noqa: F401
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Copyright 2026 SynapseOne Contributors
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
"""synapseone Python SDK — gateway client for the SynapseOne platform.
|
|
4
|
+
|
|
5
|
+
The SDK is split per service; use the namespace that matches the service you
|
|
6
|
+
talk to:
|
|
7
|
+
|
|
8
|
+
====================== =============================================
|
|
9
|
+
``client.iam`` auth, orgs/accounts/users, roles, policies,
|
|
10
|
+
API keys, credentials, service accounts
|
|
11
|
+
``client.workflow`` workflow templates, CRUD/lifecycle, executions,
|
|
12
|
+
test runs
|
|
13
|
+
``client.lds`` LDS datasources, attributes, records, NGSI-LD
|
|
14
|
+
entities, licenses
|
|
15
|
+
``client.base`` SynapseBase collections/documents, queries,
|
|
16
|
+
batches, counters, change feed, object mapping
|
|
17
|
+
``client.faststore`` Synapse Faststore cache/KV, hashes, lists, sets,
|
|
18
|
+
sorted sets, locks
|
|
19
|
+
``client.queue`` SynapseQueue publish/consume/ack, dead letters
|
|
20
|
+
====================== =============================================
|
|
21
|
+
|
|
22
|
+
The old flat methods on :class:`SynapseOneClient` still work but are deprecated:
|
|
23
|
+
each one emits a ``DeprecationWarning`` and forwards to the namespaced call
|
|
24
|
+
(e.g. ``client.list_templates()`` -> ``client.workflow.list_templates()``).
|
|
25
|
+
|
|
26
|
+
Example::
|
|
27
|
+
|
|
28
|
+
from synapseone_sdk import SynapseOneClient
|
|
29
|
+
|
|
30
|
+
client = SynapseOneClient("http://localhost:8080", token=jwt)
|
|
31
|
+
parks = client.base.collection("parks", model=Park, mapping={"name": "profile.name"})
|
|
32
|
+
parks.create(Park(name="Central"))
|
|
33
|
+
result = client.queue.publish("emails", {"to": "a@b.c"})
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import warnings
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
from ._http import UNSET, HttpClient, SynapseOneError
|
|
42
|
+
from .ai import AiService
|
|
43
|
+
from .base import (
|
|
44
|
+
BaseCollection,
|
|
45
|
+
BaseService,
|
|
46
|
+
QueryResult,
|
|
47
|
+
VectorSearchResult,
|
|
48
|
+
data_to_object,
|
|
49
|
+
normalize_mapping,
|
|
50
|
+
object_to_data,
|
|
51
|
+
)
|
|
52
|
+
from .faststore import FaststoreService
|
|
53
|
+
from .iam import IamService
|
|
54
|
+
from .inference import InferenceService
|
|
55
|
+
from .lds import LdsService
|
|
56
|
+
from .queue import QueueService
|
|
57
|
+
from .stream import StreamService
|
|
58
|
+
from .workflow import WorkflowService, trigger_webhook, unwrap_executions
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"UNSET",
|
|
62
|
+
"AiService",
|
|
63
|
+
"BaseCollection",
|
|
64
|
+
"BaseService",
|
|
65
|
+
"FaststoreService",
|
|
66
|
+
"IamService",
|
|
67
|
+
"InferenceService",
|
|
68
|
+
"LdsService",
|
|
69
|
+
"QueryResult",
|
|
70
|
+
"VectorSearchResult",
|
|
71
|
+
"QueueService",
|
|
72
|
+
"StreamService",
|
|
73
|
+
"SynapseDXClient",
|
|
74
|
+
"SynapseDXError",
|
|
75
|
+
"SynapseOneClient",
|
|
76
|
+
"SynapseOneError",
|
|
77
|
+
"WorkflowService",
|
|
78
|
+
"data_to_object",
|
|
79
|
+
"normalize_mapping",
|
|
80
|
+
"object_to_data",
|
|
81
|
+
"trigger_webhook",
|
|
82
|
+
"unwrap_executions",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class SynapseOneClient:
|
|
87
|
+
"""One client, one HTTP layer, seven service namespaces.
|
|
88
|
+
|
|
89
|
+
``base_url`` points at the gateway (``http://localhost:8080``) or a single
|
|
90
|
+
service origin. ``tenant`` is sent as ``x-synapseone-tenant`` and is only
|
|
91
|
+
required by platform callers of faststore/queue.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
base_url: str,
|
|
97
|
+
token: str | None = None,
|
|
98
|
+
api_key: str | None = None,
|
|
99
|
+
timeout: int = 15,
|
|
100
|
+
tenant: str | None = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
self._http = HttpClient(
|
|
103
|
+
base_url, token=token, api_key=api_key, tenant=tenant, timeout=timeout
|
|
104
|
+
)
|
|
105
|
+
self.iam = IamService(self._http)
|
|
106
|
+
self.workflow = WorkflowService(self._http)
|
|
107
|
+
self.ai = AiService(self._http)
|
|
108
|
+
self.lds = LdsService(self._http)
|
|
109
|
+
self.base = BaseService(self._http)
|
|
110
|
+
self.faststore = FaststoreService(self._http)
|
|
111
|
+
self.queue = QueueService(self._http)
|
|
112
|
+
self.stream = StreamService(self._http)
|
|
113
|
+
self.inference = InferenceService(self._http)
|
|
114
|
+
|
|
115
|
+
# ---- transport knobs (also mirrored by JS setToken/setApiKey and Go) ----
|
|
116
|
+
@property
|
|
117
|
+
def base_url(self) -> str:
|
|
118
|
+
return self._http.base_url
|
|
119
|
+
|
|
120
|
+
@base_url.setter
|
|
121
|
+
def base_url(self, value: str) -> None:
|
|
122
|
+
self._http.base_url = value.rstrip("/")
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def token(self) -> str | None:
|
|
126
|
+
return self._http.token
|
|
127
|
+
|
|
128
|
+
@token.setter
|
|
129
|
+
def token(self, value: str | None) -> None:
|
|
130
|
+
self._http.token = value
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def api_key(self) -> str | None:
|
|
134
|
+
return self._http.api_key
|
|
135
|
+
|
|
136
|
+
@api_key.setter
|
|
137
|
+
def api_key(self, value: str | None) -> None:
|
|
138
|
+
self._http.api_key = value
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def tenant(self) -> str | None:
|
|
142
|
+
return self._http.tenant
|
|
143
|
+
|
|
144
|
+
@tenant.setter
|
|
145
|
+
def tenant(self, value: str | None) -> None:
|
|
146
|
+
self._http.tenant = value
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def timeout(self) -> int:
|
|
150
|
+
return self._http.timeout
|
|
151
|
+
|
|
152
|
+
@timeout.setter
|
|
153
|
+
def timeout(self, value: int) -> None:
|
|
154
|
+
self._http.timeout = value
|
|
155
|
+
|
|
156
|
+
def set_token(self, token: str) -> None:
|
|
157
|
+
"""Set/replace the Bearer JWT (mirrors JS setToken / Go SetToken)."""
|
|
158
|
+
self.token = token
|
|
159
|
+
|
|
160
|
+
def set_api_key(self, api_key: str) -> None:
|
|
161
|
+
"""Set/replace the X-API-Key (mirrors JS setApiKey / Go SetAPIKey)."""
|
|
162
|
+
self.api_key = api_key
|
|
163
|
+
|
|
164
|
+
def call(self, method: str, path: str, body: Any | None = None) -> Any:
|
|
165
|
+
"""Generic escape hatch (forward-compat with endpoints added after this SDK).
|
|
166
|
+
|
|
167
|
+
method is e.g. 'GET'/'POST'/'PUT'/'DELETE'; path must start with '/'.
|
|
168
|
+
"""
|
|
169
|
+
verb = (method or "GET").upper()
|
|
170
|
+
if not path.startswith("/"):
|
|
171
|
+
path = "/" + path
|
|
172
|
+
if verb in ("GET", "HEAD"):
|
|
173
|
+
return self._http.request(verb, path)
|
|
174
|
+
return self._http.request(verb, path, body)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
# deprecated flat wrappers (legacy merged API)
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
_IAM_METHODS = (
|
|
182
|
+
"login",
|
|
183
|
+
"refresh",
|
|
184
|
+
"logout",
|
|
185
|
+
"logout_all",
|
|
186
|
+
"verify",
|
|
187
|
+
"me",
|
|
188
|
+
"oidc_config",
|
|
189
|
+
"oidc_start",
|
|
190
|
+
"oidc_callback",
|
|
191
|
+
"list_orgs",
|
|
192
|
+
"create_org",
|
|
193
|
+
"create_account",
|
|
194
|
+
"list_accounts",
|
|
195
|
+
"create_user",
|
|
196
|
+
"list_users",
|
|
197
|
+
"user_policies",
|
|
198
|
+
"authorize",
|
|
199
|
+
"attach_policy",
|
|
200
|
+
"detach_policy",
|
|
201
|
+
"role_policies",
|
|
202
|
+
"assign_role",
|
|
203
|
+
"unassign_role",
|
|
204
|
+
"user_roles",
|
|
205
|
+
"simulate",
|
|
206
|
+
"list_audit",
|
|
207
|
+
"create_api_key",
|
|
208
|
+
"revoke_api_key",
|
|
209
|
+
"list_api_keys",
|
|
210
|
+
"verify_api_key",
|
|
211
|
+
"create_service_account",
|
|
212
|
+
"list_service_accounts",
|
|
213
|
+
"list_roles",
|
|
214
|
+
"create_role",
|
|
215
|
+
"delete_role",
|
|
216
|
+
"list_policies",
|
|
217
|
+
"create_policy",
|
|
218
|
+
"delete_policy",
|
|
219
|
+
"service_account_token",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
_WORKFLOW_METHODS = (
|
|
223
|
+
"list_templates",
|
|
224
|
+
"get_template",
|
|
225
|
+
"get_workflow",
|
|
226
|
+
"activate_workflow",
|
|
227
|
+
"deactivate_workflow",
|
|
228
|
+
"execute_workflow",
|
|
229
|
+
"list_workflows",
|
|
230
|
+
"create_workflow_from_template",
|
|
231
|
+
"create_workflow",
|
|
232
|
+
"update_workflow",
|
|
233
|
+
"delete_workflow",
|
|
234
|
+
"list_executions",
|
|
235
|
+
"get_execution",
|
|
236
|
+
"test_run",
|
|
237
|
+
"create_credential",
|
|
238
|
+
"list_credentials",
|
|
239
|
+
"get_credential",
|
|
240
|
+
"delete_credential",
|
|
241
|
+
"rotate_credential",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
_LDS_METHODS = {
|
|
245
|
+
"list_lds_licenses": "list_licenses",
|
|
246
|
+
"list_lds_datasources": "list_datasources",
|
|
247
|
+
"create_lds_datasource": "create_datasource",
|
|
248
|
+
"get_lds_datasource": "get_datasource",
|
|
249
|
+
"update_lds_datasource": "update_datasource",
|
|
250
|
+
"delete_lds_datasource": "delete_datasource",
|
|
251
|
+
"get_lds_datasource_attributes": "get_datasource_attributes",
|
|
252
|
+
"set_lds_datasource_attributes": "set_datasource_attributes",
|
|
253
|
+
"ingest_lds_records": "ingest_records",
|
|
254
|
+
"list_lds_entities": "list_entities",
|
|
255
|
+
"get_lds_entity": "get_entity",
|
|
256
|
+
"get_lds_datasource_stats": "get_datasource_stats",
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _deprecated(service: str, legacy: str, target: str):
|
|
261
|
+
replacement = f"client.{service}.{target}()"
|
|
262
|
+
|
|
263
|
+
def wrapper(self: SynapseOneClient, *args: Any, **kwargs: Any) -> Any:
|
|
264
|
+
warnings.warn(
|
|
265
|
+
f"SynapseOneClient.{legacy}() is deprecated; use {replacement} instead",
|
|
266
|
+
DeprecationWarning,
|
|
267
|
+
stacklevel=2,
|
|
268
|
+
)
|
|
269
|
+
return getattr(getattr(self, service), target)(*args, **kwargs)
|
|
270
|
+
|
|
271
|
+
wrapper.__name__ = legacy
|
|
272
|
+
wrapper.__qualname__ = f"SynapseOneClient.{legacy}"
|
|
273
|
+
wrapper.__doc__ = f"Deprecated alias for ``{replacement}``."
|
|
274
|
+
return wrapper
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
for _name in _IAM_METHODS:
|
|
278
|
+
setattr(SynapseOneClient, _name, _deprecated("iam", _name, _name))
|
|
279
|
+
for _name in _WORKFLOW_METHODS:
|
|
280
|
+
setattr(SynapseOneClient, _name, _deprecated("workflow", _name, _name))
|
|
281
|
+
for _legacy, _target in _LDS_METHODS.items():
|
|
282
|
+
setattr(SynapseOneClient, _legacy, _deprecated("lds", _legacy, _target))
|
|
283
|
+
setattr(SynapseOneClient, "ai_agent", _deprecated("ai", "ai_agent", "agent"))
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
# SynapseDX aliases (rebrand window: same classes, old import names keep working)
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
SynapseDXClient = SynapseOneClient
|
|
291
|
+
SynapseDXError = SynapseOneError
|
synapseone_sdk/_http.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Copyright 2026 SynapseOne Contributors
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
"""Shared HTTP plumbing for the synapseone SDK.
|
|
4
|
+
|
|
5
|
+
Every service module (iam, workflow, lds, base, faststore, queue) talks to the
|
|
6
|
+
platform through one :class:`HttpClient`, which adds auth headers, the optional
|
|
7
|
+
platform tenant header and maps every failure (network or HTTP) to
|
|
8
|
+
:class:`SynapseOneError`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
# Sentinel for "argument was not provided", distinct from None (an explicit
|
|
18
|
+
# null must be distinguishable from "leave untouched" in partial updates).
|
|
19
|
+
UNSET = object()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SynapseOneError(Exception):
|
|
23
|
+
"""All SDK errors surface as SynapseOneError (network or HTTP).
|
|
24
|
+
|
|
25
|
+
``status`` carries the HTTP status code when the failure came from a JSON
|
|
26
|
+
response (None for network errors) and ``body`` carries the parsed error
|
|
27
|
+
body when one was returned, so callers can branch on codes such as
|
|
28
|
+
``not_found`` or ``version_conflict``.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
message: str,
|
|
34
|
+
*,
|
|
35
|
+
status: int | None = None,
|
|
36
|
+
body: Any | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
self.status = status
|
|
40
|
+
self.body = body
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class HttpClient:
|
|
44
|
+
"""Thin request layer shared by all SynapseOne services.
|
|
45
|
+
|
|
46
|
+
``base_url`` points at the gateway (``http://localhost:8080``) or directly
|
|
47
|
+
at a service origin. ``tenant`` is only needed by platform callers of the
|
|
48
|
+
tenant-scoped services (faststore/queue) and is sent as
|
|
49
|
+
``x-synapseone-tenant``.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
base_url: str,
|
|
55
|
+
token: str | None = None,
|
|
56
|
+
api_key: str | None = None,
|
|
57
|
+
tenant: str | None = None,
|
|
58
|
+
timeout: int = 15,
|
|
59
|
+
) -> None:
|
|
60
|
+
self.base_url = base_url.rstrip("/")
|
|
61
|
+
self.token = token
|
|
62
|
+
self.api_key = api_key
|
|
63
|
+
self.tenant = tenant
|
|
64
|
+
self.timeout = timeout
|
|
65
|
+
|
|
66
|
+
def headers(self) -> dict[str, str]:
|
|
67
|
+
h = {"Content-Type": "application/json"}
|
|
68
|
+
if self.token:
|
|
69
|
+
h["Authorization"] = f"Bearer {self.token}"
|
|
70
|
+
if self.api_key:
|
|
71
|
+
h["X-API-Key"] = self.api_key
|
|
72
|
+
if self.tenant:
|
|
73
|
+
h["x-synapseone-tenant"] = self.tenant
|
|
74
|
+
return h
|
|
75
|
+
|
|
76
|
+
def request(self, method: str, path: str, body: Any | None = None) -> Any:
|
|
77
|
+
try:
|
|
78
|
+
r = requests.request(
|
|
79
|
+
(method or "GET").upper(),
|
|
80
|
+
self.base_url + path,
|
|
81
|
+
json=body,
|
|
82
|
+
headers=self.headers(),
|
|
83
|
+
timeout=self.timeout,
|
|
84
|
+
)
|
|
85
|
+
except Exception as e: # network, DNS, timeout...
|
|
86
|
+
raise SynapseOneError(f"synapseone network {path}: {e}") from e
|
|
87
|
+
if r.status_code >= 400:
|
|
88
|
+
raise SynapseOneError(
|
|
89
|
+
f"synapseone {r.status_code} {path}: {r.text[:500]}",
|
|
90
|
+
status=r.status_code,
|
|
91
|
+
body=_try_json(r),
|
|
92
|
+
)
|
|
93
|
+
if not r.text:
|
|
94
|
+
return None
|
|
95
|
+
try:
|
|
96
|
+
return r.json()
|
|
97
|
+
except ValueError:
|
|
98
|
+
return r.text # non-JSON body (parity with JS SDK text fallback)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _try_json(r: Any) -> Any:
|
|
102
|
+
try:
|
|
103
|
+
return r.json()
|
|
104
|
+
except ValueError:
|
|
105
|
+
return None
|
synapseone_sdk/ai.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Copyright 2026 SynapseOne Contributors
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
"""Synapse AI client: the guidance-only API assistant (``POST /v1/ai/agent``).
|
|
4
|
+
|
|
5
|
+
The agent maps a natural-language question to one catalogued SynapseOne
|
|
6
|
+
endpoint and returns the method/path plus a ready-to-try body. It never
|
|
7
|
+
executes the call itself — callers decide whether to run it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ._http import HttpClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AiService:
|
|
18
|
+
"""Methods for the SynapseOne AI Assistant service (``/v1/ai``)."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, http: HttpClient) -> None:
|
|
21
|
+
self._http = http
|
|
22
|
+
|
|
23
|
+
def agent(
|
|
24
|
+
self, question: str, history: list[dict[str, Any]] | None = None
|
|
25
|
+
) -> dict[str, Any]:
|
|
26
|
+
"""Ask the API assistant a question (1..2000 chars).
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
question:
|
|
31
|
+
Natural language question about SynapseOne APIs.
|
|
32
|
+
history:
|
|
33
|
+
Optional list of prior turns (``[{"role": "user"|"ai", "text": "..."}]``).
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
dict
|
|
38
|
+
Result containing ``ok``, ``method``, ``path``, ``auth``, ``pathValues``,
|
|
39
|
+
``body``, ``explanation``, ``reason``, and optional ``guidance``.
|
|
40
|
+
"""
|
|
41
|
+
body: dict[str, Any] = {"question": question}
|
|
42
|
+
if history:
|
|
43
|
+
body["history"] = history
|
|
44
|
+
return self._http.request("POST", "/v1/ai/agent", body)
|