Skip to main content
Version: v6.0.0 [Falcon]

Lab 2: Develop a custom agent

In this lab you build your own agent, the Throughput Checker, and run it on your computer. You tell it the downlink throughput of a network, and it tells you if the network meets its throughput target. In Lab 3, it will learn to measure the throughput by itself.

â„šī¸ About this example. The agent you build in Labs 2 to 4 is a small example. It shows how to develop an agent, how to integrate it with the platform, by calling the agents already running on MX-PDK and deploying it next to them, and how to extend a blueprint with it. The job it does, comparing a throughput with a target, does not need AI at all: a short script would do it faster, cheaper and more reliably. Agents are worth it when a task needs language: understanding free-form questions, deciding what to look at, and explaining results.

You use two tools from the BubbleRAN Agentic Toolkit (BAT):

  • the BAT CLI (bat command): it creates the project for you and runs common tasks;
  • the BAT ADK (bat-adk Python library): you write the agent's logic with it. It is built on LangGraph.

By the end of this lab you will be able to:

  • create a blueprint and an agent with bat;
  • configure an agent: its settings, its secrets and its agent card;
  • give an LLM a tool, and let it use the tool in a ReAct loop;
  • run the agent and test it with curl.

Before you start, read the prerequisites for Labs 2 to 5. This lab runs on your computer only: you don't need the cluster yet.

Part 1: The idea​

  "Network o-ran has 42 Mbps of downlink throughput. Does it meet the target?"
│
┌────────────────────â–ŧ─────────────────────┐
│ judge (the ReAct loop) │
│ │
│ 1. The LLM reads the question. │
│ 2. It calls the tool check_target(42). │
│ 3. The tool answers: BELOW TARGET. │
│ 4. The LLM writes the final answer. │
└────────────────────â”Ŧ─────────────────────┘
│
"o-ran: BELOW TARGET, 42 Mbps measured, target 50 Mbps"

Your agent uses an LLM in a ReAct loop (Reason + Act): the LLM reads the request, decides which tool to call, reads the tool's result, and repeats until it can answer. A tool is a plain Python function that the LLM can call.

Here the work is split in two:

  • the LLM handles the language: it finds the throughput in your sentence, even if you write it in your own words;
  • the tool check_target applies the rule: it compares the throughput with the target. Rules belong in code, because code always gives the same result.

Part 2: Install the tools​

Step 1: Install uv​

uv installs Python and the Python packages your agent needs. You don't need to install Python yourself.

curl -LsSf https://astral.sh/uv/install.sh | sh

Close your terminal and open a new one, then check:

uv --version

You should see a version number, for example uv 0.11.17.

Step 2: Install the BAT CLI​

uv tool install bat-cli==2026.10
bat version

You should see:

bat-cli 2026.10

Part 3: Create the blueprint and the agent​

A blueprint is a folder that holds one or more agents. All its agents are packed into one container image, like the Observability blueprint of Lab 1. You create a blueprint called tutorial, with one agent called throughput_checker:

bat init blueprint tutorial
cd tutorial
bat add agent throughput_checker --clients Checker

You should see:

Created BAT blueprint in: /home/you/tutorial
Files written: 11
Next: cd into it and run `bat add agent <name>`.
Added agent 'throughput_checker' to blueprint 'tutorial'.
Files written: 9
Run it with: make throughput_checker
  • The agent name uses _, not -, because it is also a Python name.
  • --clients Checker creates an LLM client called CheckerClient: the piece of code that talks to the LLM.

The blueprint now looks like this:

tutorial/
├── .env your secrets, like the API key (never share it)
├── pyproject.toml the Python packages the blueprint needs
├── __main__.py starts the agent named on the command line
├── Makefile shortcuts: run an agent, build the image
├── Dockerfile how to build the container image (Lab 4)
├── tutorial.spec how to pack the code into one program (Lab 4)
├── docker-compose.yaml runs the agents with Docker (not used here)
├── README.md
└── throughput_checker/ your agent
├── app.py starts the agent
├── config.yaml its settings: port, LLM, agents it calls
├── agent.json its agent card
└── src/
├── graph.py its logic
└── llm_clients/
└── checker_client.py its instructions for the LLM

