Skip to main content

Ando Platform — Python SDK

Client Python sincrono per la Ando Platform API v1 (https://api.ando-ai.com/platform/v1). Unica dipendenza: httpx>=0.24.

Endpoint predefinito: https://api.ando-ai.com/platform/v1.

Requisiti di accesso

Questo pacchetto contiene solo il client SDK: non include né rende autonomamente disponibile il servizio Ando Platform. Per usarlo servono un account Ando attivo (o un accesso di valutazione), i relativi diritti di licenza e una API key emessa da Ando. Per richiedere l'attivazione, contatta Ando su ando-ai.com.

Installazione

Requisito: Python 3.9+.

pip install ando-ai

Uso

from ando_ai import AndoPlatform, ApiError, JobFailed

with AndoPlatform("ando_sk_live_...") as ando:
    accepted = ando.upload_document("contratto.pdf")
    ando.wait_for_job(accepted["job_id"])

    result = ando.answer("Qual è la durata del contratto?")
    print(result["answer"], result["citations"])

    ando.delete_document(accepted["document_id"])

Il client è un context manager; in alternativa chiama ando.close() a fine lavoro.

Cosa puoi costruire — i tre livelli

Lo stesso stack, esposto a tre profondità crescenti. Non sono alternative: sono quanto lavoro fa il server al posto tuo.

1. query() — recuperi i chunk, generi tu

Per chi ha già il proprio LLM/prompt e vuole solo il retrieval (con citazioni e score).

hits = ando.query("penali per ritardata consegna", k=8)
for hit in hits["results"]:
    print(hit["score"], hit["section"], hit["text"][:120])
    # hit["document_id"] / hit["chunk_id"] = la citazione da mostrare

2. answer() — risposta sintetizzata e ancorata

Il server recupera e sintetizza, citando i chunk usati. La risposta rispecchia la lingua della domanda (non si forza nulla). Se il retrieval non trova nulla: answer è None e no_results è True.

result = ando.answer("Qual è la durata del contratto?", verify=True)
print(result["answer"])
print(result["citations"])            # [{document_id, chunk_id}, ...]
print(result["verification"])         # {verified, score, notes} | None

3. agentic_answer() — l'agente completo, anche sui dati strutturati (PREVIEW)

Questa è la differenza tra «ti do dei chunk» e «ho interrogato il tuo gestionale». Gira la stessa macchina della chat prodotto: escalation TRAMA T1→T2→T3 e, sulle sources strutturate pubblicate del project, text-to-SQL — quindi una domanda la cui risposta sta in una tabella viene risposta dalla tabella, non approssimata dai documenti.

Read-only per costruzione: nessuna scrittura, nessuna azione, nessuna persistenza. steps[] mostra cosa ha fatto l'agente (tier di retrieval, SQL eseguito con credenziali redatte, specialist scelto).

result = ando.agentic_answer(
    "Quali clienti hanno esposizione più critica e per quale importo?",
    k=25,
    with_diagnostics=True,       # tier, timing, plan cache
)
print(result["answer"])
print(result["sources_used"])    # es. ["database", "fs"]
for step in result["steps"]:
    print(step["type"], step)    # tipi sconosciuti = additivi, non ramificarci sopra

Una chiamata agentica è multi-step: il timeout di default è AGENTIC_TIMEOUT (120s), non i 30s del client. Si sovrascrive per chiamata con timeout=.

PREVIEW: POST /agentic/answer non è ancora GA — request e response possono cambiare. Vedi docs/PLATFORM_API_CONTRACT.md.

Conversazioni — memoria tra turni agentici

Un turno agentico è stateless di default. Apri un thread con create_conversation() e passa il suo id a agentic_answer(): i follow-up si risolvono contro la storia del thread («e il secondo?» funziona) e ogni scambio viene persistito per il turno successivo. Scope query:read.

conv = ando.create_conversation(title="Analisi esposizione")
ando.agentic_answer("Chi sono i primi 3 clienti per esposizione?",
                    conversation_id=conv["conversation_id"])
ando.agentic_answer("E il secondo?",                       # follow-up risolto
                    conversation_id=conv["conversation_id"])

ando.list_conversations()                        # thread della chiave, cursor-paginati
ando.get_conversation_turns(conv["conversation_id"])   # scambi, dal più vecchio
ando.delete_conversation(conv["conversation_id"])      # irreversibile

Restringere il campo: filters, sources, partition

Valgono su query() e answer(). Ogni filtro può solo restringere: la resource policy della chiave è sempre intersecata sopra e vince.

# scorciatoie da keyword (le due dimensioni più usate)
ando.answer("fatture scadute", sources=["fs"], partition="cliente-42")

# oggetto completo, con condizioni sui metadata di ingest
ando.query(
    "resi 2025",
    filters={
        "sources": ["fs", "drive"],
        "partition": "cliente-42",
        "metadata": {
            "op": "and",
            "conditions": [
                {"key": "doc_type", "op": "eq", "value": "invoice"},
                {"key": "year", "op": "gte", "value": 2025},
            ],
        },
    },
)

partition e metadata sono esattamente quelli impostati all'ingest — il cerchio si chiude sullo stesso client:

ando.upload_document(
    "fattura-2025-11.pdf",
    metadata={"doc_type": "invoice", "year": 2025},   # oggetto PIATTO, max 32 chiavi
    partition="cliente-42",                            # A-Za-z0-9._:- , max 128 char
)

Passare la stessa dimensione due volte (keyword e dentro filters) è un ValueError lato client, prima di qualunque HTTP: sceglierne una in silenzio cambierebbe quali documenti vengono cercati. I nomi di source sconosciuti non vengono rifiutati dal SDK: i valori enum sono additivi per contratto, decide il server.

Generazione documenti — generate()

Dai tuoi dati a un file docx/pptx/xlsx/pdf/md. Il contesto arriva da una query (retrieval TRAMA, resource policy rispettata) e/o da document_ids espliciti; almeno uno dei due è obbligatorio (altrimenti ValueError prima di qualunque HTTP). È sempre asincrono: 202 con un generation_id da pollare, poi si scarica il file. Il contenuto rispecchia la lingua di instructions/query. Scope unico per tutto: generate:write.

accepted = ando.generate(
    "docx",
    "Prepara un report sul fatturato 2025 con sintesi e dettaglio.",
    query="fatturato 2025",
)
gen = ando.wait_for_generation(accepted["generation_id"])   # -> completed | GenerationFailed
artifact = ando.download_generation(gen["generation_id"])   # DownloadedFile
artifact.save("report.docx")                                # bytes su disco

wait_for_generation condivide identica policy di retry/backoff di wait_for_job (transient rate_limited/5xx/rete ritentati entro la deadline); solleva GenerationFailed (con error_code) su failed, GenerationTimeout allo scadere.

CAD — analisi, DFM, conversione

Upload STEP/DXF asincroni (202 con job_id + result_id). Analisi e DFM tornano un report JSON, la conversione un file STEP. Scope cad:read per analyze/dfm-check/similar/jobs/results, cad:write per convert. Un job CAD è un job: wait_for_cad_job riusa JobFailed/JobTimeout.

job = ando.cad_analyze("bracket.step")                      # {result_id, job_id, kind: "analysis"}
ando.wait_for_cad_job(job["job_id"])
report = ando.get_cad_result(job["result_id"]).json()       # report JSON

conv = ando.cad_convert("profilo.dxf", "extrude", params={"height_mm": 10})
ando.wait_for_cad_job(conv["job_id"])
ando.get_cad_result(conv["result_id"]).save("solido.step")  # STEP su disco

cad_find_similar() è la ricerca per somiglianza query-by-upload (live): carichi uno STEP/IGES/STL, un disegno PDF o un'immagine; viene embeddato al volo e confrontato con il corpus CAD ingerito del project. Il file di query non viene mai salvato. La chiamata è sincrona — uno STEP grande può richiedere ~10-30s di tessellazione, dimensiona il timeout di conseguenza.

simili = ando.cad_find_similar("staffa.step", top_k=5)
for hit in simili["results"]:
    print(hit["filename"], hit["similarity"], hit["description"])

Azioni — il write-loop

La metà write simmetrica della Push API: chiedi all'app del project di fare qualcosa (aprire un ticket, spostare un ordine) e ne verifichi l'esito. declare (una tantum, sources:manage) → propose → execute (actions:write) → l'app fa complete. Le letture usano actions:read.

actions = ando.actions()

# dichiarazione una tantum (chiave sources:manage)
actions.declare(
    "acme-desk",
    "create_ticket",
    params_schema={
        "type": "object",
        "properties": {"subject": {"type": "string"}},
        "required": ["subject"],
    },
    risk="medium",
)

# per richiesta (chiave actions:write) — propose + execute in una chiamata
dispatched = actions.run(
    "acme-desk",
    "create_ticket",
    {"subject": "Refund request"},
    idempotency_key="refund-4711",   # un run ritentato replay-a la proposta
)
print(dispatched["id"], dispatched["status"])   # ... "dispatched"

Le azioni high/critical richiedono confirm_risk=True su run/execute. propose_action accetta un idempotency_key opzionale (header Idempotency-Key): una propose ritentata con la stessa chiave replay-a il primo 201 invece di duplicare. Ogni metodo esiste anche come funzione di modulo (propose_action(client, ...)) e come delega sul client (ando.propose_action(...)).

Capability con binding HTTP — dichiara, poi invoke

Dichiara la capability con un binding HTTP e Ando può chiamare direttamente il tuo sistema: invoke_connector_action è il fratello sincrono di propose + execute — un round trip, esito nella risposta.

# dichiarazione CON binding (chiave sources:manage)
actions.declare(
    "acme-desk",
    "create_ticket",
    params_schema={
        "type": "object",
        "properties": {"subject": {"type": "string"}},
        "required": ["subject"],
    },
    risk="medium",
    binding={
        "method": "POST",
        "path": "/tickets",                        # relativo, sul TUO sistema
        "body_template": {"title": "{{subject}}"}, # placeholder {{param}}
        "response": {"id_field": "id"},            # dove vive l'id creato
        "timeout_seconds": 15,
    },
)

# invoke sincrono (chiave actions:write)
esito = ando.invoke_connector_action(
    "acme-desk", "create_ticket", {"subject": "Refund request"}
)
print(esito["ok"], esito["status_code"], esito["response"])

I params sono validati contro il params_schema dichiarato e vale lo stesso risk gate di execute: una capability high/critical viene rifiutata (invalid_request) senza confirm_risk=True. Le chiavi di test validano e gate-ano per davvero ma non chiamano mai l'endpoint bound — la risposta porta test: true. La dichiarazione è una sostituzione completa: ri-dichiarare senza binding cancella quello memorizzato.

Flows — orchestrazione come API

Costruisci, esegui e supervisiona gli stessi flow del builder prodotto, white-label. Scope: flows:read (list/palette/stats/inbox/run detail) e flows:manage (create/update/delete/run/approve/reject).

palette = ando.flows_palette()          # ogni node type + campi di config
flows = ando.list_flows(page=1, page_size=20)
draft = ando.generate_flow(
    "Quando arriva una fattura, valida i dati e chiedi approvazione"
)  # copilot: NL -> draft flow (costo LLM, metered)

created = ando.create_flow(draft["flow"])
ando.run_flow(created["flow_id"], dry_run=True)
ando.flow_stats(created["flow_id"])
ando.update_flow(created["flow_id"], {"is_active": False})

# run manuale seminato con un PDF caricato: il suo content_item_id finisce
# nel trigger_data del run per i nodi che bindano
# {"$ref": "trigger.content_item_id"} (es. ai.cad_analyze)
run = ando.run_flow_with_upload(created["flow_id"], "fattura.pdf")

# polling di un run: stato, trigger, context accumulato, output per nodo
detail = ando.get_flow_run(run["run_id"])
print(detail["status"], detail["node_runs"])

# HITL: gli step `logic.approval` parcheggiano il run in `pending_approval`
inbox = ando.flows_pending_approvals()
ando.approve_flow_run("run_...", edited_value="testo corretto")
ando.approve_flow_run(
    "run_...",
    # correzione strutturata per-campo dello STESSO output editabile — se
    # arrivano entrambi, edited_fields vince sulle chiavi sovrapposte
    edited_fields={"to": "acme@example.com", "subject": "Ordine 42"},
)
ando.reject_flow_run("run_...", reason="importo errato")

run_flow_with_upload accetta solo PDF (invalid_request su tutto il resto). flow_templates() elenca la galleria di template; delete_flow(flow_id) rimuove un flow.

Usage — consumi e costi del project

usage() (usage:read) ritorna chiamate/token/costo aggregati del project — mese corrente di default, group_by="day" per la serie di fatturazione o "purpose" per capire cosa genera costo:

report = ando.usage(from_="2026-08-01", to="2026-08-07")
print(report["total_cost_usd"], report["usage"])

Sources — cosa può interrogare questa chiave

Prima di eseguire una /query (come fa la UI prodotto lasciando scegliere un connettore) bisogna scoprire cosa la chiave può interrogare. list_sources() lo dice, già intersecato con la resource policy della chiave.

for s in ando.list_sources()["sources"]:
    print(s["id"], s["kind"], s["queryable"], s.get("reason"))
    # id unstrutturati (fs/drive/…) -> filters.sources di /query
    # id strutturati (conn_<uuid>)  -> sources di /agentic/answer

Registra un database read-only del cliente (es. il back office SQL di un gestionale) come source strutturata — le credenziali sono cifrate at rest e mai ritornate; la risposta è white-label (capability, mai il vendor):

import time

source = ando.register_database_source(
    name="Gestionale produzione",
    connector_type="mysql",   # postgresql | mysql | sqlserver | oracle | ...
    credentials={"host": "db.example.com", "user": "ando_ro",
                 "password": "…", "database": "mexal"},
    config={"schema_filter": ["mexal"], "max_rows_per_query": 500},
)
ando.test_source(source["source_id"])   # {ok, error?} — probe, non un ApiError
ando.discover_source_schema(source["source_id"])
while True:
    profiling = ando.get_source_profiling_status(source["source_id"])
    if profiling["status"] == "complete":
        break
    time.sleep(2)

Per una source strutturata lo schema profilato si cura e poi si pubblica (il gate di queryabilità): finché non è pubblicato, queryable=false, reason="schema non pubblicato". Curare = sources:manage, leggere lo schema = query:read.

ando.get_source_schema("conn_…")                 # stato + objects (K-Schema)
ando.curate_source_schema(                        # override della proposta AI
    "conn_…", "public.orders",
    description="Ordini di vendita",
    column_descriptions={"total": "Importo totale, IVA inclusa"},
)
ando.publish_source_schema("conn_…")              # apre /query + /agentic

Webhooks

Endpoint HTTPS in uscita, firmati Standard Webhooks. Scope keys:manage. Il secret (whsec_…) è mostrato una sola volta alla creazione.

hook = ando.create_webhook("https://example.com/hooks", ["ingestion.completed"])
print(hook["secret"])        # salvalo ora — non torna più
ando.list_webhooks()         # mai il secret; cursor/limit opzionali
ando.test_webhook(hook["webhook_id"])            # test.ping sincrono

# storico deliveries + replay manuale (firma fresca, tentativo immediato)
deliveries = ando.list_webhook_deliveries(hook["webhook_id"], limit=100)
ando.replay_webhook_delivery(deliveries["deliveries"][0]["delivery_id"])

ando.delete_webhook(hook["webhook_id"])

Riferimento

Costruttore:

AndoPlatform(
    api_key,                                        # ando_sk_live_... / ando_sk_test_...
    base_url="https://api.ando-ai.com/platform/v1",
    timeout=30,                                     # secondi, per richiesta
)
Metodo Endpoint Scope Note
health() GET /health — liveness pubblica
me() GET /me chiave valida identità del project
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)
get_document(id) GET /documents/{id} query:read stato + metadati
delete_document(id) DELETE /documents/{id} ingest:write rimozione indice + storage
get_job(id) GET /jobs/{id} query:read queued → processing → completed | failed
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
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
answer(text, k=10, ...) POST /query query:read scorciatoia per mode="answer", stessi parametri di narrowing
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}
create_conversation(title=None) POST /conversations query:read apre un thread di memoria per i turni agentici
list_conversations(cursor=None, limit=None) GET /conversations query:read thread della chiave, cursor-paginati
get_conversation_turns(id, cursor=None, limit=None) GET /conversations/{id} query:read scambi del thread, dal più vecchio
delete_conversation(id) DELETE /conversations/{id} query:read irreversibile
connectors() GET /connectors sources:manage elenco connector del project
connector(slug, **options) — — costruisce un AndoConnector legato a questo client (Push API)
connector_kinds() GET /connectors/kinds chiave valida kind disponibili + contratto Record
validate_records(records, connector=None) POST /connectors/validate ingest:write dry-run dei record, nessuna scrittura
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
actions() — — costruisce un AndoActions legato a questo client (run(...) = propose + execute)
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
connector_actions(connector) GET /connectors/{slug}/actions actions:read azioni dichiarate del connector
delete_connector_action(connector, name) DELETE /connectors/{slug}/actions/{name} sources:manage rimuove una dichiarazione
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
propose_action(connector, action, params=None, idempotency_key=None) POST /actions actions:write valida + preview, nessun side effect; Idempotency-Key opzionale
execute_action(action_id, confirm_risk=False) POST /actions/{id}/execute actions:write dispatch del webhook action.requested; high/critical richiedono confirm_risk
complete_action(action_id, status, result=None, error=None) POST /actions/{id}/complete actions:write l'app riporta l'esito (completed/failed)
get_action(action_id) GET /actions/{id} actions:read polling di una richiesta
list_actions(connector=None, limit=None, cursor=None) GET /actions actions:read richieste del project, cursor-paginate
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)
get_generation(id) GET /generate/{id} generate:write stato; download_url a completed
wait_for_generation(id, timeout=180, poll=2.0) polling su GET /generate/{id} generate:write ritorna il payload a completed; GenerationFailed/GenerationTimeout
download_generation(id) GET /generate/{id}/download generate:write DownloadedFile (bytes + content type + filename)
cad_analyze(path_or_bytes, ...) POST /cad/analyze cad:read STEP/DXF; job async kind="analysis"
cad_dfm_check(path_or_bytes, ruleset_name=None, ...) POST /cad/dfm-check cad:read solo STEP; ruleset_name inviato solo se valorizzato
cad_convert(path_or_bytes, operation, params=None, target="step", ...) POST /cad/convert cad:write DXF→STEP; operation extrude/revolve; params validati server-side
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
get_cad_job(id) GET /cad/jobs/{id} cad:read stato job CAD
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)
get_cad_result(id) GET /cad/results/{id} cad:read DownloadedFile; .json() per analisi/DFM, .save() per STEP
list_sources() GET /sources query:read cosa la chiave può interrogare (già intersecato con la policy)
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
test_source(source_id) POST /sources/{id}/test sources:manage probe di connessione; un probe fallito è {ok: false} nel body 200, non un ApiError
discover_source_schema(source_id) POST /sources/{id}/discover sources:manage avvia discovery e profiling asincroni
get_source_profiling_status(source_id) GET /sources/{id}/profiling-status sources:manage stato e contatori del profiling
get_source_schema(source_id) GET /sources/{id}/schema query:read K-Schema profilato di una source strutturata
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)
publish_source_schema(source_id, published=True) POST /sources/{id}/schema/publish sources:manage gate di queryabilità; published=False per revertire
create_webhook(url, events) POST /webhooks keys:manage il secret appare SOLO qui (201)
list_webhooks(cursor=None, limit=None) GET /webhooks keys:manage mai il secret; cursor-paginato con cursor/limit
delete_webhook(id) DELETE /webhooks/{id} keys:manage rimozione; deliveries pending scartate
test_webhook(id) POST /webhooks/{id}/test keys:manage test.ping sincrono; esito nel body 200
list_webhook_deliveries(webhook_id, limit=50) GET /webhooks/{id}/deliveries keys:manage storico deliveries, dal più recente
replay_webhook_delivery(delivery_id) POST /webhooks/deliveries/{id}/replay keys:manage re-invio immediato con firma fresca; anche su righe delivered
flows_palette() GET /flows/palette flows:read node type costruibili + campi di config
flow_templates() GET /flows/templates flows:read galleria di template
list_flows(page=1, page_size=20, status=None) GET /flows flows:read flow del project, paginati
create_flow(flow) POST /flows flows:manage stesso contratto FlowCreate del builder prodotto (o template_id)
get_flow(flow_id) GET /flows/{id} flows:read definizione completa
flow_stats(flow_id) GET /flows/{id}/stats flows:read contatori/esiti dei run
update_flow(flow_id, patch) PATCH /flows/{id} flows:manage update parziale, incl. is_active
delete_flow(flow_id) DELETE /flows/{id} flows:manage rimozione
run_flow(flow_id, dry_run=False, trigger_data=None) POST /flows/{id}/run flows:manage run manuale; gli step approval parcheggiano il run
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
get_flow_run(run_id) GET /flows/runs/{id} flows:read dettaglio completo di un run: stato, trigger, context, node_runs, campi HITL
generate_flow(prompt) POST /flows/generate flows:manage copilot NL → draft flow (costo LLM, metered)
flows_pending_approvals() GET /flows/pending-approvals flows:read inbox HITL dei run in pending_approval
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)
reject_flow_run(run_id, *, reason=None) POST /flows/runs/{id}/reject flows:manage "no" terminale: il run non riprende mai
usage(from_=None, to=None, group_by="day") GET /usage usage:read chiamate/token/costo aggregati; group_by day/purpose
create_key(name, scopes, live=True, expires_at=None) POST /keys keys:manage la chiave completa appare SOLO in questa risposta
list_keys() GET /keys keys:manage mai hash o chiavi complete
revoke_key(id) POST /keys/{id}/revoke keys:manage revoca immediata

