A2A — Agent-to-Agent
Il backend OpenData AI è un server A2A: pubblica una AgentCard e accetta chiamate JSON-RPC da qualunque agente che parla il protocollo Agent-to-Agent. È il modo giusto per delegare task “cerca questi dataset” o “classifica questo dataset” da un orchestratore esterno.
Discovery — AgentCard
La AgentCard è esposta su /.well-known/agent-card.json (SDK 1.0) e su /.well-known/agent.json come alias legacy 0.3. Contiene metadata, skill esposte e supported versions.
curl -s https://api.opendata-ai.it/.well-known/agent-card.json | jq
{
"name": "opendata-ai",
"version": "1.0.0",
"skills": [
{ "id": "search_open_data", "name": "Cerca open data multi-fonte" },
{ "id": "find_geo_resources", "name": "Cerca risorse geografiche" },
{ "id": "classify_dataset", "name": "Classifica un dataset" }
],
"endpoints": { "jsonrpc": "https://api.opendata-ai.it/a2a/" }
}Skill esposte
| Skill | Cosa fa | Input principale |
|---|---|---|
search_open_data | Fan-out su CKAN + SDMX, sintesi narrativa + lista risorse. | Una query in linguaggio naturale. |
find_geo_resources | Stessa cosa ma bias geografico (Shapefile, GeoJSON, KML, WMS). | Una query in linguaggio naturale. |
classify_dataset | Classifica un dataset rispetto a una tassonomia data. Cache Redis 24h + Postgres durable. | source, dataset_id, taxonomy[]. |
SDK 1.0 — SendMessage (raccomandato)
PascalCase, role come enum protobuf:
curl -sX POST https://api.opendata-ai.it/a2a/ \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <clerk_jwt>' \
-H 'A2A-Version: 1.0' \
-d '{
"jsonrpc":"2.0","id":"1","method":"SendMessage",
"params":{"message":{
"messageId":"m-1",
"role":"ROLE_USER",
"parts":[{"text":"qualità aria a Milano"}],
"metadata":{"skill":"search_open_data"}
}}
}'SDK 0.3 (compat) — message/send
slash-case, role lowercase, kind: text esplicito nei parts:
curl -sX POST https://api.opendata-ai.it/a2a/ \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <clerk_jwt>' \
-H 'A2A-Version: 0.3' \
-d '{
"jsonrpc":"2.0","id":"1","method":"message/send",
"params":{"message":{
"messageId":"m-2",
"role":"user",
"parts":[{"kind":"text","text":"qualità aria a Milano"}],
"metadata":{"skill":"search_open_data"}
}}
}'Python — A2A SDK ufficiale
pip install a2a-sdk
# client.py
import os
from a2a.client import A2AClient
client = A2AClient(
"https://api.opendata-ai.it/a2a/",
headers={"Authorization": f"Bearer {os.environ['OPENDATA_JWT']}"},
)
reply = client.send_message(
text="popolazione di Milano per età, ultimi 5 anni",
metadata={"skill": "search_open_data"},
)
# Il primo artifact è la sintesi narrativa
print(reply.artifacts[0].parts[0].text)
# Il secondo è il payload strutturato {text, resources}
import json
data = json.loads(reply.artifacts[1].parts[0].data)
for r in data["resources"]:
print("-", r["title"], "→", r["url"])Vincoli da rispettare
messageIdobbligatorio — il SDK valida con pydantic strict.- Header
A2A-Versiondeve coincidere col metodo (1.0↔SendMessage,0.3↔message/send). In alternativa puoi omettere l'header: il server lo deduce dal nome del metodo. - Skill si seleziona via
message.metadata.skill(defaultsearch_open_data). - Auth: in produzione gli endpoint A2A sono dietro JWT Clerk. In dev locale con
AUTH_ENABLED=falsel'header diventa opzionale.
Shape della risposta
result.task.status.state = completed / TASK_STATE_COMPLETED, più una lista di artifacts:
- artifact[0] — la sintesi narrativa testuale.
- artifact[1] — un JSON strutturato
{text, resources}con la stessa shape diPOST /datasets/search.
Quando usare A2A vs MCP
- MCP espone tool a un singolo LLM. Sceglilo se vuoi costruire il tuo agente che chiama i nostri server CKAN / ISTAT / OSM uno per volta.
- A2A espone l'intero agente OpenData AI a un altro agente. Sceglilo se vuoi delegare l'intera domanda “cerca tra gli open data” al nostro orchestratore, ricevendo la sintesi pre-fatta.