There are also a few hidden files (.gitignore, .dockerignore, .python-version) and some __init__.py files. You don't need to change them.

Open pyproject.toml and check the dependencies line. It must be:

pyproject.toml: the dependencies (click to open)
dependencies = [
"bat-adk[openai,telemetry]==2026.10"
]

If it is different, change it to match.

Part 4: Configure the agent​

This lab uses OpenAI. If you use another LLM, also make the changes listed in Using another LLM.

Step 1: The settings: throughput_checker/config.yaml​

Replace the content of throughput_checker/config.yaml with:

throughput_checker/config.yaml (click to open)
# This agent's own config. `make throughput_checker` reads this file.
agent_card: throughput_checker/agent.json

endpoint:
url: http://localhost
port: 9900

model:
provider: openai
name: gpt-6-luna
reasoning_effort: low

checkpoints: false

telemetry:
output: []
PartMeaning
agent_cardWhere the agent card is.
endpointWhere your agent listens: http://localhost, port 9900.
modelThe LLM your agent uses. It is the same LLM as in the fabric of Lab 1.
checkpoints: falseThe agent does not remember previous questions: each question is answered on its own.
telemetryTracing is off on your computer (output: []). In the cluster, the AIFabric turns it on. See Phoenix.

Step 2: The secrets: .env​

Open the file .env at the root of the blueprint (in tutorial/, not in throughput_checker/) and replace its content with:

.env (click to open)
OPENAI_API_KEY=<YOUR_API_KEY>
TARGET_DL_MBPS=50
  • OPENAI_API_KEY: your OpenAI key. It stays on your computer: it is never put in the container image.
  • TARGET_DL_MBPS: the downlink throughput target, in Mbps.

Step 3: The agent card: throughput_checker/agent.json​

The agent card tells other agents, and the Supervisor, what your agent does. Replace the content of throughput_checker/agent.json with:

throughput_checker/agent.json (click to open)
{
"name": "Throughput Checker",
"description": "Checks if a network meets its downlink throughput target.\n\nGive it the measured downlink throughput of one or more networks, and it compares each one with the target set by the operator.",
"version": "1.0.0",
"capabilities": {
"streaming": true,
"pushNotifications": false
},
"defaultInputModes": ["text"],
"defaultOutputModes": ["text"],
"skills": [
{
"id": "throughput-target-check",
"name": "Throughput target check",
"description": "Compare a measured downlink throughput with the target.",
"examples": [
"Network <network-name> has 42 Mbps of downlink throughput. Does it meet the target?",
"Is 120 Mbps of downlink throughput enough for the target?"
],
"tags": ["throughput", "target", "5G"]
}
]
}
  • description: one short sentence first, then the details. The Supervisor reads the whole card to decide when to call your agent, so be clear.
  • skills and examples: what the agent can do, with example questions. <network-name> is a placeholder: MX-UI lets you fill it in.

Part 5: Write the code​

Step 1: The tool: tools.py​

Create a new file, throughput_checker/src/tools.py, with this content:

throughput_checker/src/tools.py (click to open)
from langchain_core.tools import tool
import os


@tool
def check_target(
measured_mbps: float,
) -> str:
"""Compare a measured downlink throughput, in Mbps, with the target."""
target = float(os.getenv("TARGET_DL_MBPS", "50"))
verdict = "MEETS TARGET" if measured_mbps >= target else "BELOW TARGET"
return f"{verdict}: {measured_mbps} Mbps measured, target {target} Mbps"
  • @tool turns the function into a tool the LLM can call.
  • The LLM reads the function's name, its parameters and its docstring (the text between """) to know what the tool does and when to call it. Write them clearly.
  • The target comes from the environment variable TARGET_DL_MBPS (your .env file), or is 50.

Step 2: The LLM client: checker_client.py​

The LLM client holds the instructions your agent gives the LLM. Replace the content of throughput_checker/src/llm_clients/checker_client.py with:

