Skip to content
This repository was archived by the owner on Dec 23, 2025. It is now read-only.
Open
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: 0 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ jobs:
matrix:
include:
# Update this to build your own agent images.
- name: adk-debate-judge
dockerfile: scenarios/debate/Dockerfile.adk-debate-judge
- name: debate-judge
dockerfile: scenarios/debate/Dockerfile.debate-judge
- name: debater
Expand Down
43 changes: 25 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
## Quickstart
1. Clone the repo
```
git clone git@github.com:agentbeats/tutorial.git agentbeats-tutorial
git clone git@github.com:RDI-Foundation/agentbeats-tutorial.git agentbeats-tutorial
cd agentbeats-tutorial
```
2. Install dependencies
```
uv sync
uv sync --extra debate
```
This installs the optional dependencies needed to run the debate scenario (Gemini).
3. Set environment variables
```
cp sample.env .env
```
Add your Google API key to the .env file
Fill in the keys you plan to use (e.g. `GOOGLE_API_KEY` for the debate example and `OPENAI_API_KEY` for the tau2 example).

4. Run the [debate example](#example)
```
Expand All @@ -25,6 +26,8 @@ This command will:

**Note:** Use `--show-logs` to see agent outputs during the assessment, and `--serve-only` to start agents without running the assessment.

**Note:** If you see `Error: Some agent endpoints are already in use`, change the ports in the scenario TOML (or stop the process using them).

To run this example manually, start the agent servers in separate terminals, and then in another terminal run the A2A client on the scenario.toml file to initiate the assessment.

After running, you should see an output similar to this.
Expand All @@ -33,21 +36,22 @@ After running, you should see an output similar to this.

## Project Structure
```
src/
└─ agentbeats/
├─ green_executor.py # base A2A green agent executor
├─ models.py # pydantic models for green agent IO
├─ client.py # A2A messaging helpers
├─ client_cli.py # CLI client to start assessment
└─ run_scenario.py # run agents and start assessment

scenarios/
└─ debate/ # implementation of the debate example
├─ debate_judge.py # green agent impl using the official A2A SDK
├─ adk_debate_judge.py # alternative green agent impl using Google ADK
├─ debate_judge_common.py # models and utils shared by above impls
├─ debater.py # debater agent (Google ADK)
└─ scenario.toml # config for the debate example
├─ debate/
│ ├─ judge/src/ # green agent (green-agent-template structure)
│ ├─ debater/src/ # purple agent (agent-template structure)
│ ├─ Dockerfile.debate-judge
│ ├─ Dockerfile.debater
│ └─ scenario.toml
└─ tau2/
├─ evaluator/src/ # green agent (green-agent-template structure)
├─ agent/src/ # purple agent (agent-template structure)
├─ Dockerfile.tau2-evaluator
├─ Dockerfile.tau2-agent
├─ setup.sh # downloads tau2-bench data for local runs
└─ scenario.toml

src/agentbeats/ # optional local runner + A2A client helpers (`agentbeats-run`)
```

# AgentBeats Tutorial
Expand Down Expand Up @@ -118,7 +122,10 @@ To make things concrete, we will use a debate scenario as our toy example:
- Two purple agents (`Debater`) participate by presenting arguments for their side of the topic.

To run this example, we start all three servers and then use an A2A client to send an `assessment_request` to the green agent and observe its outputs.
The full example code is given in the template repository. Follow the quickstart guide to setup the project and run the example.
The debate example is implemented using the same structure as the supported templates:

- Green agent: `scenarios/debate/judge/src/` (green-agent-template style)
- Purple agent: `scenarios/debate/debater/src/` (agent-template style)

### Dockerizing Agent

Expand Down
20 changes: 13 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,22 @@ description = "Agentbeats Tutorial"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"a2a-sdk>=0.3.5",
"google-adk>=1.14.1",
"a2a-sdk[http-server]>=0.3.20",
"httpx>=0.28.1",
"pydantic>=2.11.9",
"python-dotenv>=1.1.1",
"uvicorn>=0.38.0",
]

[project.optional-dependencies]
debate = [
"google-genai>=1.36.0",
]
tau2-agent = [
"litellm>=1.0.0",
"loguru>=0.7.0",
]
tau2-evaluator = [
"nest-asyncio>=1.6.0",
"pydantic>=2.11.9",
"python-dotenv>=1.1.1",
"uvicorn>=0.35.0",
# tau2 from GitHub
"tau2 @ git+https://github.com/sierra-research/tau2-bench.git",
]

Expand Down
1 change: 1 addition & 0 deletions sample.env
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
GOOGLE_GENAI_USE_VERTEXAI=FALSE
GOOGLE_API_KEY=
OPENAI_API_KEY=
8 changes: 5 additions & 3 deletions scenarios/debate/Dockerfile.debate-judge
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
FROM ghcr.io/astral-sh/uv:python3.12-trixie
FROM ghcr.io/astral-sh/uv:python3.13-bookworm

ENV UV_HTTP_TIMEOUT=300

RUN adduser agentbeats
USER agentbeats
Expand All @@ -9,10 +11,10 @@ COPY src src

RUN \
--mount=type=cache,target=/home/agentbeats/.cache/uv,uid=1000 \
uv sync --locked
uv sync --locked --no-dev --no-install-project --extra debate

COPY scenarios scenarios

ENTRYPOINT ["uv", "run", "scenarios/debate/debate_judge.py"]
ENTRYPOINT ["uv", "run", "scenarios/debate/judge/src/server.py"]
CMD ["--host", "0.0.0.0"]
EXPOSE 9009
8 changes: 5 additions & 3 deletions scenarios/debate/Dockerfile.debater
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
FROM ghcr.io/astral-sh/uv:python3.12-trixie
FROM ghcr.io/astral-sh/uv:python3.13-bookworm

ENV UV_HTTP_TIMEOUT=300

RUN adduser agentbeats
USER agentbeats
Expand All @@ -9,10 +11,10 @@ COPY src src

RUN \
--mount=type=cache,target=/home/agentbeats/.cache/uv,uid=1000 \
uv sync --locked
uv sync --locked --no-dev --no-install-project --extra debate

COPY scenarios scenarios

ENTRYPOINT ["uv", "run", "scenarios/debate/debater.py"]
ENTRYPOINT ["uv", "run", "scenarios/debate/debater/src/server.py"]
CMD ["--host", "0.0.0.0"]
EXPOSE 9019
39 changes: 39 additions & 0 deletions scenarios/debate/debater/src/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from dotenv import load_dotenv
from google import genai

from a2a.server.tasks import TaskUpdater
from a2a.types import Message, Part, TaskState, TextPart
from a2a.utils import get_message_text, new_agent_text_message


load_dotenv()


SYSTEM_PROMPT = """
You are a professional debater.
Follow the role instructions in the prompt (Pro/Affirmative or Con/Negative).
Write a persuasive, well-structured argument. Keep it concise (<= 200 words).
"""


class Agent:
def __init__(self):
self.client = genai.Client()

async def run(self, message: Message, updater: TaskUpdater) -> None:
prompt = get_message_text(message)

await updater.update_status(TaskState.working, new_agent_text_message("Thinking..."))

response = self.client.models.generate_content(
model="gemini-2.5-flash-lite",
config=genai.types.GenerateContentConfig(system_instruction=SYSTEM_PROMPT),
contents=prompt,
)
text = response.text or ""

await updater.add_artifact(
parts=[Part(root=TextPart(text=text))],
name="Response",
)

68 changes: 68 additions & 0 deletions scenarios/debate/debater/src/executor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.tasks import TaskUpdater
from a2a.types import (
InvalidRequestError,
Task,
TaskState,
UnsupportedOperationError,
)
from a2a.utils import (
new_agent_text_message,
new_task,
)
from a2a.utils.errors import ServerError

from agent import Agent


TERMINAL_STATES = {
TaskState.completed,
TaskState.canceled,
TaskState.failed,
TaskState.rejected,
}


class Executor(AgentExecutor):
def __init__(self):
self.agents: dict[str, Agent] = {}

async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
msg = context.message
if not msg:
raise ServerError(error=InvalidRequestError(message="Missing message in request"))

task = context.current_task
if task and task.status.state in TERMINAL_STATES:
raise ServerError(
error=InvalidRequestError(
message=f"Task {task.id} already processed (state: {task.status.state})"
)
)

if not task:
task = new_task(msg)
await event_queue.enqueue_event(task)

context_id = task.context_id
updater = TaskUpdater(event_queue, task.id, context_id)
await updater.start_work()

try:
agent = self.agents.get(context_id)
if not agent:
agent = Agent()
self.agents[context_id] = agent

await agent.run(msg, updater)
if not updater._terminal_state_reached:
await updater.complete()
except Exception as e:
print(f"Task failed with agent error: {e}")
await updater.failed(
new_agent_text_message(f"Agent error: {e}", context_id=context_id, task_id=task.id)
)

async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
raise ServerError(error=UnsupportedOperationError())
55 changes: 55 additions & 0 deletions scenarios/debate/debater/src/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import argparse

import uvicorn
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import (
AgentCapabilities,
AgentCard,
AgentSkill,
)

from executor import Executor


def main():
parser = argparse.ArgumentParser(description="Run the debate participant (purple agent).")
parser.add_argument("--host", type=str, default="127.0.0.1", help="Host to bind the server")
parser.add_argument("--port", type=int, default=9019, help="Port to bind the server")
parser.add_argument("--card-url", type=str, help="URL to advertise in the agent card")
args = parser.parse_args()

skill = AgentSkill(
id="debate_participant",
name="Debate Participant",
description="Participates in a debate as either Pro or Con given role instructions.",
tags=["debate"],
examples=["Debate topic: Should artificial intelligence be regulated? Present your opening argument."],
)

agent_card = AgentCard(
name="Debater",
description="A debater agent that produces persuasive arguments given role instructions.",
url=args.card_url or f"http://{args.host}:{args.port}/",
version="1.0.0",
default_input_modes=["text"],
default_output_modes=["text"],
capabilities=AgentCapabilities(streaming=True),
skills=[skill],
)

request_handler = DefaultRequestHandler(
agent_executor=Executor(),
task_store=InMemoryTaskStore(),
)
server = A2AStarletteApplication(
agent_card=agent_card,
http_handler=request_handler,
)
uvicorn.run(server.build(), host=args.host, port=args.port)


if __name__ == "__main__":
main()

Loading