Skip to content

Commit 12c677c

Browse files
authored
Initial commit
0 parents  commit 12c677c

12 files changed

Lines changed: 1327 additions & 0 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: Test and Publish Agent
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
tags:
9+
- 'v*' # Trigger on version tags like v1.0.0, v1.1.0
10+
11+
jobs:
12+
test-and-publish:
13+
runs-on: ubuntu-latest
14+
15+
# These permissions are required for the workflow to:
16+
# - Read repository contents (checkout code)
17+
# - Write to GitHub Container Registry (push Docker images)
18+
permissions:
19+
contents: read
20+
packages: write
21+
22+
steps:
23+
- name: Checkout repository
24+
uses: actions/checkout@v4
25+
26+
- name: Extract metadata for Docker
27+
id: meta
28+
uses: docker/metadata-action@v5
29+
with:
30+
images: ghcr.io/${{ github.repository }}
31+
tags: |
32+
type=ref,event=pr
33+
type=semver,pattern={{version}}
34+
type=semver,pattern={{major}}
35+
type=raw,value=latest,enable={{is_default_branch}}
36+
37+
- name: Build Docker image
38+
uses: docker/build-push-action@v5
39+
with:
40+
context: .
41+
push: false
42+
tags: ${{ steps.meta.outputs.tags }}
43+
labels: ${{ steps.meta.outputs.labels }}
44+
load: true
45+
platforms: linux/amd64
46+
47+
- name: Start agent container
48+
env:
49+
SECRETS_JSON: ${{ toJson(secrets) }}
50+
run: |
51+
echo "$SECRETS_JSON" | jq -r 'to_entries[] | "\(.key)=\(.value)"' > .env
52+
docker run -d -p 9009:9009 --name agent-container --env-file .env $(echo "${{ steps.meta.outputs.tags }}" | head -n1) --host 0.0.0.0 --port 9009
53+
timeout 30 bash -c 'until curl -sf http://localhost:9009/.well-known/agent-card.json > /dev/null; do sleep 1; done'
54+
55+
- name: Set up uv
56+
uses: astral-sh/setup-uv@v4
57+
58+
- name: Install test dependencies
59+
run: uv sync --extra test
60+
61+
- name: Run tests
62+
run: uv run pytest -v --agent-url http://localhost:9009
63+
64+
- name: Stop container and show logs
65+
if: always()
66+
run: |
67+
echo "=== Agent Container Logs ==="
68+
docker logs agent-container || true
69+
docker stop agent-container || true
70+
71+
- name: Log in to GitHub Container Registry
72+
if: success() && github.event_name != 'pull_request'
73+
uses: docker/login-action@v3
74+
with:
75+
registry: ghcr.io
76+
username: ${{ github.actor }}
77+
password: ${{ secrets.GITHUB_TOKEN }}
78+
79+
- name: Push Docker image
80+
if: success() && github.event_name != 'pull_request'
81+
run: docker push --all-tags ghcr.io/${GITHUB_REPOSITORY,,}
82+
83+
- name: Output image digest
84+
if: success() && github.event_name != 'pull_request'
85+
run: |
86+
echo "## Docker Image Published :rocket:" >> $GITHUB_STEP_SUMMARY
87+
echo "" >> $GITHUB_STEP_SUMMARY
88+
echo "**Tags:** ${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.DS_Store
2+
.env
3+
.python-version
4+
5+
# Python-generated files
6+
__pycache__/
7+
*.py[oc]
8+
build/
9+
dist/
10+
wheels/
11+
*.egg-info
12+
13+
# Virtual environments
14+
.venv

Dockerfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
FROM ghcr.io/astral-sh/uv:python3.13-bookworm
2+
3+
RUN adduser agent
4+
USER agent
5+
WORKDIR /home/agent
6+
7+
COPY pyproject.toml uv.lock README.md ./
8+
COPY src src
9+
10+
RUN \
11+
--mount=type=cache,target=/home/agent/.cache/uv,uid=1000 \
12+
uv sync --locked
13+
14+
ENTRYPOINT ["uv", "run", "src/server.py"]
15+
CMD ["--host", "0.0.0.0"]
16+
EXPOSE 9009

