Lab 3: Connect your agent to the cluster
In Lab 2, you gave the Throughput Checker the measured throughput yourself. In this lab, your agent asks for it: it calls the KPI agent you deployed in Lab 1, and then checks the answer with the same ReAct loop as before. Your agent still runs on your computer, but it uses a real agent of your cluster.
By the end of this lab you will be able to:
- reach an agent that runs in the cluster from your computer, with a port-forward;
- call another agent from your own agent, with a CallAgentNode;
- combine a fixed step and a ReAct loop in one graph.
Before you start, you need:
- the fabric
observability-labof Lab 1, running; - the blueprint
tutorial/of Lab 2, working; - the prerequisites for Labs 2 to 5.
Part 1: The idea
Your computer The cluster
┌──────────────────────────────────┐ ┌─────────────────┐
│ Throughput Checker (port 9900) │ │ │
│ │ │ │
│ 1. ask_kpi ──────────────────────┼──── localhost:9310 ───┼──► KPI agent │
│ │ (tunnel) │ (port 9900) │
│ 2. judge: the ReAct loop of │ └─────────────────┘
│ Lab 2, on the KPI answer │
└──────────────────────────────────┘
Your agent now works in two steps:
START ──► ask_kpi ──► judge ──► END
- ask_kpi: it sends a question to the KPI agent, for example "What is the average downlink throughput, in Mbps, of network o-ran?", and waits for the answer. This step always happens: your code decides it, not the LLM.
- judge: the ReAct loop of Lab 2. It reads the KPI answer instead of
your question, calls
check_target, and answers.
The KPI agent runs inside the cluster. Its address only works inside the
cluster, so your computer cannot reach it directly. A port-forward opens
a tunnel: everything your computer sends to localhost:9310 goes to the KPI
agent. You develop on your computer, but use the real agents of the cluster.
Part 2: Reach the KPI agent from your computer
Step 1: Open a terminal on the cluster
kubectl commands must run where kubectl can talk to the cluster: on the
cluster master node.
-
If you work on the master node. Open a new terminal there. No other setup is needed.
-
If you work on your own computer. Open a new terminal and connect to the master node with SSH. The
-Loption carries port9310of your computer to the master node:ssh -L 9310:localhost:9310 <user>@<master-node>Replace
<user>and<master-node>with your user name and the address of the master node. Run the next commands in this SSH session. Check thatkubectlworks there:kubectl get nodesYou should see the list of your cluster nodes. If you see an error, ask your cluster administrator for a working kubeconfig file on the master node.
Step 2: Find the KPI agent's Service
Every agent of an AIFabric has a Service: its address in the cluster. Find the one of the KPI agent:
kubectl get orelem -n trirematics -o wide
You should see one line per agent of your fabric:
NAME MODEL READY SHADOW-SERVICE
base.supervisor.observability-lab base true tssf54a7590-656b-ff4d-dd8e-bd67f08c2bbdeec8a8cd24877c0c6663ce93
kpi.kpi.observability-lab kpi true tsscca1531e-885c-1ca3-fcef-701b39b503ee292258bc34f19e3e736cb0e1
topology.topology.observability-lab topology true tssbce5b24a-4ee6-fa1b-1187-43bc7742a98dbf06d66b219e085c084fca1f
Each name is <mode>.<agent>.<fabric>. The SHADOW-SERVICE column is the
name of the agent's Service. Copy the one on the kpi.kpi.observability-lab
line.
Step 3: Start the port-forward
Run this on the machine where you develop. Paste the Service name in place
of <kpi-service>:
brc expose <kpi-service> -n trirematics --local-port 9310
You should see:
Exposing 1 service(s), press Ctrl+C to stop.
✔ <kpi-service> ready → http://localhost:9310
9900is the port every agent listens on in the cluster; brc finds it from the Service.9310is the port you use on your side. Any free port works; the lab uses9310.
Keep this terminal open. If you close it or press Ctrl+C, the tunnel
closes. If the KPI agent restarts, brc reconnects to it on its own.
Step 4: Check the tunnel
Open a new terminal where you develop (on your computer, or on the master node if you work there) and run:
curl http://localhost:9310/.well-known/agent-card.json
You should see the agent card of the KPI agent:
{"name":"KPI Agent","description":"Measures how your networks perform: throughput, PRB usage, ...
💡 The same trick works for MCP servers. For example, to develop an agent that reads metrics directly, expose the VictoriaMetrics MCP server in another terminal (
brc expose vmm-victoria-metrics-mcp -n monitoring --local-port 8085) and add it toconfig.yaml:mcp-servers:
- name: victoria-metrics
url: http://localhost:8085/mcpIf brc and the cluster kubeconfig are only on the master node, run brc there and carry the ports to your computer over SSH:
ssh -L 9310:localhost:9310 -L 8085:localhost:8085 <user>@<master-node>.
Part 3: Update the settings and the agent card
Step 1: throughput_checker/config.yaml
Add the KPI agent at the end of the file, so that it looks like this:
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: []
remote-agents:
- name: kpi
url: http://localhost:9310
remote-agents lists the agents yours can call. kpi is reached through
your port-forward, at localhost:9310.
Step 2: throughput_checker/agent.json
Your agent no longer needs the number from the user, so its card changes.
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\nAsks the KPI agent for the downlink throughput of the network named in the request, and compares it 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 the measured downlink throughput of a network with its target.",
"examples": [
"Does <network-name> meet its throughput target?",
"Is the downlink throughput of <network-name> above the target?",
"Which networks are below their throughput target?"
],
"tags": ["throughput", "target", "KPI", "5G"]
}
]
}
Part 4: Update the code
tools.py and checker_client.py don't change: the ReAct loop does the same
job, on a different text. Only graph.py changes. 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 CallAgentNode, ReActLoop
from langgraph.graph import END, START
from typing import Optional, Self
from typing_extensions import override
KPI_QUESTION = (
"What is the average downlink throughput, in Mbps, over the last "
"5 minutes, of the network named in this request: {query}"
)
class ThroughputCheckerAgentState(AgentState):
query: str
kpi_question: str
kpi_answer: Optional[str] = None
kpi_status: Optional[AgentTaskStatus] = None
kpi_content: Optional[str] = None
kpi_needs_input: bool = False
response: Optional[str] = None
@classmethod
@override
def from_query(
cls,
query: str,
) -> Self:
return cls(
query=query,
kpi_question=KPI_QUESTION.format(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.ask_kpi = CallAgentNode(
config=config,
StateType=ThroughputCheckerAgentState,
loop_name="ask_kpi",
agent_name="kpi",
build_message=self.build_message,
input_key="kpi_question",
output_key="kpi_answer",
status_key="kpi_status",
agent_input_required_key="kpi_needs_input",
agent_response_status_key="kpi_status",
agent_response_content_key="kpi_content",
)
self.judge = ReActLoop(
config=config,
StateType=ThroughputCheckerAgentState,
loop_name="judge",
chat_model_client=self.checker_client,
input_key="kpi_answer",
output_key="response",
)
self.graph_builder.add_node("ask_kpi", self.ask_kpi.as_runnable())
self.graph_builder.add_node("judge", self.judge.as_runnable())
self.graph_builder.add_edge(START, "ask_kpi")
self.graph_builder.add_edge("ask_kpi", "judge")
self.graph_builder.add_edge("judge", END)
What changed since Lab 2:
The state has new fields, for the call to the KPI agent:
| Field | Holds |
|---|---|
query | The question the user sent. |
kpi_question | The question your agent sends to the KPI agent. |
kpi_answer | The answer of the KPI agent. |
kpi_status, kpi_content, kpi_needs_input | Progress of the call to the KPI agent. CallAgentNode needs them; you don't use them. |
response | The final answer of your agent. |
from_query now also writes the question for the KPI agent, from the
template KPI_QUESTION.
The graph has a new node before the ReAct loop:
self.ask_kpiis a CallAgentNode, a ready-made node of the BAT ADK. It sends the text inkpi_questionto the agent namedkpi(as written inconfig.yaml), waits for its answer and writes it inkpi_answer.self.judgeis the same ReActLoop as in Lab 2. Only itsinput_keychanged: it now readskpi_answerinstead ofquery.- The edges are now
START → ask_kpi → judge → END.
Part 5: Run and test
Step 1: Start the agent
Check that the port-forward of Part 2 is still running. Then, in the
tutorial/ folder, run:
make throughput_checker
You should see, among other lines:
INFO ... - Agent configuration loaded [checkpoints=False] [#agents=1] [#mcp=0]
INFO: Uvicorn running on http://0.0.0.0:9900 (Press CTRL+C to quit)
#agents=1: your agent found the KPI agent through the tunnel.
Step 2: Ask a question
This time, the question has no number: your agent gets it from the KPI agent.
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": "Does network o-ran meet its throughput target?"}]
}
}
}' | grep -o '"text": "[^"]*"'
The answer can take up to a minute: your agent waits for the KPI agent, then for the LLM. You should see:
"text": "Does network o-ran meet its throughput target?"
"text": "Generating response..."
"text": "o-ran: BELOW TARGET — 42.3 Mbps measured, target 50.0 Mbps."
Your numbers depend on your network. If the throughput is close to 0 Mbps,
create traffic from the master node with
brc test throughput ue1 dl gateway -- --time 60 and ask again.
In Lab 2, the same question got UNKNOWN, because it had no number. Now your agent gets the number from the KPI agent.
💡 Optional: if you use Phoenix, the trace of this question now also shows the call to the KPI agent, before the ReAct loop.
If something goes wrong
| What you see | What to do |
|---|---|
make throughput_checker stops with Network communication error fetching agent card from http://localhost:9310/... | The tunnel to the KPI agent is not running. Start it again (Part 2, Steps 1 and 3), then start the agent. |
kubectl get nodes fails on the master node | kubectl has no working kubeconfig there. Ask your cluster administrator. |
brc expose says ✘ <kpi-service> not installed | The Service name is wrong. Copy it again from kubectl get orelem -n trirematics -o wide. |
brc expose says ✘ <kpi-service> not ready | The KPI agent is not running yet. Check it with kubectl get pods -n trirematics, then try again. |
brc expose says cannot reach the cluster | brc has no working kubeconfig on this machine. Run it where kubectl get nodes works. |
The answer is UNKNOWN | The KPI agent gave no throughput number. Check the network name, and ask the KPI agent directly in MX-UI to see its answer. |
The answer is Agent execution failed (...) | Read the error in the terminal where the agent runs. It is often the tunnel, the API key or the network connection. |
| Still stuck? | Contact BubbleRAN customer support through your usual support channel. |
What you learned
- A port-forward lets the agent on your computer use the real agents of the cluster, so you can develop and test without deploying.
remote-agentsinconfig.yamllists the agents yours can call.- A CallAgentNode calls another agent and stores its answer in the state.
- A graph can mix fixed steps, which your code decides, and a ReAct loop, where the LLM decides.
In Lab 4 you put this agent in the cluster, next to the agents of Lab 1.