Tutti i metodi ritornano il body JSON della risposta come dict, tranne download_generation / get_cad_result che ritornano un DownloadedFile (content bytes, content_type, filename, più .json() e .save(path)).

Costanti esportate: DEFAULT_K (10), MAX_K (50), FILTER_SOURCES, GENERATE_FORMATS, WEBHOOK_EVENTS, ACTION_RISK_LEVELS (tutte informative: i valori enum sono additivi, il SDK non li valida), AGENTIC_TIMEOUT (120s), DEFAULT_BATCH_SIZE (200), MAX_BATCH_SIZE (500), DEFAULT_BASE_URL. Tipi esportati: ActionBinding (il binding HTTP di una capability — un normale dict con quelle chiavi va bene ovunque).

CLI (ando)

Il pacchetto espone lo script ando (o python -m ando_ai):

ando kinds                                   # kind disponibili + contratto Record
ando validate records.jsonl                  # dry-run dei record (nessuna scrittura)
cat records.jsonl | ando validate -          # …da stdin (JSON array o JSONL)
ando sync acme-crm records.jsonl --mode full # push di un batch
ando sources                                 # cosa può interrogare questa chiave
ando me                                      # identità della chiave
ando mcp --client cursor                     # config MCP per il tuo agente

Auth: --api-key o $ANDO_API_KEY; base URL: --base-url o $ANDO_BASE_URL. ando validate consuma POST /connectors/validate e esce con codice 1 se un record è rifiutato — la CI di un connettore può fare da gate sul contratto. ando mcp stampa l'install one-command del server MCP hosted (Claude Code / Cursor / Claude Desktop); senza --client le stampa tutte.