throughput_checker/src/llm_clients/checker_client.py (click to open)
from bat.chat_model_client import ChatModelClient, ChatModelClientConfig


class CheckerClient(ChatModelClient):
SYSTEM_INSTRUCTIONS = (
"You check whether 5G networks meet their downlink throughput "
"target.\n"
"The text you get gives the measured downlink throughput of one or "
"more networks.\n"
"For each network, call the tool check_target with its measured "
"value, in Mbps.\n"
"Then answer in at most 5 short lines, one per network, with the "
"result of the tool.\n"
"If the text gives no downlink throughput number, do not call the "
"tool: answer UNKNOWN and say why.\n"
"Never compare the numbers yourself, and never invent a number."
)

def __init__(
self,
tools,
):
super().__init__(
system_instructions=self.SYSTEM_INSTRUCTIONS,
chat_model_config=ChatModelClientConfig.load(
client_name="CheckerClient"
),
tools=tools,
)
  • SYSTEM_INSTRUCTIONS tells the LLM what to do, when to call the tool, and how to answer. Clear rules give answers that are short and always in the same shape.
  • ChatModelClientConfig.load reads the model part of your config.yaml, so the client knows which LLM to call.
  • tools are the tools the LLM may call. You pass them in Step 3.

Step 3: The logic: graph.py​

An agent built with the BAT ADK is a graph: a set of steps (nodes) connected by arrows (edges). Each request goes through the graph from START to END. Your graph has one node, the ReAct loop:

START ──â–ē judge ──â–ē END

Replace the content of throughput_checker/src/graph.py with:

throughput_checker/src/graph.py (click to open)
from .llm_clients.checker_client import CheckerClient
from .tools import check_target

from bat.agent import AgentGraph, AgentState, AgentTaskResult, AgentTaskStatus
from bat.prebuilt import ReActLoop
from langgraph.graph import END, START
from typing import Optional, Self
from typing_extensions import override


class ThroughputCheckerAgentState(AgentState):
query: str
response: Optional[str] = None

@classmethod
@override
def from_query(
cls,
query: str,
) -> Self:
return cls(query=query)

@override
def to_task_result(
self,
) -> AgentTaskResult:
return AgentTaskResult(
task_status=(
AgentTaskStatus.AGENT_TASK_STATUS_COMPLETED
if self.response
else AgentTaskStatus.AGENT_TASK_STATUS_WORKING
),
content=self.response or "Generating response...",
)


class ThroughputCheckerAgentGraph(AgentGraph):
@override
def setup(
self,
config,
) -> None:
self.checker_client = CheckerClient(
tools=[check_target],
)
self.judge = ReActLoop(
config=config,
StateType=ThroughputCheckerAgentState,
loop_name="judge",
chat_model_client=self.checker_client,
input_key="query",
output_key="response",
)

self.graph_builder.add_node("judge", self.judge.as_runnable())
self.graph_builder.add_edge(START, "judge")
self.graph_builder.add_edge("judge", END)

What the code does:

The state (ThroughputCheckerAgentState) is the memory of one request. Every node reads it and writes to it.

FieldHolds
queryThe question the user sent.
responseThe final answer of your agent.
  • from_query creates the state when a question arrives.
  • to_task_result tells BAT when the answer is ready: as long as response is empty, the agent is still working; once it is set, the task is completed and response is sent back.

The graph (ThroughputCheckerAgentGraph) is built in setup, once, when the agent starts:

  • self.checker_client is the LLM client of Step 2, with the tool check_target.
  • self.judge is a ReActLoop, a ready-made node of the BAT ADK. It gives the text in query to the LLM, runs the tools the LLM asks for, gives their results back to the LLM, and writes the final answer in response.
  • add_node adds the node, and add_edge connects it: START → judge → END.

Part 6: Run and test​

Step 1: Start the agent​

In the tutorial/ folder, run:

make throughput_checker

The first time, uv downloads Python and the packages: it takes a minute or two. You should see, among other lines:

