Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Install package and test deps
run: |
pip install -e .
pip install pytest pytest-cov pydantic-ai crewai
pip install pytest pytest-cov pydantic-ai crewai "a2a-sdk[http-server]>=0.3.0"

- name: Run unit tests with coverage
run: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ __pycache__/
.Python
env/
venv/
.venv/
ENV/
env.bak/
venv.bak/
Expand Down
109 changes: 109 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,9 @@ export GRADIENT_MODEL_ACCESS_KEY=your_gradient_key

# Optional: Enable verbose trace logging
export GRADIENT_VERBOSE=1

# Optional: A2A protocol — base URL for AgentCard discovery
export A2A_BASE_URL=https://your-app.ondigitalocean.app
```

## Project Structure
Expand All @@ -350,6 +353,112 @@ The Gradient ADK is designed to work with any Python-based AI agent framework:
- ✅ **CrewAI** - Use trace decorators for agent and task execution
- ✅ **Custom Frameworks** - Use trace decorators for any function

## A2A Protocol Support

The Gradient ADK supports the [Agent-to-Agent (A2A) protocol v0.3.0](https://github.com/google/A2A), enabling any `@entrypoint` agent to communicate with A2A-compatible clients. Install with `pip install gradient-adk[a2a]`.

### Wrapping an Agent with A2A

Any `@entrypoint` agent can be exposed as an A2A server with no code changes:

```python
from gradient_adk import entrypoint
from gradient_adk.a2a import create_a2a_server

@entrypoint
async def my_agent(data: dict, context) -> dict:
return {"output": f"You said: {data.get('prompt', '')}"}

app = create_a2a_server(my_agent)
```

Run with `uvicorn my_module:app --host 0.0.0.0 --port 8000`. The agent is discoverable at `/.well-known/agent-card.json` and accepts JSON-RPC calls (`message/send`, `tasks/get`, `tasks/cancel`).

### How the Protocol Works

A2A uses a discover-then-call pattern over JSON-RPC. Here is the full client-server flow:

1. **Discover** — The client fetches the AgentCard at `GET /.well-known/agent-card.json`. This returns the agent's name, transport URL, supported capabilities, and input/output modes. The client uses this to decide whether it can talk to this agent.

2. **Send** — The client sends a message via `POST /` with JSON-RPC method `message/send`. The server validates the message (text-only in MVP), creates a task, executes the agent, and returns a `Task` object with a `taskId` and current status.

3. **Poll** — The client checks task progress via `tasks/get` with the `taskId`. Once the task reaches a terminal state (`completed`, `failed`, or `canceled`), the response includes the agent's output in the task artifacts. The `historyLength` parameter controls how much conversation history is returned.

4. **Cancel** (optional) — The client can request cancellation via `tasks/cancel`. This is best-effort and idempotent — if the agent already finished, the cancel is a no-op.

```
Client Server
│ │
├── GET /.well-known/agent-card.json ──► AgentCard (capabilities, URL)
│ │
├── POST / message/send ──────────────► Create task → Execute agent
│◄─────────────────── Task {id, status} │
│ │
├── POST / tasks/get ─────────────────► Return task state + artifacts
│◄──────────── Task {id, status, result} │
│ │
└── POST / tasks/cancel ──────────────► Best-effort cancellation
```

### Deploying to DigitalOcean App Platform

When you deploy to App Platform, the public URL is assigned after deployment. The A2A server needs this URL for the AgentCard so that clients know where to send requests. The workflow is:

1. **Deploy your agent** to App Platform as usual with `gradient agent deploy`
2. **Get your app's public URL** from the App Platform dashboard (e.g., `https://your-agent-abc123.ondigitalocean.app`)
3. **Set the environment variable** in your app's settings:
```bash
A2A_BASE_URL=https://your-agent-abc123.ondigitalocean.app
```
4. **Redeploy** — the agent restarts and the AgentCard now advertises the correct public URL

For local development, no configuration is needed — it defaults to `http://localhost:8000`.

### Calling a Remote A2A Agent from Another Agent

Once deployed, any A2A-compatible agent or client can call your agent:

```python
import httpx

# Discover the remote agent
card = httpx.get("https://your-agent.ondigitalocean.app/.well-known/agent-card.json").json()
rpc_url = card["url"]

# Send a message
response = httpx.post(rpc_url, json={
"jsonrpc": "2.0", "id": "1",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{"kind": "text", "text": "Hello from another agent!"}],
"message_id": "msg-1",
"kind": "message",
}
},
})
task = response.json()["result"]

# Poll until done
result = httpx.post(rpc_url, json={
"jsonrpc": "2.0", "id": "2",
"method": "tasks/get",
"params": {"id": task["id"]},
}).json()["result"]
```

See `examples/a2a/client.py` for a complete async client with discovery, send, poll, and cancel.

### Supported Operations

- **`message/send`**: Send a message to the agent, creates or continues a task
- **`tasks/get`**: Poll task state and retrieve results (supports `historyLength`)
- **`tasks/cancel`**: Best-effort task cancellation (idempotent)
- **Agent Discovery**: `GET /.well-known/agent-card.json` for capabilities and transport URL

Text-only input/output (`text/plain`) in the current release. Streaming, push notifications, and authenticated extended cards are explicitly disabled via AgentCard capability flags.

## Support