Errori

Gerarchia (base comune AndoPlatformError):

  • ApiError — errore dell'envelope {"error": {"code", "message"}}. Attributi: code (stabile: invalid_api_key, expired_api_key, revoked_api_key, insufficient_scope, rate_limited, invalid_request, not_found, internal_error), message, status e retry_after (secondi dall'header Retry-After, valorizzato sui 429).
  • NetworkError — problema di trasporto (DNS, connessione, TLS, timeout); l'eccezione httpx originale è in __cause__. Su agentic_answer un read timeout arriva qui: di solito significa «alza timeout=», non «il server è rotto».
  • JobFailed — job di ingestion (o CAD) terminato failed; porta error_code, error_message e il payload completo in job.
  • JobTimeout — wait_for_job / wait_for_cad_job ha esaurito il timeout senza uno stato terminale.
  • GenerationFailed — generazione terminata failed; porta error_code, error_message e il payload in generation.
  • GenerationTimeout — wait_for_generation ha esaurito il timeout.
from ando_ai import AndoPlatform, ApiError

with AndoPlatform("ando_sk_live_...") as ando:
    try:
        ando.query("scadenze")
    except ApiError as exc:
        if exc.code == "rate_limited":
            print(f"Riprova tra {exc.retry_after}s")
        elif exc.code == "insufficient_scope":
            print(f"Scope mancante: {exc.message}")
        else:
            raise

