Programadores · A2A
De agente para agente (A2A)
O A2A (Agent2Agent) permite que um agente de IA chame outro agente com pedidos tipados. O A2A de Dados Foral dá aos seus agentes a API de Dados Foral como competências A2A: os mesmos dados, a mesma chave e a mesma quota da API REST, sem modelo de linguagem. Os agentes de empresas no Foral também falam A2A (última secção).
1. A2A de Dados Foral
O A2A de Dados Foral é um agente A2A 1.0 sobre a API de Dados Foral. O seu agente envia um pedido tipado — uma competência e a respetiva entrada — e recebe logo os dados: contratação pública (Portal BASE), sinais municipais, concelhos, os seus alertas e o catálogo de indicadores. As respostas são leituras determinísticas: sem modelo de linguagem, sem tarefa para acompanhar.
É para contas Premium e para os respetivos agentes. São os mesmos dados, a mesma chave e a mesma quota da API REST e do MCP de Dados.
| O quê | Valor |
|---|---|
| Cartão do agente | https://data.foral.ai/.well-known/agent-card.json |
| Endpoint (JSON-RPC 2.0) | https://data.foral.ai/a2a |
| Protocolo | A2A 1.0 — envie A2A-Version: 1.0 em todos os pedidos |
| Autenticação | A chave da API de Dados (sk_live_…) ou um token de acesso OAuth |
2. Ler o cartão do agente
O cartão é público: não precisa de chave. Com A2A-Version: 1.0 (cabeçalho ou ?A2A-Version=1.0) recebe o cartão A2A 1.0. Sem ele, recebe o antigo cartão de compatibilidade 0.3.
O cartão lista todas as competências. O JSON Schema da entrada de cada competência está na extensão Foral do cartão (capabilities.extensions[0].params.inputSchemas).
Ler o cartão
curl -s https://data.foral.ai/.well-known/agent-card.json \
-H "A2A-Version: 1.0"3. Autenticação
Envie Authorization: Bearer sk_live_… — a mesma chave da API REST. Crie-a na página Chaves de API. Guarde-a no seu servidor — nunca numa página de browser.
Um token de acesso OAuth também funciona: o token que um cliente obtém para o MCP de Dados (resource=https://data.foral.ai/mcp) também é aceite em /a2a. O cartão indica o servidor de autorização. Pode desligar uma aplicação a qualquer momento em Aplicações ligadas.
4. Chamar uma competência
O SendMessage recebe uma mensagem com exatamente UMA parte de dados: {"skill": "<id>", "input": {…}}, com mediaType: application/json. Partes de texto, ficheiro ou URL são recusadas com -32005.
A resposta é uma message com uma parte de dados: o mesmo JSON do endpoint REST da competência. Uma lista (as duas pesquisas) vem como {"items": [...]}. Não é criada nenhuma tarefa.
O messageId é seu (1 a 128 caracteres). Não é deduplicado: cada competência é uma leitura, e cada chamada conta como um pedido, como um GET repetido.
procurement_lookup_municipalities
curl -s https://data.foral.ai/a2a \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{
"jsonrpc": "2.0", "id": 1, "method": "SendMessage",
"params": {"message": {
"messageId": "msg-0001", "role": "ROLE_USER",
"parts": [{"data": {"skill": "procurement_lookup_municipalities",
"input": {"q": "Faro"}},
"mediaType": "application/json"}]
}}
}'Resposta
{
"jsonrpc": "2.0", "id": 1,
"result": {"message": {
"role": "ROLE_AGENT", "messageId": "msg_…",
"parts": [{"data": {"items": [
{"municipality": "<code>", "slug": "faro", "name": "Faro",
"district_name": "Faro", "match": "exact", …}
]}, "mediaType": "application/json"}],
"metadata": {"skill": "procurement_lookup_municipalities",
"rest": "GET /v1/procurement/lookup/municipalities"}
}}
}5. As suas competências (cartão alargado)
O GetExtendedAgentCard devolve o cartão reduzido às competências que a sua chave pode chamar: os domínios de dados do seu plano e os âmbitos da chave. Uma competência fora deles é recusada com -32004 e o erro indica o motivo (DOMAIN_NOT_IN_PLAN ou INSUFFICIENT_SCOPE).
Cartão alargado
curl -s https://data.foral.ai/a2a \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{"jsonrpc": "2.0", "id": 2, "method": "GetExtendedAgentCard"}'6. Competências
As competências são as ferramentas do MCP de Dados, com os mesmos nomes e a mesma entrada. Comece por me: devolve o plano, os domínios, os âmbitos e os limites.
| Competência | Domínio de dados |
|---|---|
me | — |
municipalities_search | indicators |
municipalities_get | indicators |
indicators_list | indicators |
indicators_series | indicators |
indicators_snapshot | indicators |
signals_list | signals |
signals_get | signals |
signal_cases_list | signals |
procurement_contracts | procurement |
procurement_aggregate | procurement |
procurement_entities | procurement |
procurement_entity_profile | procurement |
procurement_lookup_municipalities | procurement |
procurement_lookup_entities | procurement |
procurement_announcements | procurement |
procurement_amendments | procurement |
alert_rules | alerts |
alert_matches | alerts |
7. Python
Um cliente JSON-RPC simples com httpx: encontra o código de um concelho pelo nome e depois agrega os respetivos contratos por ano.
foral_data_a2a.py
# pip install httpx
import uuid
import httpx
URL = "https://data.foral.ai/a2a"
HEADERS = {
"Authorization": "Bearer sk_live_...",
"A2A-Version": "1.0",
}
def skill(name: str, data: dict) -> dict:
body = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": "SendMessage",
"params": {"message": {
"messageId": str(uuid.uuid4()), "role": "ROLE_USER",
"parts": [{"data": {"skill": name, "input": data},
"mediaType": "application/json"}],
}}}
reply = httpx.post(URL, json=body, headers=HEADERS, timeout=60).json()
if "error" in reply:
raise RuntimeError(reply["error"])
return reply["result"]["message"]["parts"][0]["data"]
faro = skill("procurement_lookup_municipalities", {"q": "Faro"})["items"][0]
totals = skill("procurement_aggregate", {"executed_in": [faro["municipality"]],
"from": "2025-01-01", "group_by": ["year"]})
print(totals)8. Limites e quota
Os limites são os da API REST. Cada pedido HTTP conta para o limite por minuto. Cada SendMessage a uma competência de dados conta como um pedido na quota mensal; me, o cartão e o GetExtendedAgentCard não gastam quota. Uma resposta 429 traz Retry-After (segundos).
9. Resolução de problemas
Uma recusa dos dados (quota, plano, âmbito, não encontrado, filtro inválido) é um erro JSON-RPC. O respetivo data traz um google.rpc.ErrorInfo (o reason é o código de erro da API REST) e o documento de erro completo da API REST.
| Resposta | O que significa |
|---|---|
401 | Chave ou token em falta, errado, expirado ou revogado. |
403 | O pedido veio de uma página de browser noutro site (cabeçalho Origin), ou o seu plano não inclui acesso à API de Dados (api-access-required). |
-32009 | Falta o cabeçalho A2A-Version: 1.0 (uma versão vazia significa 0.3). |
-32005 | Uma parte de texto, ficheiro ou URL. Envie uma parte de dados {skill, input}. |
-32602 | Competência desconhecida ou entrada inválida — o erro indica o campo. Também um filtro ou cursor inválido (VALIDATION_ERROR, INVALID_CURSOR). |
-32004 | Recusado: QUOTA_EXCEEDED (quota mensal esgotada), DOMAIN_NOT_IN_PLAN, INSUFFICIENT_SCOPE, NOT_FOUND, SERVICE_UNAVAILABLE. Também o SendStreamingMessage (sem fluxo). |
-32001 | Tarefa não encontrada. Este agente nunca cria tarefas; o ListTasks está sempre vazio. |
429 | Limite de pedidos. Espere Retry-After segundos. |
10. Agentes de empresas e Mercado
O agente Foral de uma empresa também pode ser publicado no canal A2A. O agente de outra empresa fala então com ele através de pedidos tipados: verificar uma área de serviço, avaliar um projeto, pesquisar uma entidade adjudicante.
Cartão: https://agents.foral.ai/<slug>/.well-known/agent-card.json. Endpoint: https://agents.foral.ai/<slug>/a2a. <slug> é o identificador da publicação, visível no estúdio da empresa. As regras do A2A-Version: 1.0 e das partes de dados são as mesmas de cima.
Autenticação: uma credencial do agente (sk_live_…) ligada a UMA publicação e ao canal A2A. Um administrador da empresa cria-a no estúdio (passo Publicar). Veja Agentes Foral.
Compradores no Mercado: uma credencial licenciada chega às competências de leitura do agente. O seu cartão é o autenticado (GetExtendedAgentCard).
Limites: 60 pedidos/minuto por credencial e 200/minuto por publicação (valores por omissão), partilhados com os outros canais de máquina do agente.
Ler o cartão de uma empresa
curl -s https://agents.foral.ai/<slug>/.well-known/agent-card.json \
-H "A2A-Version: 1.0"assess_project_fit → task
curl -s https://agents.foral.ai/<slug>/a2a \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{
"jsonrpc": "2.0", "id": 2, "method": "SendMessage",
"params": {"message": {
"messageId": "msg-0002", "role": "ROLE_USER",
"parts": [{"data": {"skill": "assess_project_fit",
"input": {"project_type": "moradia", "municipality": "Lagos",
"area_m2": 180}},
"mediaType": "application/json"}]
}}
}'Ler a tarefa
curl -s https://agents.foral.ai/<slug>/a2a \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{"jsonrpc": "2.0", "id": 3, "method": "GetTask", "params": {"id": "<task id>"}}'| Competência | O que faz | Resposta |
|---|---|---|
check_service_area | Diz se um concelho está dentro da área de serviço declarada. | message |
assess_project_fit | Factos tipados do projeto; devolve um veredicto tipado com motivos. | task |
procurement_research_buyer | Uma entidade adjudicante e um período: perfil e contratos, com citações. | task |
procurement_compare_contracts | Concelhos numa dimensão e num ano, ou dois NIF lado a lado. | task |
procurement_track_opportunities | Anúncios abertos por distrito e prefixo CPV, os que fecham primeiro à frente. | task |
prepare_action / commit_action | Uma ação com consequências: primeiro uma proposta, depois uma confirmação separada. | task |
get_artifact | Uma síntese já publicada deste agente, como referência. | message |
task lê-se com GetTask até o estado ser final. O cartão de cada publicação é a fonte de verdade: só as competências que lista podem ser chamadas. Um 404 significa um <slug> errado, uma publicação fora de produção ou uma credencial de outra publicação.