README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# A2A Agent Template
2+
3+
A minimal template for building [A2A (Agent-to-Agent)](https://a2a-protocol.org/latest/) green agents compatible with the [AgentBeats](https://agentbeats.dev) platform.
4+
5+
## Project Structure
6+
7+
```
8+
src/
9+
├─ server.py # Server setup and agent card configuration
10+
├─ executor.py # A2A request handling
11+
├─ agent.py # Your agent implementation goes here
12+
└─ messenger.py # A2A messaging utilities
13+
tests/
14+
└─ test_agent.py # Agent tests
15+
Dockerfile # Docker configuration
16+
pyproject.toml # Python dependencies
17+
.github/
18+
└─ workflows/
19+
└─ test-and-publish.yml # CI workflow
20+
```
21+
22+
## Getting Started
23+
24+
1. **Create your repository** - Click "Use this template" to create your own repository from this template
25+
26+
2. **Implement your agent** - Add your agent logic to [`src/agent.py`](src/agent.py)
27+
28+
3. **Configure your agent card** - Fill in your agent's metadata (name, skills, description) in [`src/server.py`](src/server.py)
29+
30+
4. **Write your tests** - Add custom tests for your agent in [`tests/test_agent.py`](tests/test_agent.py)
31+
32+
For a concrete example of implementing a green agent using this template, see this [draft PR](https://github.com/RDI-Foundation/green-agent-template/pull/3).
33+
34+
## Running Locally
35+
36+
```bash
37+
# Install dependencies
38+
uv sync
39+
40+
# Run the server
41+
uv run src/server.py
42+
```
43+
44+
## Running with Docker
45+
46+
```bash
47+
# Build the image
48+
docker build -t my-agent .
49+
50+
# Run the container
51+
docker run -p 9009:9009 my-agent
52+
```
53+
54+
## Testing
55+
56+
Run A2A conformance tests against your agent.
57+
58+
```bash
59+
# Install test dependencies
60+
uv sync --extra test
61+
62+
# Start your agent (uv or docker; see above)
63+
64+
# Run tests against your running agent URL
65+
uv run pytest --agent-url http://localhost:9009
66+
```
67+
68+
## Publishing
69+
70+
The repository includes a GitHub Actions workflow that automatically builds, tests, and publishes a Docker image of your agent to GitHub Container Registry.
71+
72+
If your agent needs API keys or other secrets, add them in Settings → Secrets and variables → Actions → Repository secrets. They'll be available as environment variables during CI tests.
73+
74+
- **Push to `main`** → publishes `latest` tag:
75+
```
76+
ghcr.io/<your-username>/<your-repo-name>:latest
77+
```
78+
79+
- **Create a git tag** (e.g. `git tag v1.0.0 && git push origin v1.0.0`) → publishes version tags:
80+
```
81+
ghcr.io/<your-username>/<your-repo-name>:1.0.0
82+
ghcr.io/<your-username>/<your-repo-name>:1
83+
```
84+
85+
Once the workflow completes, find your Docker image in the Packages section (right sidebar of your repository). Configure the package visibility in package settings.
86+
87+
> **Note:** Organization repositories may need package write permissions enabled manually (Settings → Actions → General). Version tags must follow [semantic versioning](https://semver.org/) (e.g., `v1.0.0`).

pyproject.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[project]
2+
name = "green-agent-template"
3+
version = "0.1.0"
4+
description = "A template for A2A green agents"
5+
readme = "README.md"
6+
requires-python = ">=3.13"
7+
dependencies = [
8+
"a2a-sdk[http-server]>=0.3.20",
9+
"pydantic>=2.12.5",
10+
"uvicorn>=0.38.0",
11+
]
12+
13+
[project.optional-dependencies]
14+
test = [
15+
"pytest>=8.0.0",
16+
"pytest-asyncio>=0.24.0",
17+
"httpx>=0.28.1",
18+
]

src/agent.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from typing import Any
2+
from pydantic import BaseModel, HttpUrl, ValidationError
3+
from a2a.server.tasks import TaskUpdater
4+
from a2a.types import Message, TaskState, Part, TextPart, DataPart
5+
from a2a.utils import get_message_text, new_agent_text_message
6+
7+
from messenger import Messenger
8+
9+
10+
class EvalRequest(BaseModel):
11+
"""Request format sent by the AgentBeats platform to green agents."""
12+
participants: dict[str, HttpUrl] # role -> agent URL
13+
config: dict[str, Any]
14+
15+
16+
class Agent:
17+
# Fill in: list of required participant roles, e.g. ["pro_debater", "con_debater"]
18+
required_roles: list[str] = []
19+
# Fill in: list of required config keys, e.g. ["topic", "num_rounds"]
20+
required_config_keys: list[str] = []
21+
22+
def __init__(self):
23+
self.messenger = Messenger()
24+
# Initialize other state here
25+
26+
def validate_request(self, request: EvalRequest) -> tuple[bool, str]:
27+
missing_roles = set(self.required_roles) - set(request.participants.keys())
28+
if missing_roles:
29+
return False, f"Missing roles: {missing_roles}"
30+
31+
missing_config_keys = set(self.required_config_keys) - set(request.config.keys())
32+
if missing_config_keys:
33+
return False, f"Missing config keys: {missing_config_keys}"
34+
35+
# Add additional request validation here
36+
37+
return True, "ok"
38+
39+
async def run(self, message: Message, updater: TaskUpdater) -> None:
40+
"""Implement your agent logic here.
41+
42+
Args:
43+
message: The incoming message
44+
updater: Report progress (update_status) and results (add_artifact)
45+
46+
Use self.messenger.talk_to_agent(message, url) to call other agents.
47+
"""
48+
input_text = get_message_text(message)
49+
50+
try:
51+
request: EvalRequest = EvalRequest.model_validate_json(input_text)
52+
ok, msg = self.validate_request(request)
53+
if not ok:
54+
await updater.reject(new_agent_text_message(msg))
55+
return
56+
except ValidationError as e:
57+
await updater.reject(new_agent_text_message(f"Invalid request: {e}"))
58+
return
59+
60+
# Replace example code below with your agent logic
61+
# Use request.participants to get participant agent URLs by role
62+
# Use request.config for assessment parameters
63+
64+
await updater.update_status(
65+
TaskState.working, new_agent_text_message("Thinking...")
66+
)
67+
await updater.add_artifact(
68+
parts=[
69+
Part(root=TextPart(text="The agent performed well.")),
70+
Part(root=DataPart(data={
71+
# structured assessment results
72+
}))
73+
],
74+
name="Result",
75+
)

src/executor.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from a2a.server.agent_execution import AgentExecutor, RequestContext
2+
from a2a.server.events import EventQueue
3+
from a2a.server.tasks import TaskUpdater
4+
from a2a.types import (
5+
Task,
6+
TaskState,
7+
UnsupportedOperationError,
8+
InvalidRequestError,
9+
)
10+
from a2a.utils.errors import ServerError
11+
from a2a.utils import (
12+
new_agent_text_message,
13+
new_task,
14+
)
15+
16+
from agent import Agent
17+
18+
19+
TERMINAL_STATES = {
20+
TaskState.completed,
21+
TaskState.canceled,
22+
TaskState.failed,
23+
TaskState.rejected
24+
}
25+
26+
27+
class Executor(AgentExecutor):
28+
def __init__(self):
29+
self.agents: dict[str, Agent] = {} # context_id to agent instance
30+
31+
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
32+
msg = context.message
33+
if not msg:
34+
raise ServerError(error=InvalidRequestError(message="Missing message in request"))
35+
36+
task = context.current_task
37+
if task and task.status.state in TERMINAL_STATES:
38+
raise ServerError(error=InvalidRequestError(message=f"Task {task.id} already processed (state: {task.status.state})"))
39+
40+
if not task:
41+
task = new_task(msg)
42+
await event_queue.enqueue_event(task)
43+
44+
context_id = task.context_id
45+
agent = self.agents.get(context_id)
46+
if not agent:
47+
agent = Agent()
48+
self.agents[context_id] = agent
49+
50+
updater = TaskUpdater(event_queue, task.id, context_id)
51+
52+
await updater.start_work()
53+
try:
54+
await agent.run(msg, updater)
55+
if not updater._terminal_state_reached:
56+
await updater.complete()
57+
except Exception as e:
58+
print(f"Task failed with agent error: {e}")
59+
await updater.failed(new_agent_text_message(f"Agent error: {e}", context_id=context_id, task_id=task.id))
60+
61+
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
62+
raise ServerError(error=UnsupportedOperationError())

0 commit comments

Comments
 (0)