Test

I test del SDK girano senza rete né backend (httpx.MockTransport):

uv run pytest tests/unit/test_platform_sdk_client.py

License

Proprietary — Copyright (c) 2026 Ando S.r.l. This SDK is licensed for use solely with the Ando Platform services; it is not open source. See the LICENSE file shipped in this package for the full terms.

Metadata

Release files for ando-ai 0.4.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ando-ai 0.4.1
File Size Uploaded Provenance Actions
ando_ai-0.4.1.tar.gz 70.2 kB With provenance Details for ando_ai-0.4.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for ando-ai 0.4.1
File Interpreter ABI Platform Actions
ando_ai-0.4.1-py3-none-any.whl Python 3 none any Details for ando_ai-0.4.1-py3-none-any.whl

Total release size: 123.2 kB

Release files / ando_ai-0.4.1.tar.gz

Download URL ando_ai-0.4.1.tar.gz
Size 70.2 kB
Tags Source
SHA-256 checksum
How to use checksums
19c044d86a8baa6bc7079f4666a126b4ec53f8a22ff4d5bd31575135eb03f188
BLAKE2b-256 checksum
How to use checksums
875839217074cab946eb7075dd236e91f33af4c160217064f32b7bd8c67efc32
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Release files / ando_ai-0.4.1-py3-none-any.whl

Download URL ando_ai-0.4.1-py3-none-any.whl
Size 53.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
324a189f5f9244cd8028abcd140269061b8dda893f71b3978e2bc7e5e8dd6d30
BLAKE2b-256 checksum
How to use checksums
e09d006651113dc541e8ef4a46dfe7ba33cdc60931be9adf476fae48bc392f41
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Security for ando-ai 0.4.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for ando_ai-0.4.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page