Skip to content

Commit 5c21606

Browse files
authored
Initial commit
0 parents  commit 5c21606

File tree

13 files changed

+1381
-0
lines changed

13 files changed

+1381
-0
lines changed
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: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
amber-manifest.json5 # Amber manifest
18+
.github/
19+
└─ workflows/
20+
└─ test-and-publish.yml # CI workflow
21+
```
22+
23+
## Getting Started
24+
25+
1. **Create your repository** - Click "Use this template" to create your own repository from this template
26+
27+
2. **Implement your agent** - Add your agent logic to [`src/agent.py`](src/agent.py)
28+
29+
3. **Configure your agent card** - Fill in your agent's metadata (name, skills, description) in [`src/server.py`](src/server.py)
30+
31+
4. **Fill out your [Amber](https://github.com/RDI-Foundation/amber) manifest** - Update [`amber-manifest.json5`](amber-manifest.json5) to use your agent in Amber scenarios
32+
33+
5. **Write your tests** - Add custom tests for your agent in [`tests/test_agent.py`](tests/test_agent.py)
34+
35+
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).
36+
37+
## Running Locally
38+
39+
```bash
40+
# Install dependencies
41+
uv sync
42+
43+
# Run the server
44+
uv run src/server.py
45+
```
46+
47+
## Running with Docker
48+
49+
```bash
50+
# Build the image
51+
docker build -t my-agent .
52+
53+
# Run the container
54+
docker run -p 9009:9009 my-agent
55+
```
56+
57+
## Testing
58+
59+
Run A2A conformance tests against your agent.
60+
61+
```bash
62+
# Install test dependencies
63+
uv sync --extra test
64+
65+
# Start your agent (uv or docker; see above)
66+
67+
# Run tests against your running agent URL
68+
uv run pytest --agent-url http://localhost:9009
69+
```
70+
71+
## Publishing
72+
73+
The repository includes a GitHub Actions workflow that automatically builds, tests, and publishes a Docker image of your agent to GitHub Container Registry.
74+
75+
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.
76+
77+
- **Push to `main`** → publishes `latest` tag:
78+
```
79+
ghcr.io/<your-username>/<your-repo-name>:latest
80+
```
81+
82+
- **Create a git tag** (e.g. `git tag v1.0.0 && git push origin v1.0.0`) → publishes version tags:
83+
```
84+
ghcr.io/<your-username>/<your-repo-name>:1.0.0
85+
ghcr.io/<your-username>/<your-repo-name>:1
86+
```
87+
88+
Once the workflow completes, find your Docker image in the Packages section (right sidebar of your repository). Configure the package visibility in package settings.
89+
90+
> **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`).

amber-manifest.json5

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
manifest_version: "0.1.0",
3+
4+
// TODO: Declare all config your agent needs from the deployer
5+
// Use secret: true for sensitive values like API keys
6+
config_schema: {
7+
type: "object",
8+
properties: {
9+
my_api_key: { type: "string", secret: true },
10+
},
11+
required: ["my_api_key"],
12+
additionalProperties: false,
13+
},
14+
15+
// External services this agent uses
16+
slots: {
17+
proxy: { kind: "a2a", optional: true },
18+
},
19+
20+
// Runtime configuration
21+
program: {
22+
// TODO: Replace with your docker image
23+
image: "ghcr.io/<your-username>/<your-repo-name>:latest",
24+
entrypoint: "uv run python src/server.py --host 0.0.0.0 --port 9009",
25+
26+
// TODO: Pass config values as environment variables
27+
env: {
28+
MY_API_KEY: "${config.my_api_key}",
29+
},
30+
31+
network: {
32+
endpoints: [
33+
{ name: "a2a_endpoint", port: 9009 }, // ensure port matches your entrypoint
34+
],
35+
},
36+
},
37+
38+
// Capabilities provided by this agent
39+
provides: {
40+
a2a: { kind: "a2a", endpoint: "a2a_endpoint" },
41+
},
42+
43+
// Capabilities exposed to the scenario
44+
exports: { a2a: "a2a" },
45+
46+
// TODO: Describe the assessment request your green agent receives
47+
metadata: {
48+
assessment_config: {}, // replace with your default assessment parameters, e.g. {"num_rounds": 2}
49+
participant_roles: [], // list the roles your green agent expects, e.g. ["pro_debater", "con_debater"]
50+
},
51+
}

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+
)

0 commit comments

Comments
 (0)