Current section
Files
Jump to
Current section
Files
lib/claudio/a2a/transport/http.ex
defmodule Claudio.A2A.Transport.HTTP do
@moduledoc """
JSON-RPC 2.0 over HTTP transport for the A2A protocol.
This is the default transport. Uses Req for HTTP and Poison for JSON.
"""
@behaviour Claudio.A2A.Transport
alias Claudio.A2A.{AgentCard, Task, Message}
import Claudio.A2A.Util, only: [maybe_put: 3]
@agent_card_path "/.well-known/agent-card.json"
@impl true
def discover(base_url, opts) do
url = String.trim_trailing(base_url, "/") <> @agent_card_path
case http_get(url, opts) do
{:ok, body} -> {:ok, AgentCard.from_map(body)}
error -> error
end
end
@impl true
def send_message(endpoint, %Message{} = message, opts) do
params = build_send_params(message, opts)
case json_rpc(endpoint, "message/send", params, opts) do
{:ok, %{"id" => _} = result} -> {:ok, Task.from_map(result)}
{:ok, %{"messageId" => _} = result} -> {:ok, Message.from_map(result)}
error -> error
end
end
@impl true
def get_task(endpoint, task_id, opts) do
params =
%{"id" => task_id}
|> maybe_put("historyLength", Keyword.get(opts, :history_length))
case json_rpc(endpoint, "tasks/get", params, opts) do
{:ok, result} -> {:ok, Task.from_map(result)}
error -> error
end
end
@impl true
def list_tasks(endpoint, opts) do
params =
%{}
|> maybe_put("contextId", Keyword.get(opts, :context_id))
|> maybe_put("status", state_to_string(Keyword.get(opts, :status)))
|> maybe_put("pageSize", Keyword.get(opts, :page_size))
|> maybe_put("pageToken", Keyword.get(opts, :page_token))
|> maybe_put("historyLength", Keyword.get(opts, :history_length))
case json_rpc(endpoint, "tasks/list", params, opts) do
{:ok, result} ->
tasks = Enum.map(result["tasks"] || [], &Task.from_map/1)
{:ok,
%{
tasks: tasks,
next_page_token: result["nextPageToken"] || ""
}}
error ->
error
end
end
@impl true
def cancel_task(endpoint, task_id, opts) do
params = %{"id" => task_id}
case json_rpc(endpoint, "tasks/cancel", params, opts) do
{:ok, result} -> {:ok, Task.from_map(result)}
error -> error
end
end
# JSON-RPC helpers
defp json_rpc(endpoint, method, params, opts) do
request_id = generate_request_id()
body = %{
"jsonrpc" => "2.0",
"method" => method,
"id" => request_id,
"params" => params
}
case http_post(endpoint, body, opts) do
{:ok, %{"result" => result}} -> {:ok, result}
{:ok, %{"error" => error}} -> {:error, parse_rpc_error(error)}
{:ok, other} -> {:error, {:unexpected_response, other}}
error -> error
end
end
defp http_get(url, opts) do
headers = build_headers(opts)
req_opts = [headers: headers, decode_body: false] ++ timeout_opts(opts)
case Req.get(url, req_opts) do
{:ok, %{status: 200, body: body}} -> decode_json(body)
{:ok, %{status: status}} -> {:error, {:http_error, status}}
{:error, reason} -> {:error, reason}
end
end
defp http_post(endpoint, body, opts) do
headers = [{"content-type", "application/json"}] ++ build_headers(opts)
encoded = Poison.encode!(body)
req_opts = [body: encoded, headers: headers, decode_body: false] ++ timeout_opts(opts)
case Req.post(endpoint, req_opts) do
{:ok, %{status: 200, body: body}} -> decode_json(body)
{:ok, %{status: status}} -> {:error, {:http_error, status}}
{:error, reason} -> {:error, reason}
end
end
defp timeout_opts(opts) do
Enum.reduce([:receive_timeout, :connect_options], [], fn key, acc ->
case Keyword.get(opts, key) do
nil -> acc
value -> [{key, value} | acc]
end
end)
end
defp build_headers(opts) do
case Keyword.get(opts, :auth_token) do
nil -> []
token -> [{"authorization", "Bearer #{token}"}]
end
end
defp build_send_params(message, opts) do
%{"message" => Message.to_map(message)}
|> maybe_put("configuration", Keyword.get(opts, :configuration))
|> maybe_put("metadata", Keyword.get(opts, :metadata))
end
defp parse_rpc_error(%{"code" => code, "message" => message}) do
%{code: code, message: message}
end
defp parse_rpc_error(error), do: error
defp decode_json(body) do
case Poison.decode(body) do
{:ok, decoded} -> {:ok, decoded}
{:error, _} -> {:error, :invalid_json}
end
end
defp generate_request_id do
"req-" <> Base.encode16(:crypto.strong_rand_bytes(4), case: :lower)
end
defp state_to_string(nil), do: nil
defp state_to_string(:submitted), do: "TASK_STATE_SUBMITTED"
defp state_to_string(:working), do: "TASK_STATE_WORKING"
defp state_to_string(:completed), do: "TASK_STATE_COMPLETED"
defp state_to_string(:failed), do: "TASK_STATE_FAILED"
defp state_to_string(:canceled), do: "TASK_STATE_CANCELED"
defp state_to_string(:input_required), do: "TASK_STATE_INPUT_REQUIRED"
defp state_to_string(:rejected), do: "TASK_STATE_REJECTED"
defp state_to_string(:auth_required), do: "TASK_STATE_AUTH_REQUIRED"
defp state_to_string(other) when is_binary(other), do: other
end