ando-ai 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ando_ai-0.4.0/LICENSE +50 -0
- ando_ai-0.4.0/PKG-INFO +595 -0
- ando_ai-0.4.0/README.md +578 -0
- ando_ai-0.4.0/ando_ai/__init__.py +147 -0
- ando_ai-0.4.0/ando_ai/__main__.py +6 -0
- ando_ai-0.4.0/ando_ai/actions.py +502 -0
- ando_ai-0.4.0/ando_ai/cli.py +517 -0
- ando_ai-0.4.0/ando_ai/client.py +1709 -0
- ando_ai-0.4.0/ando_ai/connectors.py +471 -0
- ando_ai-0.4.0/ando_ai.egg-info/PKG-INFO +595 -0
- ando_ai-0.4.0/ando_ai.egg-info/SOURCES.txt +15 -0
- ando_ai-0.4.0/ando_ai.egg-info/dependency_links.txt +1 -0
- ando_ai-0.4.0/ando_ai.egg-info/entry_points.txt +2 -0
- ando_ai-0.4.0/ando_ai.egg-info/requires.txt +1 -0
- ando_ai-0.4.0/ando_ai.egg-info/top_level.txt +1 -0
- ando_ai-0.4.0/pyproject.toml +39 -0
- ando_ai-0.4.0/setup.cfg +4 -0
ando_ai-0.4.0/LICENSE
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Ando Platform SDK License
|
|
2
|
+
=========================
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Ando S.r.l. (ando-ai.com). All rights reserved.
|
|
5
|
+
|
|
6
|
+
THIS IS NOT AN OPEN SOURCE LICENSE. This software is proprietary and
|
|
7
|
+
confidential to Ando S.r.l. ("Ando").
|
|
8
|
+
|
|
9
|
+
1. License grant. Subject to these terms, Ando grants you a limited,
|
|
10
|
+
non-exclusive, non-transferable, non-sublicensable, revocable license to
|
|
11
|
+
install and use this software (the "SDK") solely to access the Ando
|
|
12
|
+
Platform services under an active agreement or evaluation arrangement
|
|
13
|
+
with Ando.
|
|
14
|
+
|
|
15
|
+
2. Restrictions. Except as expressly permitted above or by mandatory
|
|
16
|
+
applicable law, you may not: (a) copy, modify, adapt, translate or
|
|
17
|
+
create derivative works of the SDK; (b) distribute, sell, rent, lease,
|
|
18
|
+
sublicense or otherwise transfer the SDK to any third party;
|
|
19
|
+
(c) reverse engineer, decompile or disassemble the SDK; (d) use the SDK
|
|
20
|
+
to develop a product or service that competes with the Ando Platform;
|
|
21
|
+
(e) remove or alter any proprietary notices.
|
|
22
|
+
|
|
23
|
+
3. Ownership. The SDK is licensed, not sold. Ando and its licensors retain
|
|
24
|
+
all right, title and interest in and to the SDK, including all
|
|
25
|
+
intellectual property rights. No rights are granted by implication,
|
|
26
|
+
estoppel or otherwise.
|
|
27
|
+
|
|
28
|
+
4. Feedback. If you provide feedback about the SDK, Ando may use it
|
|
29
|
+
without restriction or obligation.
|
|
30
|
+
|
|
31
|
+
5. No warranty. THE SDK IS PROVIDED "AS IS" AND "AS AVAILABLE", WITHOUT
|
|
32
|
+
WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
|
|
33
|
+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE
|
|
34
|
+
AND NON-INFRINGEMENT.
|
|
35
|
+
|
|
36
|
+
6. Limitation of liability. TO THE MAXIMUM EXTENT PERMITTED BY LAW, ANDO
|
|
37
|
+
SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL,
|
|
38
|
+
CONSEQUENTIAL OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS, REVENUE,
|
|
39
|
+
DATA OR GOODWILL, ARISING OUT OF OR RELATED TO THE SDK, EVEN IF ADVISED
|
|
40
|
+
OF THE POSSIBILITY OF SUCH DAMAGES.
|
|
41
|
+
|
|
42
|
+
7. Termination. This license terminates automatically if you breach these
|
|
43
|
+
terms or when your agreement with Ando ends. Upon termination you must
|
|
44
|
+
stop using and delete all copies of the SDK.
|
|
45
|
+
|
|
46
|
+
8. Governing law. This license is governed by the laws of Italy, without
|
|
47
|
+
regard to conflict-of-law principles. Exclusive venue: the courts of
|
|
48
|
+
Bari, Italy.
|
|
49
|
+
|
|
50
|
+
For licensing questions: Ando S.r.l. — Bari, Italy — VAT/C.F. 09171580724 — ando-ai.com
|
ando_ai-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ando-ai
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Official Python client SDK and CLI for the Ando Platform API. Requires an active Ando account and API key.
|
|
5
|
+
Author: Ando S.r.l.
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://ando-ai.com
|
|
8
|
+
Project-URL: Repository, https://github.com/AndreaBovinelli/ando-agent
|
|
9
|
+
Project-URL: Issues, https://github.com/AndreaBovinelli/ando-agent/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: httpx>=0.24
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# Ando Platform — Python SDK
|
|
19
|
+
|
|
20
|
+
Client Python **sincrono** per la Ando Platform API v1
|
|
21
|
+
(`https://api.ando-ai.com/platform/v1`). Unica dipendenza: `httpx>=0.24`.
|
|
22
|
+
|
|
23
|
+
Endpoint predefinito: `https://api.ando-ai.com/platform/v1`.
|
|
24
|
+
|
|
25
|
+
## Requisiti di accesso
|
|
26
|
+
|
|
27
|
+
Questo pacchetto contiene solo il client SDK: non include né rende
|
|
28
|
+
autonomamente disponibile il servizio Ando Platform. Per usarlo servono un
|
|
29
|
+
account Ando attivo (o un accesso di valutazione), i relativi diritti di
|
|
30
|
+
licenza e una API key emessa da Ando. Per richiedere l'attivazione, contatta
|
|
31
|
+
Ando su [ando-ai.com](https://ando-ai.com).
|
|
32
|
+
|
|
33
|
+
## Installazione
|
|
34
|
+
|
|
35
|
+
Requisito: Python 3.9+.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install ando-ai
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Uso
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from ando_ai import AndoPlatform, ApiError, JobFailed
|
|
45
|
+
|
|
46
|
+
with AndoPlatform("ando_sk_live_...") as ando:
|
|
47
|
+
accepted = ando.upload_document("contratto.pdf")
|
|
48
|
+
ando.wait_for_job(accepted["job_id"])
|
|
49
|
+
|
|
50
|
+
result = ando.answer("Qual è la durata del contratto?")
|
|
51
|
+
print(result["answer"], result["citations"])
|
|
52
|
+
|
|
53
|
+
ando.delete_document(accepted["document_id"])
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Il client è un context manager; in alternativa chiama `ando.close()` a fine
|
|
57
|
+
lavoro.
|
|
58
|
+
|
|
59
|
+
## Cosa puoi costruire — i tre livelli
|
|
60
|
+
|
|
61
|
+
Lo stesso stack, esposto a tre profondità crescenti. Non sono alternative:
|
|
62
|
+
sono quanto lavoro fa il server al posto tuo.
|
|
63
|
+
|
|
64
|
+
### 1. `query()` — recuperi i chunk, generi tu
|
|
65
|
+
|
|
66
|
+
Per chi ha già il proprio LLM/prompt e vuole solo il retrieval (con
|
|
67
|
+
citazioni e score).
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
hits = ando.query("penali per ritardata consegna", k=8)
|
|
71
|
+
for hit in hits["results"]:
|
|
72
|
+
print(hit["score"], hit["section"], hit["text"][:120])
|
|
73
|
+
# hit["document_id"] / hit["chunk_id"] = la citazione da mostrare
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. `answer()` — risposta sintetizzata e ancorata
|
|
77
|
+
|
|
78
|
+
Il server recupera **e** sintetizza, citando i chunk usati. La risposta
|
|
79
|
+
rispecchia la lingua della domanda (non si forza nulla). Se il retrieval
|
|
80
|
+
non trova nulla: `answer` è `None` e `no_results` è `True`.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
result = ando.answer("Qual è la durata del contratto?", verify=True)
|
|
84
|
+
print(result["answer"])
|
|
85
|
+
print(result["citations"]) # [{document_id, chunk_id}, ...]
|
|
86
|
+
print(result["verification"]) # {verified, score, notes} | None
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 3. `agentic_answer()` — l'agente completo, anche sui dati strutturati (PREVIEW)
|
|
90
|
+
|
|
91
|
+
Questa è la differenza tra «ti do dei chunk» e «ho interrogato il tuo
|
|
92
|
+
gestionale». Gira la stessa macchina della chat prodotto: escalation TRAMA
|
|
93
|
+
T1→T2→T3 e, sulle **sources strutturate pubblicate** del project,
|
|
94
|
+
text-to-SQL — quindi una domanda la cui risposta sta in una tabella viene
|
|
95
|
+
risposta *dalla tabella*, non approssimata dai documenti.
|
|
96
|
+
|
|
97
|
+
**Read-only per costruzione**: nessuna scrittura, nessuna azione, nessuna
|
|
98
|
+
persistenza. `steps[]` mostra cosa ha fatto l'agente (tier di retrieval,
|
|
99
|
+
SQL eseguito con credenziali redatte, specialist scelto).
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
result = ando.agentic_answer(
|
|
103
|
+
"Quali clienti hanno esposizione più critica e per quale importo?",
|
|
104
|
+
k=25,
|
|
105
|
+
with_diagnostics=True, # tier, timing, plan cache
|
|
106
|
+
)
|
|
107
|
+
print(result["answer"])
|
|
108
|
+
print(result["sources_used"]) # es. ["database", "fs"]
|
|
109
|
+
for step in result["steps"]:
|
|
110
|
+
print(step["type"], step) # tipi sconosciuti = additivi, non ramificarci sopra
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Una chiamata agentica è multi-step: il timeout di default è
|
|
114
|
+
`AGENTIC_TIMEOUT` (120s), non i 30s del client. Si sovrascrive per
|
|
115
|
+
chiamata con `timeout=`.
|
|
116
|
+
|
|
117
|
+
> **PREVIEW**: `POST /agentic/answer` non è ancora GA — request e response
|
|
118
|
+
> possono cambiare. Vedi `docs/PLATFORM_API_CONTRACT.md`.
|
|
119
|
+
|
|
120
|
+
### Conversazioni — memoria tra turni agentici
|
|
121
|
+
|
|
122
|
+
Un turno agentico è stateless di default. Apri un thread con
|
|
123
|
+
`create_conversation()` e passa il suo id a `agentic_answer()`: i follow-up
|
|
124
|
+
si risolvono contro la storia del thread («e il secondo?» funziona) e ogni
|
|
125
|
+
scambio viene persistito per il turno successivo. Scope `query:read`.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
conv = ando.create_conversation(title="Analisi esposizione")
|
|
129
|
+
ando.agentic_answer("Chi sono i primi 3 clienti per esposizione?",
|
|
130
|
+
conversation_id=conv["conversation_id"])
|
|
131
|
+
ando.agentic_answer("E il secondo?", # follow-up risolto
|
|
132
|
+
conversation_id=conv["conversation_id"])
|
|
133
|
+
|
|
134
|
+
ando.list_conversations() # thread della chiave, cursor-paginati
|
|
135
|
+
ando.get_conversation_turns(conv["conversation_id"]) # scambi, dal più vecchio
|
|
136
|
+
ando.delete_conversation(conv["conversation_id"]) # irreversibile
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Restringere il campo: `filters`, `sources`, `partition`
|
|
140
|
+
|
|
141
|
+
Valgono su `query()` e `answer()`. Ogni filtro può solo **restringere**: la
|
|
142
|
+
resource policy della chiave è sempre intersecata sopra e vince.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
# scorciatoie da keyword (le due dimensioni più usate)
|
|
146
|
+
ando.answer("fatture scadute", sources=["fs"], partition="cliente-42")
|
|
147
|
+
|
|
148
|
+
# oggetto completo, con condizioni sui metadata di ingest
|
|
149
|
+
ando.query(
|
|
150
|
+
"resi 2025",
|
|
151
|
+
filters={
|
|
152
|
+
"sources": ["fs", "drive"],
|
|
153
|
+
"partition": "cliente-42",
|
|
154
|
+
"metadata": {
|
|
155
|
+
"op": "and",
|
|
156
|
+
"conditions": [
|
|
157
|
+
{"key": "doc_type", "op": "eq", "value": "invoice"},
|
|
158
|
+
{"key": "year", "op": "gte", "value": 2025},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`partition` e `metadata` sono esattamente quelli impostati all'ingest — il
|
|
166
|
+
cerchio si chiude sullo stesso client:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
ando.upload_document(
|
|
170
|
+
"fattura-2025-11.pdf",
|
|
171
|
+
metadata={"doc_type": "invoice", "year": 2025}, # oggetto PIATTO, max 32 chiavi
|
|
172
|
+
partition="cliente-42", # A-Za-z0-9._:- , max 128 char
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Passare la stessa dimensione due volte (keyword **e** dentro `filters`) è
|
|
177
|
+
un `ValueError` lato client, prima di qualunque HTTP: sceglierne una in
|
|
178
|
+
silenzio cambierebbe quali documenti vengono cercati. I nomi di source
|
|
179
|
+
sconosciuti **non** vengono rifiutati dal SDK: i valori enum sono additivi
|
|
180
|
+
per contratto, decide il server.
|
|
181
|
+
|
|
182
|
+
## Generazione documenti — `generate()`
|
|
183
|
+
|
|
184
|
+
Dai tuoi dati a un file `docx`/`pptx`/`xlsx`/`pdf`/`md`. Il contesto arriva da
|
|
185
|
+
una `query` (retrieval TRAMA, resource policy rispettata) e/o da
|
|
186
|
+
`document_ids` espliciti; almeno uno dei due è obbligatorio (altrimenti
|
|
187
|
+
`ValueError` prima di qualunque HTTP). È **sempre asincrono**: `202` con un
|
|
188
|
+
`generation_id` da pollare, poi si scarica il file. Il contenuto rispecchia la
|
|
189
|
+
lingua di `instructions`/`query`. Scope unico per tutto: `generate:write`.
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
accepted = ando.generate(
|
|
193
|
+
"docx",
|
|
194
|
+
"Prepara un report sul fatturato 2025 con sintesi e dettaglio.",
|
|
195
|
+
query="fatturato 2025",
|
|
196
|
+
)
|
|
197
|
+
gen = ando.wait_for_generation(accepted["generation_id"]) # -> completed | GenerationFailed
|
|
198
|
+
artifact = ando.download_generation(gen["generation_id"]) # DownloadedFile
|
|
199
|
+
artifact.save("report.docx") # bytes su disco
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`wait_for_generation` condivide identica policy di retry/backoff di
|
|
203
|
+
`wait_for_job` (transient `rate_limited`/5xx/rete ritentati entro la deadline);
|
|
204
|
+
solleva `GenerationFailed` (con `error_code`) su `failed`, `GenerationTimeout`
|
|
205
|
+
allo scadere.
|
|
206
|
+
|
|
207
|
+
## CAD — analisi, DFM, conversione
|
|
208
|
+
|
|
209
|
+
Upload STEP/DXF asincroni (`202` con `job_id` + `result_id`). Analisi e DFM
|
|
210
|
+
tornano un **report JSON**, la conversione un **file STEP**. Scope `cad:read`
|
|
211
|
+
per analyze/dfm-check/similar/jobs/results, `cad:write` per convert. Un job CAD
|
|
212
|
+
**è un job**: `wait_for_cad_job` riusa `JobFailed`/`JobTimeout`.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
job = ando.cad_analyze("bracket.step") # {result_id, job_id, kind: "analysis"}
|
|
216
|
+
ando.wait_for_cad_job(job["job_id"])
|
|
217
|
+
report = ando.get_cad_result(job["result_id"]).json() # report JSON
|
|
218
|
+
|
|
219
|
+
conv = ando.cad_convert("profilo.dxf", "extrude", params={"height_mm": 10})
|
|
220
|
+
ando.wait_for_cad_job(conv["job_id"])
|
|
221
|
+
ando.get_cad_result(conv["result_id"]).save("solido.step") # STEP su disco
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`cad_find_similar()` è la **ricerca per somiglianza query-by-upload** (live):
|
|
225
|
+
carichi uno STEP/IGES/STL, un disegno PDF o un'immagine; viene embeddato al
|
|
226
|
+
volo e confrontato con il corpus CAD ingerito del project. Il file di query
|
|
227
|
+
non viene mai salvato. La chiamata è **sincrona** — uno STEP grande può
|
|
228
|
+
richiedere ~10-30s di tessellazione, dimensiona il timeout di conseguenza.
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
simili = ando.cad_find_similar("staffa.step", top_k=5)
|
|
232
|
+
for hit in simili["results"]:
|
|
233
|
+
print(hit["filename"], hit["similarity"], hit["description"])
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Azioni — il write-loop
|
|
237
|
+
|
|
238
|
+
La metà **write** simmetrica della Push API: chiedi all'app del project di
|
|
239
|
+
*fare* qualcosa (aprire un ticket, spostare un ordine) e ne verifichi
|
|
240
|
+
l'esito. `declare` (una tantum, `sources:manage`) → `propose` → `execute`
|
|
241
|
+
(`actions:write`) → l'app fa `complete`. Le letture usano `actions:read`.
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
actions = ando.actions()
|
|
245
|
+
|
|
246
|
+
# dichiarazione una tantum (chiave sources:manage)
|
|
247
|
+
actions.declare(
|
|
248
|
+
"acme-desk",
|
|
249
|
+
"create_ticket",
|
|
250
|
+
params_schema={
|
|
251
|
+
"type": "object",
|
|
252
|
+
"properties": {"subject": {"type": "string"}},
|
|
253
|
+
"required": ["subject"],
|
|
254
|
+
},
|
|
255
|
+
risk="medium",
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# per richiesta (chiave actions:write) — propose + execute in una chiamata
|
|
259
|
+
dispatched = actions.run(
|
|
260
|
+
"acme-desk",
|
|
261
|
+
"create_ticket",
|
|
262
|
+
{"subject": "Refund request"},
|
|
263
|
+
idempotency_key="refund-4711", # un run ritentato replay-a la proposta
|
|
264
|
+
)
|
|
265
|
+
print(dispatched["id"], dispatched["status"]) # ... "dispatched"
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Le azioni `high`/`critical` richiedono `confirm_risk=True` su
|
|
269
|
+
`run`/`execute`. `propose_action` accetta un `idempotency_key` opzionale
|
|
270
|
+
(header `Idempotency-Key`): una propose ritentata con la stessa chiave
|
|
271
|
+
replay-a il primo `201` invece di duplicare. Ogni metodo esiste anche come
|
|
272
|
+
funzione di modulo (`propose_action(client, ...)`) e come delega sul client
|
|
273
|
+
(`ando.propose_action(...)`).
|
|
274
|
+
|
|
275
|
+
### Capability con binding HTTP — dichiara, poi `invoke`
|
|
276
|
+
|
|
277
|
+
Dichiara la capability **con un `binding` HTTP** e Ando può chiamare
|
|
278
|
+
direttamente il tuo sistema: `invoke_connector_action` è il fratello
|
|
279
|
+
sincrono di propose + execute — un round trip, esito nella risposta.
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
# dichiarazione CON binding (chiave sources:manage)
|
|
283
|
+
actions.declare(
|
|
284
|
+
"acme-desk",
|
|
285
|
+
"create_ticket",
|
|
286
|
+
params_schema={
|
|
287
|
+
"type": "object",
|
|
288
|
+
"properties": {"subject": {"type": "string"}},
|
|
289
|
+
"required": ["subject"],
|
|
290
|
+
},
|
|
291
|
+
risk="medium",
|
|
292
|
+
binding={
|
|
293
|
+
"method": "POST",
|
|
294
|
+
"path": "/tickets", # relativo, sul TUO sistema
|
|
295
|
+
"body_template": {"title": "{{subject}}"}, # placeholder {{param}}
|
|
296
|
+
"response": {"id_field": "id"}, # dove vive l'id creato
|
|
297
|
+
"timeout_seconds": 15,
|
|
298
|
+
},
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# invoke sincrono (chiave actions:write)
|
|
302
|
+
esito = ando.invoke_connector_action(
|
|
303
|
+
"acme-desk", "create_ticket", {"subject": "Refund request"}
|
|
304
|
+
)
|
|
305
|
+
print(esito["ok"], esito["status_code"], esito["response"])
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
I `params` sono validati contro il `params_schema` dichiarato e vale lo
|
|
309
|
+
**stesso risk gate di execute**: una capability `high`/`critical` viene
|
|
310
|
+
rifiutata (`invalid_request`) senza `confirm_risk=True`. Le chiavi di test
|
|
311
|
+
validano e gate-ano per davvero ma **non** chiamano mai l'endpoint bound —
|
|
312
|
+
la risposta porta `test: true`. La dichiarazione è una sostituzione
|
|
313
|
+
completa: ri-dichiarare senza `binding` cancella quello memorizzato.
|
|
314
|
+
|
|
315
|
+
## Flows — orchestrazione come API
|
|
316
|
+
|
|
317
|
+
Costruisci, esegui e supervisiona gli stessi flow del builder prodotto,
|
|
318
|
+
white-label. Scope: `flows:read` (list/palette/stats/inbox/run detail) e
|
|
319
|
+
`flows:manage` (create/update/delete/run/approve/reject).
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
palette = ando.flows_palette() # ogni node type + campi di config
|
|
323
|
+
flows = ando.list_flows(page=1, page_size=20)
|
|
324
|
+
draft = ando.generate_flow(
|
|
325
|
+
"Quando arriva una fattura, valida i dati e chiedi approvazione"
|
|
326
|
+
) # copilot: NL -> draft flow (costo LLM, metered)
|
|
327
|
+
|
|
328
|
+
created = ando.create_flow(draft["flow"])
|
|
329
|
+
ando.run_flow(created["flow_id"], dry_run=True)
|
|
330
|
+
ando.flow_stats(created["flow_id"])
|
|
331
|
+
ando.update_flow(created["flow_id"], {"is_active": False})
|
|
332
|
+
|
|
333
|
+
# run manuale seminato con un PDF caricato: il suo content_item_id finisce
|
|
334
|
+
# nel trigger_data del run per i nodi che bindano
|
|
335
|
+
# {"$ref": "trigger.content_item_id"} (es. ai.cad_analyze)
|
|
336
|
+
run = ando.run_flow_with_upload(created["flow_id"], "fattura.pdf")
|
|
337
|
+
|
|
338
|
+
# polling di un run: stato, trigger, context accumulato, output per nodo
|
|
339
|
+
detail = ando.get_flow_run(run["run_id"])
|
|
340
|
+
print(detail["status"], detail["node_runs"])
|
|
341
|
+
|
|
342
|
+
# HITL: gli step `logic.approval` parcheggiano il run in `pending_approval`
|
|
343
|
+
inbox = ando.flows_pending_approvals()
|
|
344
|
+
ando.approve_flow_run("run_...", edited_value="testo corretto")
|
|
345
|
+
ando.approve_flow_run(
|
|
346
|
+
"run_...",
|
|
347
|
+
# correzione strutturata per-campo dello STESSO output editabile — se
|
|
348
|
+
# arrivano entrambi, edited_fields vince sulle chiavi sovrapposte
|
|
349
|
+
edited_fields={"to": "acme@example.com", "subject": "Ordine 42"},
|
|
350
|
+
)
|
|
351
|
+
ando.reject_flow_run("run_...", reason="importo errato")
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`run_flow_with_upload` accetta solo PDF (`invalid_request` su tutto il
|
|
355
|
+
resto). `flow_templates()` elenca la galleria di template;
|
|
356
|
+
`delete_flow(flow_id)` rimuove un flow.
|
|
357
|
+
|
|
358
|
+
## Usage — consumi e costi del project
|
|
359
|
+
|
|
360
|
+
`usage()` (`usage:read`) ritorna chiamate/token/costo aggregati del project —
|
|
361
|
+
mese corrente di default, `group_by="day"` per la serie di fatturazione o
|
|
362
|
+
`"purpose"` per capire cosa genera costo:
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
report = ando.usage(from_="2026-08-01", to="2026-08-07")
|
|
366
|
+
print(report["total_cost_usd"], report["usage"])
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Sources — cosa può interrogare questa chiave
|
|
370
|
+
|
|
371
|
+
Prima di scopare una `/query` (come fa la UI prodotto lasciando scegliere un
|
|
372
|
+
connettore) bisogna **scoprire** cosa la chiave può interrogare. `list_sources()`
|
|
373
|
+
lo dice, già intersecato con la resource policy della chiave.
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
for s in ando.list_sources()["sources"]:
|
|
377
|
+
print(s["id"], s["kind"], s["queryable"], s.get("reason"))
|
|
378
|
+
# id unstrutturati (fs/drive/…) -> filters.sources di /query
|
|
379
|
+
# id strutturati (conn_<uuid>) -> sources di /agentic/answer
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Registra un database read-only del cliente (es. il back office SQL di un
|
|
383
|
+
gestionale) come source strutturata — le credenziali sono cifrate at rest e
|
|
384
|
+
**mai** ritornate; la risposta è white-label (capability, mai il vendor):
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
source = ando.register_database_source(
|
|
388
|
+
name="Gestionale produzione",
|
|
389
|
+
connector_type="mysql", # postgresql | mysql | sqlserver | oracle | ...
|
|
390
|
+
credentials={"host": "db.example.com", "user": "ando_ro",
|
|
391
|
+
"password": "…", "database": "mexal"},
|
|
392
|
+
config={"schema_filter": ["mexal"], "max_rows_per_query": 500},
|
|
393
|
+
)
|
|
394
|
+
ando.test_source(source["source_id"]) # {ok, error?} — probe, non un ApiError
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Per una source **strutturata** lo schema profilato si cura e poi si
|
|
398
|
+
**pubblica** (il gate di queryabilità): finché non è pubblicato,
|
|
399
|
+
`queryable=false, reason="schema non pubblicato"`. Curare = `sources:manage`,
|
|
400
|
+
leggere lo schema = `query:read`.
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
ando.get_source_schema("conn_…") # stato + objects (K-Schema)
|
|
404
|
+
ando.curate_source_schema( # override della proposta AI
|
|
405
|
+
"conn_…", "public.orders",
|
|
406
|
+
description="Ordini di vendita",
|
|
407
|
+
column_descriptions={"total": "Importo totale, IVA inclusa"},
|
|
408
|
+
)
|
|
409
|
+
ando.publish_source_schema("conn_…") # apre /query + /agentic
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Webhooks
|
|
413
|
+
|
|
414
|
+
Endpoint HTTPS in uscita, firmati Standard Webhooks. Scope `keys:manage`. Il
|
|
415
|
+
`secret` (`whsec_…`) è mostrato **una sola volta** alla creazione.
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
hook = ando.create_webhook("https://example.com/hooks", ["ingestion.completed"])
|
|
419
|
+
print(hook["secret"]) # salvalo ora — non torna più
|
|
420
|
+
ando.list_webhooks() # mai il secret; cursor/limit opzionali
|
|
421
|
+
ando.test_webhook(hook["webhook_id"]) # test.ping sincrono
|
|
422
|
+
|
|
423
|
+
# storico deliveries + replay manuale (firma fresca, tentativo immediato)
|
|
424
|
+
deliveries = ando.list_webhook_deliveries(hook["webhook_id"], limit=100)
|
|
425
|
+
ando.replay_webhook_delivery(deliveries["deliveries"][0]["delivery_id"])
|
|
426
|
+
|
|
427
|
+
ando.delete_webhook(hook["webhook_id"])
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Riferimento
|
|
431
|
+
|
|
432
|
+
Costruttore:
|
|
433
|
+
|
|
434
|
+
```python
|
|
435
|
+
AndoPlatform(
|
|
436
|
+
api_key, # ando_sk_live_... / ando_sk_test_...
|
|
437
|
+
base_url="https://api.ando-ai.com/platform/v1",
|
|
438
|
+
timeout=30, # secondi, per richiesta
|
|
439
|
+
)
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
| Metodo | Endpoint | Scope | Note |
|
|
443
|
+
|---|---|---|---|
|
|
444
|
+
| `health()` | `GET /health` | — | liveness pubblica |
|
|
445
|
+
| `me()` | `GET /me` | chiave valida | identità del project |
|
|
446
|
+
| `upload_document(path_or_bytes, filename=None, content_type=None, metadata=None, partition=None)` | `POST /documents` | `ingest:write` | multipart; accetta un path (filename dedotto) o `bytes` (filename obbligatorio); `metadata`/`partition` opzionali diventano parti multipart solo se valorizzati; ritorna `{document_id, job_id, status}` (202) |
|
|
447
|
+
| `get_document(id)` | `GET /documents/{id}` | `query:read` | stato + metadati |
|
|
448
|
+
| `delete_document(id)` | `DELETE /documents/{id}` | `ingest:write` | rimozione indice + storage |
|
|
449
|
+
| `get_job(id)` | `GET /jobs/{id}` | `query:read` | `queued → processing → completed \| failed` |
|
|
450
|
+
| `wait_for_job(id, timeout=120, poll=2.0)` | polling su `GET /jobs/{id}` | `query:read` | ritorna il job a `completed`; solleva `JobFailed` (con `error_code`) su `failed`, `JobTimeout` allo scadere |
|
|
451
|
+
| `query(text, mode="retrieve", k=10, filters=None, sources=None, partition=None, include_web=False, verify=False)` | `POST /query` | `query:read` | `k` max 50 da contratto; i campi opzionali finiscono nel body solo se valorizzati |
|
|
452
|
+
| `answer(text, k=10, ...)` | `POST /query` | `query:read` | scorciatoia per `mode="answer"`, stessi parametri di narrowing |
|
|
453
|
+
| `agentic_answer(query, k=10, sources=None, max_steps=None, with_diagnostics=False, include_rows=False, conversation_id=None, timeout=120)` | `POST /agentic/answer` | `query:read` | **PREVIEW** — loop agentico read-only; ritorna `{answer, citations, sources_used, steps, diagnostics}` |
|
|
454
|
+
| `create_conversation(title=None)` | `POST /conversations` | `query:read` | apre un thread di memoria per i turni agentici |
|
|
455
|
+
| `list_conversations(cursor=None, limit=None)` | `GET /conversations` | `query:read` | thread della chiave, cursor-paginati |
|
|
456
|
+
| `get_conversation_turns(id, cursor=None, limit=None)` | `GET /conversations/{id}` | `query:read` | scambi del thread, dal più vecchio |
|
|
457
|
+
| `delete_conversation(id)` | `DELETE /conversations/{id}` | `query:read` | irreversibile |
|
|
458
|
+
| `connectors()` | `GET /connectors` | `sources:manage` | elenco connector del project |
|
|
459
|
+
| `connector(slug, **options)` | — | — | costruisce un `AndoConnector` legato a questo client (Push API) |
|
|
460
|
+
| `connector_kinds()` | `GET /connectors/kinds` | chiave valida | kind disponibili + contratto Record |
|
|
461
|
+
| `validate_records(records, connector=None)` | `POST /connectors/validate` | `ingest:write` | dry-run dei record, nessuna scrittura |
|
|
462
|
+
| `update_connector(slug, name=None, config=None, enabled=None)` | `PATCH /connectors/{slug}` | `sources:manage` | rename/enable/config (sostituita intera); rotazione secret per i manifest connector |
|
|
463
|
+
| `actions()` | — | — | costruisce un `AndoActions` legato a questo client (`run(...)` = propose + execute) |
|
|
464
|
+
| `declare_connector_action(connector, name, description=None, params_schema=None, risk=None, binding=None)` | `POST /connectors/{slug}/actions` | `sources:manage` | idempotente su `name`; `binding` = binding HTTP opzionale (abilita l'invoke); ri-dichiarare senza `binding` lo cancella |
|
|
465
|
+
| `connector_actions(connector)` | `GET /connectors/{slug}/actions` | `actions:read` | azioni dichiarate del connector |
|
|
466
|
+
| `delete_connector_action(connector, name)` | `DELETE /connectors/{slug}/actions/{name}` | `sources:manage` | rimuove una dichiarazione |
|
|
467
|
+
| `invoke_connector_action(connector, name, params=None, *, confirm_risk=False)` | `POST /connectors/{slug}/actions/{name}/invoke` | `actions:write` | invoke sincrono della capability bound: `{ok, status_code, response}`; risk gate di execute (`high`/`critical` rifiutati senza `confirm_risk=True`); chiave test = `test: true`, endpoint mai chiamato |
|
|
468
|
+
| `propose_action(connector, action, params=None, idempotency_key=None)` | `POST /actions` | `actions:write` | valida + preview, nessun side effect; `Idempotency-Key` opzionale |
|
|
469
|
+
| `execute_action(action_id, confirm_risk=False)` | `POST /actions/{id}/execute` | `actions:write` | dispatch del webhook `action.requested`; `high`/`critical` richiedono `confirm_risk` |
|
|
470
|
+
| `complete_action(action_id, status, result=None, error=None)` | `POST /actions/{id}/complete` | `actions:write` | l'app riporta l'esito (`completed`/`failed`) |
|
|
471
|
+
| `get_action(action_id)` | `GET /actions/{id}` | `actions:read` | polling di una richiesta |
|
|
472
|
+
| `list_actions(connector=None, limit=None, cursor=None)` | `GET /actions` | `actions:read` | richieste del project, cursor-paginate |
|
|
473
|
+
| `generate(format, instructions, query=None, document_ids=None, k=10)` | `POST /generate` | `generate:write` | job async; serve almeno uno tra `query`/`document_ids`; ritorna `{generation_id, document_id, status}` (202) |
|
|
474
|
+
| `get_generation(id)` | `GET /generate/{id}` | `generate:write` | stato; `download_url` a `completed` |
|
|
475
|
+
| `wait_for_generation(id, timeout=180, poll=2.0)` | polling su `GET /generate/{id}` | `generate:write` | ritorna il payload a `completed`; `GenerationFailed`/`GenerationTimeout` |
|
|
476
|
+
| `download_generation(id)` | `GET /generate/{id}/download` | `generate:write` | `DownloadedFile` (bytes + content type + filename) |
|
|
477
|
+
| `cad_analyze(path_or_bytes, ...)` | `POST /cad/analyze` | `cad:read` | STEP/DXF; job async `kind="analysis"` |
|
|
478
|
+
| `cad_dfm_check(path_or_bytes, ruleset_name=None, ...)` | `POST /cad/dfm-check` | `cad:read` | solo STEP; `ruleset_name` inviato solo se valorizzato |
|
|
479
|
+
| `cad_convert(path_or_bytes, operation, params=None, target="step", ...)` | `POST /cad/convert` | `cad:write` | DXF→STEP; `operation` `extrude`/`revolve`; params validati server-side |
|
|
480
|
+
| `cad_find_similar(path_or_bytes, filename=None, content_type=None, top_k=8)` | `POST /cad/similar` | `cad:read` | ricerca per somiglianza query-by-upload; sincrona, il file di query non viene salvato |
|
|
481
|
+
| `get_cad_job(id)` | `GET /cad/jobs/{id}` | `cad:read` | stato job CAD |
|
|
482
|
+
| `wait_for_cad_job(id, timeout=180, poll=2.0)` | polling su `GET /cad/jobs/{id}` | `cad:read` | `JobFailed`/`JobTimeout` (un job CAD è un job) |
|
|
483
|
+
| `get_cad_result(id)` | `GET /cad/results/{id}` | `cad:read` | `DownloadedFile`; `.json()` per analisi/DFM, `.save()` per STEP |
|
|
484
|
+
| `list_sources()` | `GET /sources` | `query:read` | cosa la chiave può interrogare (già intersecato con la policy) |
|
|
485
|
+
| `register_database_source(name=..., connector_type=..., credentials=..., config=None)` | `POST /sources/database` | `sources:manage` | registra un DB read-only del cliente; credenziali cifrate, mai ritornate; risposta white-label |
|
|
486
|
+
| `test_source(source_id)` | `POST /sources/{id}/test` | `sources:manage` | probe di connessione; un probe fallito è `{ok: false}` nel body 200, non un ApiError |
|
|
487
|
+
| `get_source_schema(source_id)` | `GET /sources/{id}/schema` | `query:read` | K-Schema profilato di una source strutturata |
|
|
488
|
+
| `curate_source_schema(source_id, object_name, description=None, column_descriptions=None, semantic_mappings=None)` | `PATCH /sources/{id}/schema/{object}` | `sources:manage` | override curazione AI; almeno un campo (altrimenti `ValueError`) |
|
|
489
|
+
| `publish_source_schema(source_id, published=True)` | `POST /sources/{id}/schema/publish` | `sources:manage` | gate di queryabilità; `published=False` per revertire |
|
|
490
|
+
| `create_webhook(url, events)` | `POST /webhooks` | `keys:manage` | il `secret` appare SOLO qui (201) |
|
|
491
|
+
| `list_webhooks(cursor=None, limit=None)` | `GET /webhooks` | `keys:manage` | mai il secret; cursor-paginato con `cursor`/`limit` |
|
|
492
|
+
| `delete_webhook(id)` | `DELETE /webhooks/{id}` | `keys:manage` | rimozione; deliveries pending scartate |
|
|
493
|
+
| `test_webhook(id)` | `POST /webhooks/{id}/test` | `keys:manage` | `test.ping` sincrono; esito nel body 200 |
|
|
494
|
+
| `list_webhook_deliveries(webhook_id, limit=50)` | `GET /webhooks/{id}/deliveries` | `keys:manage` | storico deliveries, dal più recente |
|
|
495
|
+
| `replay_webhook_delivery(delivery_id)` | `POST /webhooks/deliveries/{id}/replay` | `keys:manage` | re-invio immediato con firma fresca; anche su righe `delivered` |
|
|
496
|
+
| `flows_palette()` | `GET /flows/palette` | `flows:read` | node type costruibili + campi di config |
|
|
497
|
+
| `flow_templates()` | `GET /flows/templates` | `flows:read` | galleria di template |
|
|
498
|
+
| `list_flows(page=1, page_size=20, status=None)` | `GET /flows` | `flows:read` | flow del project, paginati |
|
|
499
|
+
| `create_flow(flow)` | `POST /flows` | `flows:manage` | stesso contratto `FlowCreate` del builder prodotto (o `template_id`) |
|
|
500
|
+
| `get_flow(flow_id)` | `GET /flows/{id}` | `flows:read` | definizione completa |
|
|
501
|
+
| `flow_stats(flow_id)` | `GET /flows/{id}/stats` | `flows:read` | contatori/esiti dei run |
|
|
502
|
+
| `update_flow(flow_id, patch)` | `PATCH /flows/{id}` | `flows:manage` | update parziale, incl. `is_active` |
|
|
503
|
+
| `delete_flow(flow_id)` | `DELETE /flows/{id}` | `flows:manage` | rimozione |
|
|
504
|
+
| `run_flow(flow_id, dry_run=False, trigger_data=None)` | `POST /flows/{id}/run` | `flows:manage` | run manuale; gli step approval parcheggiano il run |
|
|
505
|
+
| `run_flow_with_upload(flow_id, path_or_bytes, *, filename=None, content_type=None, dry_run=False)` | `POST /flows/{id}/run-upload` | `flows:manage` | run manuale seminato da un PDF caricato (`content_item_id` nel `trigger_data`); solo PDF |
|
|
506
|
+
| `get_flow_run(run_id)` | `GET /flows/runs/{id}` | `flows:read` | dettaglio completo di un run: stato, trigger, `context`, `node_runs`, campi HITL |
|
|
507
|
+
| `generate_flow(prompt)` | `POST /flows/generate` | `flows:manage` | copilot NL → draft flow (costo LLM, metered) |
|
|
508
|
+
| `flows_pending_approvals()` | `GET /flows/pending-approvals` | `flows:read` | inbox HITL dei run in `pending_approval` |
|
|
509
|
+
| `approve_flow_run(run_id, *, edited_value=None, edited_fields=None)` | `POST /flows/runs/{id}/approve` | `flows:manage` | riprende un run in pausa; `edited_value` = una stringa, `edited_fields` = correzione strutturata per-campo (vince sulle chiavi sovrapposte) |
|
|
510
|
+
| `reject_flow_run(run_id, *, reason=None)` | `POST /flows/runs/{id}/reject` | `flows:manage` | "no" terminale: il run non riprende mai |
|
|
511
|
+
| `usage(from_=None, to=None, group_by="day")` | `GET /usage` | `usage:read` | chiamate/token/costo aggregati; `group_by` `day`/`purpose` |
|
|
512
|
+
| `create_key(name, scopes, live=True, expires_at=None)` | `POST /keys` | `keys:manage` | la chiave completa appare SOLO in questa risposta |
|
|
513
|
+
| `list_keys()` | `GET /keys` | `keys:manage` | mai hash o chiavi complete |
|
|
514
|
+
| `revoke_key(id)` | `POST /keys/{id}/revoke` | `keys:manage` | revoca immediata |
|
|
515
|
+
|
|
516
|
+
Tutti i metodi ritornano il body JSON della risposta come `dict`, tranne
|
|
517
|
+
`download_generation` / `get_cad_result` che ritornano un `DownloadedFile`
|
|
518
|
+
(`content` bytes, `content_type`, `filename`, più `.json()` e `.save(path)`).
|
|
519
|
+
|
|
520
|
+
Costanti esportate: `DEFAULT_K` (10), `MAX_K` (50), `FILTER_SOURCES`,
|
|
521
|
+
`GENERATE_FORMATS`, `WEBHOOK_EVENTS`, `ACTION_RISK_LEVELS` (tutte
|
|
522
|
+
informative: i valori enum sono additivi, il SDK non li valida),
|
|
523
|
+
`AGENTIC_TIMEOUT` (120s), `DEFAULT_BATCH_SIZE` (200), `MAX_BATCH_SIZE`
|
|
524
|
+
(500), `DEFAULT_BASE_URL`. Tipi esportati: `ActionBinding` (il binding HTTP
|
|
525
|
+
di una capability — un normale `dict` con quelle chiavi va bene ovunque).
|
|
526
|
+
|
|
527
|
+
## CLI (`ando`)
|
|
528
|
+
|
|
529
|
+
Il pacchetto espone lo script `ando` (o `python -m ando_ai`):
|
|
530
|
+
|
|
531
|
+
```bash
|
|
532
|
+
ando kinds # kind disponibili + contratto Record
|
|
533
|
+
ando validate records.jsonl # dry-run dei record (nessuna scrittura)
|
|
534
|
+
cat records.jsonl | ando validate - # …da stdin (JSON array o JSONL)
|
|
535
|
+
ando sync acme-crm records.jsonl --mode full # push di un batch
|
|
536
|
+
ando sources # cosa può interrogare questa chiave
|
|
537
|
+
ando me # identità della chiave
|
|
538
|
+
ando mcp --client cursor # config MCP per il tuo agente
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Auth: `--api-key` o `$ANDO_API_KEY`; base URL: `--base-url` o `$ANDO_BASE_URL`.
|
|
542
|
+
`ando validate` consuma `POST /connectors/validate` e **esce con codice 1** se un
|
|
543
|
+
record è rifiutato — la CI di un connettore può fare da gate sul contratto.
|
|
544
|
+
`ando mcp` stampa l'install one-command del server MCP hosted (Claude Code /
|
|
545
|
+
Cursor / Claude Desktop); senza `--client` le stampa tutte.
|
|
546
|
+
|
|
547
|
+
## Errori
|
|
548
|
+
|
|
549
|
+
Gerarchia (base comune `AndoPlatformError`):
|
|
550
|
+
|
|
551
|
+
- **`ApiError`** — errore dell'envelope `{"error": {"code", "message"}}`.
|
|
552
|
+
Attributi: `code` (stabile: `invalid_api_key`, `expired_api_key`,
|
|
553
|
+
`revoked_api_key`, `insufficient_scope`, `rate_limited`,
|
|
554
|
+
`invalid_request`, `not_found`, `internal_error`), `message`, `status`
|
|
555
|
+
e `retry_after` (secondi dall'header `Retry-After`, valorizzato sui 429).
|
|
556
|
+
- **`NetworkError`** — problema di trasporto (DNS, connessione, TLS,
|
|
557
|
+
timeout); l'eccezione `httpx` originale è in `__cause__`. Su
|
|
558
|
+
`agentic_answer` un read timeout arriva qui: di solito significa
|
|
559
|
+
«alza `timeout=`», non «il server è rotto».
|
|
560
|
+
- **`JobFailed`** — job di ingestion (o CAD) terminato `failed`; porta
|
|
561
|
+
`error_code`, `error_message` e il payload completo in `job`.
|
|
562
|
+
- **`JobTimeout`** — `wait_for_job` / `wait_for_cad_job` ha esaurito il
|
|
563
|
+
`timeout` senza uno stato terminale.
|
|
564
|
+
- **`GenerationFailed`** — generazione terminata `failed`; porta `error_code`,
|
|
565
|
+
`error_message` e il payload in `generation`.
|
|
566
|
+
- **`GenerationTimeout`** — `wait_for_generation` ha esaurito il `timeout`.
|
|
567
|
+
|
|
568
|
+
```python
|
|
569
|
+
from ando_ai import AndoPlatform, ApiError
|
|
570
|
+
|
|
571
|
+
with AndoPlatform("ando_sk_live_...") as ando:
|
|
572
|
+
try:
|
|
573
|
+
ando.query("scadenze")
|
|
574
|
+
except ApiError as exc:
|
|
575
|
+
if exc.code == "rate_limited":
|
|
576
|
+
print(f"Riprova tra {exc.retry_after}s")
|
|
577
|
+
elif exc.code == "insufficient_scope":
|
|
578
|
+
print(f"Scope mancante: {exc.message}")
|
|
579
|
+
else:
|
|
580
|
+
raise
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
## Test
|
|
584
|
+
|
|
585
|
+
I test del SDK girano senza rete né backend (`httpx.MockTransport`):
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
uv run pytest tests/unit/test_platform_sdk_client.py
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
## License
|
|
592
|
+
|
|
593
|
+
Proprietary — Copyright (c) 2026 Ando S.r.l. This SDK is licensed for use
|
|
594
|
+
solely with the Ando Platform services; it is **not** open source. See the
|
|
595
|
+
`LICENSE` file shipped in this package for the full terms.
|