ando-ai 0.4.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.
- ando_ai/__init__.py +147 -0
- ando_ai/__main__.py +6 -0
- ando_ai/actions.py +502 -0
- ando_ai/cli.py +517 -0
- ando_ai/client.py +1709 -0
- ando_ai/connectors.py +471 -0
- ando_ai-0.4.0.dist-info/METADATA +595 -0
- ando_ai-0.4.0.dist-info/RECORD +12 -0
- ando_ai-0.4.0.dist-info/WHEEL +5 -0
- ando_ai-0.4.0.dist-info/entry_points.txt +2 -0
- ando_ai-0.4.0.dist-info/licenses/LICENSE +50 -0
- ando_ai-0.4.0.dist-info/top_level.txt +1 -0
ando_ai/__init__.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Ando Platform API — official Python SDK (sync, httpx-based).
|
|
2
|
+
|
|
3
|
+
Public surface:
|
|
4
|
+
AndoPlatform — the client (context manager)
|
|
5
|
+
ApiError — envelope errors (stable ``code`` + HTTP ``status``)
|
|
6
|
+
NetworkError — transport failures (DNS/connect/TLS/timeout)
|
|
7
|
+
JobFailed — ingest job ended ``failed`` (carries ``error_code``)
|
|
8
|
+
JobTimeout — ``wait_for_job`` deadline exceeded
|
|
9
|
+
AndoPlatformError — common base class of all of the above
|
|
10
|
+
|
|
11
|
+
Three levels of the same stack (docs/PLATFORM_API_CONTRACT.md):
|
|
12
|
+
ando.query(...) — chunks + scores, you generate
|
|
13
|
+
ando.answer(...) — grounded synthesis over those chunks
|
|
14
|
+
ando.agentic_answer(...) — PREVIEW: the full agent loop (T1/T2/T3 +
|
|
15
|
+
text-to-SQL over published structured
|
|
16
|
+
sources), read-only
|
|
17
|
+
|
|
18
|
+
Higher-order surfaces, all methods on the client:
|
|
19
|
+
ando.generate(...) — render a docx/pptx/xlsx/pdf/md from your data
|
|
20
|
+
(async; wait_for_generation + download_generation)
|
|
21
|
+
ando.cad_analyze(...) / cad_dfm_check / cad_convert / cad_find_similar
|
|
22
|
+
— STEP/DXF analysis, DFM, conversion (async;
|
|
23
|
+
wait_for_cad_job + get_cad_result)
|
|
24
|
+
ando.list_sources() — what this key can query (discovery)
|
|
25
|
+
ando.get_source_schema / curate_source_schema / publish_source_schema
|
|
26
|
+
— the structured-source K-Schema + publish gate
|
|
27
|
+
ando.create_webhook / list_webhooks / delete_webhook / test_webhook
|
|
28
|
+
— outbound webhook endpoints
|
|
29
|
+
|
|
30
|
+
DownloadedFile — a fetched binary artifact (content/content_type/filename,
|
|
31
|
+
with ``.json()`` and ``.save(path)``)
|
|
32
|
+
GenerationFailed — a generation ended ``failed`` (carries ``error_code``)
|
|
33
|
+
GenerationTimeout — ``wait_for_generation`` deadline exceeded
|
|
34
|
+
|
|
35
|
+
Connector Push API (docs/PLATFORM_CONNECTOR_SDK.md):
|
|
36
|
+
AndoConnector — batching/retrying push helper bound to one slug
|
|
37
|
+
SyncSession — one open run; context manager, aborts on exception
|
|
38
|
+
SyncResult — counters of a completed run
|
|
39
|
+
RecordRejected — one refused record (absolute index, field, reason)
|
|
40
|
+
SyncAborted — pushing into an aborted/completed run
|
|
41
|
+
list_connectors — every connector of the project (also
|
|
42
|
+
``ando.connectors()`` / ``ando.connector(slug)``)
|
|
43
|
+
|
|
44
|
+
Actions write-loop (the symmetric write half):
|
|
45
|
+
AndoActions — ergonomic helper bound to a client (``ando.actions()``);
|
|
46
|
+
``run(...)`` proposes + executes in one call
|
|
47
|
+
declare_connector_action / list_connector_actions /
|
|
48
|
+
delete_connector_action — the declared-action surface on a connector
|
|
49
|
+
(``binding=`` declares the optional HTTP binding)
|
|
50
|
+
invoke_connector_action — synchronous invoke of an HTTP-bound capability
|
|
51
|
+
(same params validation + risk gate as execute)
|
|
52
|
+
propose_action / execute_action / complete_action /
|
|
53
|
+
get_action / list_actions — the request lifecycle (also as ``ando.*``)
|
|
54
|
+
ActionBinding — the HTTP binding shape ({method, path, body_template?,
|
|
55
|
+
headers?, response?, timeout_seconds?})
|
|
56
|
+
ACTION_RISK_LEVELS — informational risk vocabulary (never validated
|
|
57
|
+
client-side)
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from ando_ai.actions import (
|
|
61
|
+
ACTION_RISK_LEVELS,
|
|
62
|
+
ActionBinding,
|
|
63
|
+
AndoActions,
|
|
64
|
+
complete_action,
|
|
65
|
+
declare_connector_action,
|
|
66
|
+
delete_connector_action,
|
|
67
|
+
execute_action,
|
|
68
|
+
get_action,
|
|
69
|
+
invoke_connector_action,
|
|
70
|
+
list_actions,
|
|
71
|
+
list_connector_actions,
|
|
72
|
+
propose_action,
|
|
73
|
+
)
|
|
74
|
+
from ando_ai.client import (
|
|
75
|
+
AGENTIC_TIMEOUT,
|
|
76
|
+
DEFAULT_BASE_URL,
|
|
77
|
+
DEFAULT_K,
|
|
78
|
+
FILTER_SOURCES,
|
|
79
|
+
GENERATE_FORMATS,
|
|
80
|
+
MAX_K,
|
|
81
|
+
WEBHOOK_EVENTS,
|
|
82
|
+
AndoPlatform,
|
|
83
|
+
AndoPlatformError,
|
|
84
|
+
ApiError,
|
|
85
|
+
DownloadedFile,
|
|
86
|
+
GenerationFailed,
|
|
87
|
+
GenerationTimeout,
|
|
88
|
+
JobFailed,
|
|
89
|
+
JobTimeout,
|
|
90
|
+
NetworkError,
|
|
91
|
+
)
|
|
92
|
+
from ando_ai.connectors import (
|
|
93
|
+
DEFAULT_BATCH_SIZE,
|
|
94
|
+
MAX_BATCH_SIZE,
|
|
95
|
+
AndoConnector,
|
|
96
|
+
RecordRejected,
|
|
97
|
+
SyncAborted,
|
|
98
|
+
SyncResult,
|
|
99
|
+
SyncSession,
|
|
100
|
+
connector_kinds,
|
|
101
|
+
list_connectors,
|
|
102
|
+
validate_records,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
__version__ = "0.4.0"
|
|
106
|
+
|
|
107
|
+
__all__ = [
|
|
108
|
+
"AndoPlatform",
|
|
109
|
+
"AndoPlatformError",
|
|
110
|
+
"ApiError",
|
|
111
|
+
"JobFailed",
|
|
112
|
+
"JobTimeout",
|
|
113
|
+
"GenerationFailed",
|
|
114
|
+
"GenerationTimeout",
|
|
115
|
+
"NetworkError",
|
|
116
|
+
"DownloadedFile",
|
|
117
|
+
"DEFAULT_BASE_URL",
|
|
118
|
+
"DEFAULT_K",
|
|
119
|
+
"MAX_K",
|
|
120
|
+
"FILTER_SOURCES",
|
|
121
|
+
"GENERATE_FORMATS",
|
|
122
|
+
"WEBHOOK_EVENTS",
|
|
123
|
+
"AGENTIC_TIMEOUT",
|
|
124
|
+
"AndoConnector",
|
|
125
|
+
"SyncSession",
|
|
126
|
+
"SyncResult",
|
|
127
|
+
"RecordRejected",
|
|
128
|
+
"SyncAborted",
|
|
129
|
+
"list_connectors",
|
|
130
|
+
"connector_kinds",
|
|
131
|
+
"validate_records",
|
|
132
|
+
"DEFAULT_BATCH_SIZE",
|
|
133
|
+
"MAX_BATCH_SIZE",
|
|
134
|
+
"AndoActions",
|
|
135
|
+
"ActionBinding",
|
|
136
|
+
"ACTION_RISK_LEVELS",
|
|
137
|
+
"declare_connector_action",
|
|
138
|
+
"list_connector_actions",
|
|
139
|
+
"delete_connector_action",
|
|
140
|
+
"invoke_connector_action",
|
|
141
|
+
"propose_action",
|
|
142
|
+
"execute_action",
|
|
143
|
+
"complete_action",
|
|
144
|
+
"get_action",
|
|
145
|
+
"list_actions",
|
|
146
|
+
"__version__",
|
|
147
|
+
]
|
ando_ai/__main__.py
ADDED
ando_ai/actions.py
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"""Actions helper for the Ando Platform write-loop.
|
|
2
|
+
|
|
3
|
+
The Actions API is the symmetric WRITE half of the Push API — where the
|
|
4
|
+
Push API lets a connector send records IN, the Actions API lets a caller
|
|
5
|
+
ask the project's own app to DO something (open a ticket, post a message,
|
|
6
|
+
move a deal) and audit the outcome. Four moves, connector-agnostic and
|
|
7
|
+
white-label by construction (only the developer's own slug + action names
|
|
8
|
+
ever appear):
|
|
9
|
+
|
|
10
|
+
* A connector DECLARES the actions it supports —
|
|
11
|
+
``POST /connectors/{slug}/actions`` (name + a JSON Schema of params + a
|
|
12
|
+
risk level + optionally an HTTP ``binding``). Requires ``sources:manage``.
|
|
13
|
+
* A caller PROPOSES an action — ``POST /actions`` — params validated
|
|
14
|
+
against the declared schema, persisted with a preview, NO side effect
|
|
15
|
+
yet. Requires ``actions:write``.
|
|
16
|
+
* EXECUTE — ``POST /actions/{id}/execute`` — dispatches a signed
|
|
17
|
+
``action.requested`` webhook to the project's app. ``high``/``critical``
|
|
18
|
+
actions require ``confirm_risk``. Requires ``actions:write``.
|
|
19
|
+
* The app reports back — ``POST /actions/{id}/complete``. Requires
|
|
20
|
+
``actions:write``.
|
|
21
|
+
|
|
22
|
+
A capability declared WITH a ``binding`` (:class:`ActionBinding`) also has a
|
|
23
|
+
synchronous path — ``POST /connectors/{slug}/actions/{name}/invoke``
|
|
24
|
+
(:func:`invoke_connector_action`): Ando calls the bound endpoint and returns
|
|
25
|
+
the outcome in one round trip, same params validation, same risk gate.
|
|
26
|
+
|
|
27
|
+
Read paths (``GET /actions``, ``GET /actions/{id}``,
|
|
28
|
+
``GET /connectors/{slug}/actions``) use ``actions:read``.
|
|
29
|
+
|
|
30
|
+
This module is the thin, DRY glue over that surface: every call goes through
|
|
31
|
+
:meth:`AndoPlatform._request`, so auth, the stable error envelope and the
|
|
32
|
+
transport (including the ``MockTransport`` used in tests) have exactly ONE
|
|
33
|
+
implementation. The module functions take a client as their first argument
|
|
34
|
+
(mirroring :func:`ando.connectors.list_connectors`); the
|
|
35
|
+
:class:`AndoActions` helper binds a client so you drop it, and adds
|
|
36
|
+
:meth:`AndoActions.run` — propose + execute in one call, the write-loop's
|
|
37
|
+
happy path.
|
|
38
|
+
|
|
39
|
+
Usage:
|
|
40
|
+
|
|
41
|
+
from ando_ai import AndoPlatform
|
|
42
|
+
|
|
43
|
+
with AndoPlatform("ando_sk_live_...") as ando:
|
|
44
|
+
actions = ando.actions()
|
|
45
|
+
# One-time, with a sources:manage key:
|
|
46
|
+
actions.declare(
|
|
47
|
+
"acme-desk",
|
|
48
|
+
"create_ticket",
|
|
49
|
+
params_schema={
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": {"subject": {"type": "string"}},
|
|
52
|
+
"required": ["subject"],
|
|
53
|
+
},
|
|
54
|
+
risk="medium",
|
|
55
|
+
)
|
|
56
|
+
# Per request, with an actions:write key:
|
|
57
|
+
dispatched = actions.run(
|
|
58
|
+
"acme-desk",
|
|
59
|
+
"create_ticket",
|
|
60
|
+
{"subject": "Refund request"},
|
|
61
|
+
)
|
|
62
|
+
print(dispatched["id"], dispatched["status"]) # ... "dispatched"
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
from __future__ import annotations
|
|
66
|
+
|
|
67
|
+
from typing import Any, Dict, Optional, TypedDict
|
|
68
|
+
|
|
69
|
+
from ando_ai.client import AndoPlatform
|
|
70
|
+
|
|
71
|
+
#: Risk levels a declared action can carry, in ascending order. ``high`` and
|
|
72
|
+
#: ``critical`` require ``confirm_risk`` on execute. Informational ONLY: the
|
|
73
|
+
#: SDK never validates ``risk`` client-side — the server is the authority, so
|
|
74
|
+
#: a server-side addition needs no SDK release (the same stance the client
|
|
75
|
+
#: takes on ``k`` and ``filters.sources``).
|
|
76
|
+
ACTION_RISK_LEVELS: tuple[str, ...] = ("low", "medium", "high", "critical")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class _ActionBindingRequired(TypedDict):
|
|
80
|
+
"""The required half of :class:`ActionBinding` (see there)."""
|
|
81
|
+
|
|
82
|
+
method: str
|
|
83
|
+
path: str
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ActionBinding(_ActionBindingRequired, total=False):
|
|
87
|
+
"""HTTP binding of a declared capability — how Ando calls YOUR system.
|
|
88
|
+
|
|
89
|
+
Declared alongside the action (:func:`declare_connector_action`
|
|
90
|
+
``binding=``) and consumed by :func:`invoke_connector_action`. ``method``
|
|
91
|
+
(``POST``/``PUT``/``PATCH``/``GET``/``DELETE``) and ``path`` (relative,
|
|
92
|
+
starts with ``/``, e.g. ``"/orders"``) are required; the rest is optional:
|
|
93
|
+
``body_template`` (a JSON object with ``{{param}}`` placeholders),
|
|
94
|
+
``headers``, ``response`` (``{"id_field": "id"}`` — where the created
|
|
95
|
+
resource id lives in your response) and ``timeout_seconds`` (1..60).
|
|
96
|
+
A plain ``dict`` with these keys is accepted wherever an
|
|
97
|
+
:class:`ActionBinding` is — only structure is typed here, the server is
|
|
98
|
+
the authority on the exact rules (the SDK's usual stance).
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
body_template: Dict[str, Any]
|
|
102
|
+
headers: Dict[str, str]
|
|
103
|
+
response: Dict[str, Any]
|
|
104
|
+
timeout_seconds: int
|
|
105
|
+
|
|
106
|
+
#: Header that opts a propose into idempotent replay (see :func:`propose_action`).
|
|
107
|
+
_IDEMPOTENCY_KEY_HEADER = "Idempotency-Key"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
# Action definitions (declared on a connector) — sources:manage / actions:read
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def declare_connector_action(
|
|
116
|
+
client: AndoPlatform,
|
|
117
|
+
connector: str,
|
|
118
|
+
name: str,
|
|
119
|
+
description: Optional[str] = None,
|
|
120
|
+
params_schema: Optional[dict] = None,
|
|
121
|
+
risk: Optional[str] = None,
|
|
122
|
+
binding: Optional[ActionBinding] = None,
|
|
123
|
+
) -> dict:
|
|
124
|
+
"""``POST /connectors/{slug}/actions`` — declare an action (sources:manage).
|
|
125
|
+
|
|
126
|
+
Declare (or re-declare — it is idempotent on ``name``) an action the
|
|
127
|
+
connector can perform. ``params_schema`` is the JSON Schema (Draft
|
|
128
|
+
2020-12) that :func:`propose_action` validates the caller's params
|
|
129
|
+
against; omit it for an unconstrained action. ``risk`` gates the
|
|
130
|
+
confirmation: ``high``/``critical`` need ``confirm_risk`` on execute
|
|
131
|
+
(see :data:`ACTION_RISK_LEVELS`). ``binding`` is the optional HTTP
|
|
132
|
+
binding (:class:`ActionBinding` — ``{method, path, body_template?,
|
|
133
|
+
headers?, response?, timeout_seconds?}``) that lets Ando call your
|
|
134
|
+
system for this capability via :func:`invoke_connector_action`; omit it
|
|
135
|
+
for webhook-only dispatch. A declaration is a FULL replacement:
|
|
136
|
+
re-declaring without ``binding`` clears a stored one. Returns the stored
|
|
137
|
+
definition ``{connector, name, description, params_schema, risk,
|
|
138
|
+
binding, ...}``.
|
|
139
|
+
|
|
140
|
+
Optional fields travel ONLY when set, so a bare declaration puts the
|
|
141
|
+
exact same body on the wire as before they existed.
|
|
142
|
+
"""
|
|
143
|
+
if not connector:
|
|
144
|
+
raise ValueError("connector is required")
|
|
145
|
+
if not name:
|
|
146
|
+
raise ValueError("name is required")
|
|
147
|
+
body: Dict[str, Any] = {"name": name}
|
|
148
|
+
if description is not None:
|
|
149
|
+
body["description"] = description
|
|
150
|
+
if params_schema is not None:
|
|
151
|
+
if not isinstance(params_schema, dict):
|
|
152
|
+
raise ValueError("params_schema must be a dict (a JSON Schema object)")
|
|
153
|
+
body["params_schema"] = params_schema
|
|
154
|
+
if risk is not None:
|
|
155
|
+
body["risk"] = risk
|
|
156
|
+
if binding is not None:
|
|
157
|
+
if not isinstance(binding, dict):
|
|
158
|
+
raise ValueError("binding must be a dict (an ActionBinding object)")
|
|
159
|
+
body["binding"] = binding
|
|
160
|
+
return client._request("POST", f"/connectors/{connector}/actions", json=body)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def list_connector_actions(client: AndoPlatform, connector: str) -> dict:
|
|
164
|
+
"""``GET /connectors/{slug}/actions`` — a connector's declared actions.
|
|
165
|
+
|
|
166
|
+
Requires ``actions:read``. Returns ``{actions: [...], count}``.
|
|
167
|
+
"""
|
|
168
|
+
if not connector:
|
|
169
|
+
raise ValueError("connector is required")
|
|
170
|
+
return client._request("GET", f"/connectors/{connector}/actions")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def delete_connector_action(
|
|
174
|
+
client: AndoPlatform, connector: str, name: str
|
|
175
|
+
) -> dict:
|
|
176
|
+
"""``DELETE /connectors/{slug}/actions/{name}`` — remove a declaration.
|
|
177
|
+
|
|
178
|
+
Requires ``sources:manage``. Returns ``{deleted, connector, name}``.
|
|
179
|
+
"""
|
|
180
|
+
if not connector:
|
|
181
|
+
raise ValueError("connector is required")
|
|
182
|
+
if not name:
|
|
183
|
+
raise ValueError("name is required")
|
|
184
|
+
return client._request(
|
|
185
|
+
"DELETE", f"/connectors/{connector}/actions/{name}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def invoke_connector_action(
|
|
190
|
+
client: AndoPlatform,
|
|
191
|
+
connector: str,
|
|
192
|
+
name: str,
|
|
193
|
+
params: Optional[dict] = None,
|
|
194
|
+
*,
|
|
195
|
+
confirm_risk: bool = False,
|
|
196
|
+
) -> dict:
|
|
197
|
+
"""``POST /connectors/{slug}/actions/{name}/invoke`` — invoke an
|
|
198
|
+
HTTP-bound capability synchronously (actions:write).
|
|
199
|
+
|
|
200
|
+
The synchronous sibling of the propose/execute loop: Ando calls the
|
|
201
|
+
endpoint the capability's declared ``binding`` points at and returns
|
|
202
|
+
the outcome in one round trip — ``{ok, status_code, response}``.
|
|
203
|
+
``params`` are validated against the declared ``params_schema`` (the
|
|
204
|
+
same seam propose uses).
|
|
205
|
+
|
|
206
|
+
RISK GATE — the same one execute enforces, one vocabulary, one policy:
|
|
207
|
+
a ``high``/``critical``-risk capability is REFUSED (``invalid_request``,
|
|
208
|
+
400) unless ``confirm_risk=True`` is passed explicitly. With a TEST key
|
|
209
|
+
validation and gating run for real but the bound endpoint is NEVER
|
|
210
|
+
called — the response carries ``test: true`` (the same Twilio semantics
|
|
211
|
+
as execute).
|
|
212
|
+
|
|
213
|
+
Errors: ``invalid_request`` (400 — params fail the schema, missing
|
|
214
|
+
``confirm_risk``, or the capability declares no binding), ``not_found``
|
|
215
|
+
(404 — unknown connector or action), ``insufficient_scope`` (403).
|
|
216
|
+
"""
|
|
217
|
+
if not connector:
|
|
218
|
+
raise ValueError("connector is required")
|
|
219
|
+
if not name:
|
|
220
|
+
raise ValueError("name is required")
|
|
221
|
+
body: Dict[str, Any] = {"confirm_risk": bool(confirm_risk)}
|
|
222
|
+
if params is not None:
|
|
223
|
+
if not isinstance(params, dict):
|
|
224
|
+
raise ValueError("params must be a dict")
|
|
225
|
+
body["params"] = params
|
|
226
|
+
return client._request(
|
|
227
|
+
"POST", f"/connectors/{connector}/actions/{name}/invoke", json=body
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ---------------------------------------------------------------------------
|
|
232
|
+
# Request lifecycle — actions:write (+ resource_policy) / actions:read
|
|
233
|
+
# ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def propose_action(
|
|
237
|
+
client: AndoPlatform,
|
|
238
|
+
connector: str,
|
|
239
|
+
action: str,
|
|
240
|
+
params: Optional[dict] = None,
|
|
241
|
+
idempotency_key: Optional[str] = None,
|
|
242
|
+
) -> dict:
|
|
243
|
+
"""``POST /actions`` — propose an action (validate + preview, no run).
|
|
244
|
+
|
|
245
|
+
Requires ``actions:write`` AND a resource policy that allows
|
|
246
|
+
``connector``. ``params`` are validated against the action's declared
|
|
247
|
+
schema and the request is persisted ``proposed`` with a ``preview`` —
|
|
248
|
+
nothing runs on your systems until :func:`execute_action`. Returns the
|
|
249
|
+
``201`` request ``{id, connector, action, params, status, risk,
|
|
250
|
+
preview, ...}``.
|
|
251
|
+
|
|
252
|
+
Pass ``idempotency_key`` (opaque, <=255 chars) so a retried propose
|
|
253
|
+
(same connector + action + params) REPLAYS the first ``201`` instead of
|
|
254
|
+
creating a duplicate; reusing a key for a DIFFERENT request is a ``409``
|
|
255
|
+
(:class:`~ando_ai.client.ApiError` code ``invalid_request``).
|
|
256
|
+
Omit it and the request is sent exactly as before idempotency existed.
|
|
257
|
+
"""
|
|
258
|
+
if not connector:
|
|
259
|
+
raise ValueError("connector is required")
|
|
260
|
+
if not action:
|
|
261
|
+
raise ValueError("action is required")
|
|
262
|
+
body: Dict[str, Any] = {"connector": connector, "action": action}
|
|
263
|
+
if params is not None:
|
|
264
|
+
if not isinstance(params, dict):
|
|
265
|
+
raise ValueError("params must be a dict")
|
|
266
|
+
body["params"] = params
|
|
267
|
+
if idempotency_key is not None:
|
|
268
|
+
if not isinstance(idempotency_key, str) or not idempotency_key:
|
|
269
|
+
raise ValueError("idempotency_key must be a non-empty string")
|
|
270
|
+
return client._request(
|
|
271
|
+
"POST",
|
|
272
|
+
"/actions",
|
|
273
|
+
json=body,
|
|
274
|
+
headers={_IDEMPOTENCY_KEY_HEADER: idempotency_key},
|
|
275
|
+
)
|
|
276
|
+
return client._request("POST", "/actions", json=body)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def execute_action(
|
|
280
|
+
client: AndoPlatform, action_id: str, confirm_risk: bool = False
|
|
281
|
+
) -> dict:
|
|
282
|
+
"""``POST /actions/{id}/execute`` — dispatch a proposed action.
|
|
283
|
+
|
|
284
|
+
Requires ``actions:write`` AND a policy that allows the request's
|
|
285
|
+
connector. Dispatches a signed ``action.requested`` webhook to your app
|
|
286
|
+
and returns the request, now ``dispatched``. ``high``/``critical``-risk
|
|
287
|
+
actions require ``confirm_risk=True``.
|
|
288
|
+
|
|
289
|
+
If no webhook is subscribed to ``action.requested`` the call raises
|
|
290
|
+
:class:`~ando_ai.client.ApiError` (``invalid_request``) and the
|
|
291
|
+
request STAYS ``proposed`` (retryable) — register one via
|
|
292
|
+
``POST /v1/webhooks``, then execute again.
|
|
293
|
+
"""
|
|
294
|
+
if not action_id:
|
|
295
|
+
raise ValueError("action_id is required")
|
|
296
|
+
return client._request(
|
|
297
|
+
"POST",
|
|
298
|
+
f"/actions/{action_id}/execute",
|
|
299
|
+
json={"confirm_risk": bool(confirm_risk)},
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def complete_action(
|
|
304
|
+
client: AndoPlatform,
|
|
305
|
+
action_id: str,
|
|
306
|
+
status: str,
|
|
307
|
+
result: Optional[dict] = None,
|
|
308
|
+
error: Optional[str] = None,
|
|
309
|
+
) -> dict:
|
|
310
|
+
"""``POST /actions/{id}/complete`` — report a dispatched action's outcome.
|
|
311
|
+
|
|
312
|
+
Requires ``actions:write`` AND a policy that allows the request's
|
|
313
|
+
connector. ``status`` is ``completed`` or ``failed``; pass ``result``
|
|
314
|
+
(a free-form outcome payload) on success or ``error`` (a human-readable
|
|
315
|
+
reason) on failure. Only a ``dispatched`` request can be completed (no
|
|
316
|
+
double-completes). Returns the terminal request.
|
|
317
|
+
"""
|
|
318
|
+
if not action_id:
|
|
319
|
+
raise ValueError("action_id is required")
|
|
320
|
+
if not status:
|
|
321
|
+
raise ValueError("status is required")
|
|
322
|
+
body: Dict[str, Any] = {"status": status}
|
|
323
|
+
if result is not None:
|
|
324
|
+
if not isinstance(result, dict):
|
|
325
|
+
raise ValueError("result must be a dict")
|
|
326
|
+
body["result"] = result
|
|
327
|
+
if error is not None:
|
|
328
|
+
body["error"] = error
|
|
329
|
+
return client._request(
|
|
330
|
+
"POST", f"/actions/{action_id}/complete", json=body
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def get_action(client: AndoPlatform, action_id: str) -> dict:
|
|
335
|
+
"""``GET /actions/{id}`` — poll one action request (actions:read).
|
|
336
|
+
|
|
337
|
+
Returns the request with its current ``status`` (``proposed`` ->
|
|
338
|
+
``dispatched`` -> ``completed`` | ``failed``) and detail.
|
|
339
|
+
"""
|
|
340
|
+
if not action_id:
|
|
341
|
+
raise ValueError("action_id is required")
|
|
342
|
+
return client._request("GET", f"/actions/{action_id}")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def list_actions(
|
|
346
|
+
client: AndoPlatform,
|
|
347
|
+
connector: Optional[str] = None,
|
|
348
|
+
limit: Optional[int] = None,
|
|
349
|
+
cursor: Optional[str] = None,
|
|
350
|
+
) -> dict:
|
|
351
|
+
"""``GET /actions`` — the project's action requests, newest first.
|
|
352
|
+
|
|
353
|
+
Requires ``actions:read``. Returns ``{actions, count, has_more,
|
|
354
|
+
next_cursor}``. Cursor-paginated: pass ``next_cursor`` back as
|
|
355
|
+
``cursor`` until ``has_more`` is false. ``connector`` filters to one
|
|
356
|
+
slug. Params travel ONLY when set.
|
|
357
|
+
"""
|
|
358
|
+
params: Dict[str, Any] = {}
|
|
359
|
+
if connector is not None:
|
|
360
|
+
params["connector"] = connector
|
|
361
|
+
if limit is not None:
|
|
362
|
+
params["limit"] = limit
|
|
363
|
+
if cursor is not None:
|
|
364
|
+
params["cursor"] = cursor
|
|
365
|
+
return client._request("GET", "/actions", params=params)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class AndoActions:
|
|
369
|
+
"""Ergonomic wrapper over the Actions write-loop, bound to one client.
|
|
370
|
+
|
|
371
|
+
Constructed by :meth:`AndoPlatform.actions` (``ando.actions()``), the
|
|
372
|
+
mirror of ``ando.connector(slug)`` for the write half: it binds the
|
|
373
|
+
client so you drop the first argument on every call, and adds
|
|
374
|
+
:meth:`run` — propose + execute in one shot.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
client: A configured :class:`~ando_ai.client.AndoPlatform`.
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
def __init__(self, client: AndoPlatform) -> None:
|
|
381
|
+
self._client = client
|
|
382
|
+
|
|
383
|
+
# -- definitions (sources:manage / actions:read) ------------------------
|
|
384
|
+
|
|
385
|
+
def declare(
|
|
386
|
+
self,
|
|
387
|
+
connector: str,
|
|
388
|
+
name: str,
|
|
389
|
+
description: Optional[str] = None,
|
|
390
|
+
params_schema: Optional[dict] = None,
|
|
391
|
+
risk: Optional[str] = None,
|
|
392
|
+
binding: Optional[ActionBinding] = None,
|
|
393
|
+
) -> dict:
|
|
394
|
+
"""Declare an action on ``connector`` (:func:`declare_connector_action`)."""
|
|
395
|
+
return declare_connector_action(
|
|
396
|
+
self._client,
|
|
397
|
+
connector,
|
|
398
|
+
name,
|
|
399
|
+
description=description,
|
|
400
|
+
params_schema=params_schema,
|
|
401
|
+
risk=risk,
|
|
402
|
+
binding=binding,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def definitions(self, connector: str) -> dict:
|
|
406
|
+
"""A connector's declared actions (:func:`list_connector_actions`)."""
|
|
407
|
+
return list_connector_actions(self._client, connector)
|
|
408
|
+
|
|
409
|
+
def undeclare(self, connector: str, name: str) -> dict:
|
|
410
|
+
"""Remove a declaration (:func:`delete_connector_action`)."""
|
|
411
|
+
return delete_connector_action(self._client, connector, name)
|
|
412
|
+
|
|
413
|
+
def invoke(
|
|
414
|
+
self,
|
|
415
|
+
connector: str,
|
|
416
|
+
name: str,
|
|
417
|
+
params: Optional[dict] = None,
|
|
418
|
+
*,
|
|
419
|
+
confirm_risk: bool = False,
|
|
420
|
+
) -> dict:
|
|
421
|
+
"""Invoke an HTTP-bound capability (:func:`invoke_connector_action`)."""
|
|
422
|
+
return invoke_connector_action(
|
|
423
|
+
self._client,
|
|
424
|
+
connector,
|
|
425
|
+
name,
|
|
426
|
+
params=params,
|
|
427
|
+
confirm_risk=confirm_risk,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# -- request lifecycle (actions:write / actions:read) -------------------
|
|
431
|
+
|
|
432
|
+
def propose(
|
|
433
|
+
self,
|
|
434
|
+
connector: str,
|
|
435
|
+
action: str,
|
|
436
|
+
params: Optional[dict] = None,
|
|
437
|
+
idempotency_key: Optional[str] = None,
|
|
438
|
+
) -> dict:
|
|
439
|
+
"""Propose an action (:func:`propose_action`)."""
|
|
440
|
+
return propose_action(
|
|
441
|
+
self._client,
|
|
442
|
+
connector,
|
|
443
|
+
action,
|
|
444
|
+
params=params,
|
|
445
|
+
idempotency_key=idempotency_key,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
def execute(self, action_id: str, confirm_risk: bool = False) -> dict:
|
|
449
|
+
"""Dispatch a proposed action (:func:`execute_action`)."""
|
|
450
|
+
return execute_action(self._client, action_id, confirm_risk=confirm_risk)
|
|
451
|
+
|
|
452
|
+
def complete(
|
|
453
|
+
self,
|
|
454
|
+
action_id: str,
|
|
455
|
+
status: str,
|
|
456
|
+
result: Optional[dict] = None,
|
|
457
|
+
error: Optional[str] = None,
|
|
458
|
+
) -> dict:
|
|
459
|
+
"""Report a dispatched action's outcome (:func:`complete_action`)."""
|
|
460
|
+
return complete_action(
|
|
461
|
+
self._client, action_id, status, result=result, error=error
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
def get(self, action_id: str) -> dict:
|
|
465
|
+
"""Poll one request (:func:`get_action`)."""
|
|
466
|
+
return get_action(self._client, action_id)
|
|
467
|
+
|
|
468
|
+
def list(
|
|
469
|
+
self,
|
|
470
|
+
connector: Optional[str] = None,
|
|
471
|
+
limit: Optional[int] = None,
|
|
472
|
+
cursor: Optional[str] = None,
|
|
473
|
+
) -> dict:
|
|
474
|
+
"""List the project's requests (:func:`list_actions`)."""
|
|
475
|
+
return list_actions(
|
|
476
|
+
self._client, connector=connector, limit=limit, cursor=cursor
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# -- happy path ---------------------------------------------------------
|
|
480
|
+
|
|
481
|
+
def run(
|
|
482
|
+
self,
|
|
483
|
+
connector: str,
|
|
484
|
+
action: str,
|
|
485
|
+
params: Optional[dict] = None,
|
|
486
|
+
confirm_risk: bool = False,
|
|
487
|
+
idempotency_key: Optional[str] = None,
|
|
488
|
+
) -> dict:
|
|
489
|
+
"""Propose then execute in one call — the write-loop's happy path.
|
|
490
|
+
|
|
491
|
+
Returns the ``dispatched`` request. The ``action.requested`` webhook
|
|
492
|
+
is now on its way to your app; the app still reports the outcome via
|
|
493
|
+
:meth:`complete` (or its own webhook handler). ``confirm_risk`` is
|
|
494
|
+
required for ``high``/``critical`` actions, exactly as on
|
|
495
|
+
:meth:`execute`. Pass ``idempotency_key`` to make the PROPOSE half
|
|
496
|
+
idempotent (a retried ``run`` with the same key replays the proposal
|
|
497
|
+
rather than duplicating it before the execute).
|
|
498
|
+
"""
|
|
499
|
+
proposed = self.propose(
|
|
500
|
+
connector, action, params=params, idempotency_key=idempotency_key
|
|
501
|
+
)
|
|
502
|
+
return self.execute(proposed["id"], confirm_risk=confirm_risk)
|