- **Templates/Examples**: [https://github.com/digitalocean/gradient-adk-templates](https://github.com/digitalocean/gradient-adk-templates)
Expand Down
131 changes: 131 additions & 0 deletions examples/a2a/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Sample A2A client demonstrating discovery, send, polling, and cancel operations.

Usage:
pip install gradient-adk[a2a] httpx
python examples/a2a/client.py
"""

import asyncio
import httpx


async def discover_agent(base_url: str) -> dict:
"""Discover agent capabilities via AgentCard."""
async with httpx.AsyncClient() as client:
response = await client.get(f"{base_url}/.well-known/agent-card.json")
response.raise_for_status()
agent_card = response.json()
print(f"Discovered agent: {agent_card['name']}")
print(f"Transport URL: {agent_card['url']}")
return agent_card


async def send_message(rpc_url: str, message_text: str) -> dict:
"""Send a message to the agent."""
async with httpx.AsyncClient() as client:
response = await client.post(
rpc_url,
json={
"jsonrpc": "2.0",
"id": "1",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{"kind": "text", "text": message_text}],
"message_id": "msg-1",
"kind": "message",
}
},
},
)
response.raise_for_status()
result = response.json()
if "error" in result:
raise Exception(f"Error: {result['error']}")
return result["result"]


async def get_task(rpc_url: str, task_id: str) -> dict:
"""Poll task status."""
async with httpx.AsyncClient() as client:
response = await client.post(
rpc_url,
json={
"jsonrpc": "2.0",
"id": "2",
"method": "tasks/get",
"params": {"id": task_id},
},
)
response.raise_for_status()
result = response.json()
if "error" in result:
raise Exception(f"Error: {result['error']}")
return result["result"]


async def cancel_task(rpc_url: str, task_id: str) -> dict:
"""Cancel a task."""
async with httpx.AsyncClient() as client:
response = await client.post(
rpc_url,
json={
"jsonrpc": "2.0",
"id": "3",
"method": "tasks/cancel",
"params": {"id": task_id},
},
)
response.raise_for_status()
result = response.json()
if "error" in result:
raise Exception(f"Error: {result['error']}")
return result["result"]


async def main():
"""Demonstrate A2A client operations."""
base_url = "http://localhost:8000"
rpc_url = f"{base_url}/"

print("=== A2A Client Demo ===\n")

# 1. Discover agent
print("1. Discovering agent...")
agent_card = await discover_agent(base_url)
print()

# 2. Send message
print("2. Sending message...")
send_result = await send_message(rpc_url, "Hello, A2A!")
task_id = send_result["id"]
print(f" Task ID: {task_id}")
print(f" Status: {send_result['status']['state']}")
print()

# 3. Poll task status
print("3. Polling task status...")
task = await get_task(rpc_url, task_id)
print(f" Task ID: {task['id']}")
print(f" Status: {task['status']['state']}")
if task.get("response"):
parts = task["response"].get("parts", [])
if parts:
print(f" Response: {parts[0].get('text', 'N/A')}")
print()

# 4. Cancel task (example)
print("4. Canceling task (example)...")
send_result2 = await send_message(rpc_url, "This will be canceled")
task_id2 = send_result2["id"]
cancel_result = await cancel_task(rpc_url, task_id2)
print(f" Task ID: {cancel_result['id']}")
print(f" Status: {cancel_result['status']['state']}")
print()

print("=== Demo Complete ===")


if __name__ == "__main__":
asyncio.run(main())
70 changes: 70 additions & 0 deletions examples/a2a/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Example A2A server agent using Gradient ADK @entrypoint decorator.

This example demonstrates how to create an A2A-compatible agent that can be
accessed via the A2A protocol using the Gradient ADK.

To run this agent:
1. Ensure you have gradient-adk and a2a-sdk installed:
pip install gradient-adk[a2a]
2. Set the base URL for discovery (optional, defaults to localhost for dev):
export A2A_BASE_URL=https://your-app.ondigitalocean.app
3. Run: gradient agent run
4. The agent will be available with both Gradient and A2A protocols
"""

from gradient_adk import entrypoint


@entrypoint
async def echo_agent(data: dict, context) -> dict:
"""A simple echo agent that repeats the user's input.

This agent:
- Receives text input via A2A protocol
- Echoes it back with a prefix
- Works with both Gradient /run endpoint and A2A protocol

Args:
data: Dictionary containing the user's input.
For A2A protocol, this will contain {"prompt": "user text"}
context: Request context (not used in this simple example)

Returns:
Dictionary with the agent's response.
For A2A protocol, the "output" key will be extracted as the response.
"""
user_input = data.get("prompt", "")

if not user_input:
return {"output": "No input provided. Please send a message."}

response = f"Echo: {user_input}"

return {"output": response}


@entrypoint
async def greeting_agent(data: dict, context) -> dict:
"""A greeting agent that responds with a personalized message.

This is an alternative example showing how to create a more
sophisticated agent that still works with A2A protocol.

Args:
data: Dictionary containing the user's input
context: Request context

Returns:
Dictionary with the agent's response
"""
user_input = data.get("prompt", "").strip()

if not user_input:
return {"output": "Hello! What's your name?"}

if user_input.lower().startswith("hello"):
return {"output": f"Hello! Nice to meet you. You said: {user_input}"}
elif user_input.lower().startswith("hi"):
return {"output": f"Hi there! You said: {user_input}"}
else:
return {"output": f"Greetings! You said: {user_input}"}
11 changes: 11 additions & 0 deletions gradient_adk/a2a/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""A2A protocol v0.3.0 integration for Gradient agents.

Public API:
create_a2a_server: Create an A2A-enabled FastAPI server for a Gradient agent
"""

from gradient_adk.a2a.infrastructure.server import create_a2a_server

__all__ = [
"create_a2a_server",
]
1 change: 1 addition & 0 deletions gradient_adk/a2a/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Adapters layer - integrates with external systems (SDK, Gradient)."""
1 change: 1 addition & 0 deletions gradient_adk/a2a/adapters/primary/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Primary adapters - implement external interfaces (A2A SDK)."""
Loading