INFO     ... - Agent configuration loaded [checkpoints=False] [#agents=0] [#mcp=0]
INFO ... - Loading AgentCard from 'throughput_checker/agent.json'
INFO ... - ChatModelClient CheckerClient initialized with: model=openai:gpt-6-luna, #tools=1
INFO: Uvicorn running on http://0.0.0.0:9900 (Press CTRL+C to quit)
  • #tools=1: the LLM client has your tool.
  • Uvicorn running on http://0.0.0.0:9900: your agent is ready on port 9900.

Keep this terminal open. To stop the agent, press Ctrl+C.

Step 2: Check that it answers​

Open another terminal:

curl http://localhost:9900/ping

You should see:

"pong"

Then look at its agent card:

curl http://localhost:9900/.well-known/agent-card.json

You should see your card, starting with {"name":"Throughput Checker", ....

Step 3: Ask a question​

Agents talk with the A2A protocol: a JSON message sent over HTTP. Send your question with curl:

curl -s -N http://localhost:9900/ \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "SendStreamingMessage",
"params": {
"message": {
"message_id": "1",
"role": "ROLE_USER",
"parts": [{"text": "Network o-ran has 42 Mbps of downlink throughput. Does it meet the target?"}]
}
}
}' | grep -o '"text": "[^"]*"'

You should see three lines:

"text": "Network o-ran has 42 Mbps of downlink throughput. Does it meet the target?"
"text": "Generating response..."
"text": "o-ran: BELOW TARGET — 42.0 Mbps measured; target is 50.0 Mbps."
  1. your question;
  2. a progress message, sent while the agent works;
  3. the final answer. The LLM's wording can change a little from one run to the next.

Now try these questions in the same command, and see how the LLM decides when to call the tool:

  • Network a has 30 Mbps and network b has 80 Mbps of downlink throughput. Which ones meet the target? The LLM calls the tool twice, once per network, and answers one line per network.
  • Does network o-ran meet its throughput target? There is no number, so the LLM does not call the tool. It answers UNKNOWN.

Step 4: Change the target​

Open .env, set TARGET_DL_MBPS=40, stop the agent with Ctrl+C and start it again with make throughput_checker. Ask the first question again: the answer becomes MEETS TARGET. The rule changed, while the LLM and its instructions stayed the same.

💡 Optional: watch the ReAct loop. With Phoenix, a trace viewer, you can see each step of your agent. For the first question, the trace shows the loop: an LLM call that asks for check_target, the tool call, and a second LLM call that writes the answer. To set it up, see Phoenix: an agent on your computer.

If something goes wrong​

What you seeWhat to do
bat: command not foundRun uv tool update-shell, then close your terminal and open a new one. If it still fails, run uv tool install bat-cli==2026.10 again.
make throughput_checker stops with Missing credentials ... OPENAI_API_KEYYour API key is missing. Check the .env file at the root of tutorial/ (Part 4, Step 2).
make throughput_checker stops with No module named ...toolsThe file tools.py is missing or in the wrong folder. It must be in throughput_checker/src/ (Part 5, Step 1).
address already in useAnother program uses port 9900. Stop it, or change port in config.yaml and use the new port in your curl commands.
The answer is Agent execution failed (...)Read the error in the terminal where the agent runs. It is often the API key or the network connection.
The answer compares the numbers, but the result looks wrongCheck TARGET_DL_MBPS in .env, and restart the agent after each change.
Still stuck?Contact BubbleRAN customer support through your usual support channel.

What you learned​

  • bat init blueprint and bat add agent create a blueprint and an agent, ready to run.
  • An agent has settings (config.yaml), secrets (.env) and an agent card (agent.json).
  • With the BAT ADK, the logic is a graph of nodes that share a state.
  • A tool is a Python function the LLM can call. A ReActLoop lets the LLM decide when to call it. Rules go in tools; language goes to the LLM.
  • make <agent> runs an agent, and curl talks to it with the A2A protocol.

In Lab 3, your agent stops asking you for the number: it asks the KPI agent running in your cluster.