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

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-lab of 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
  1. 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.
  2. 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 -L option carries port 9310 of 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 that kubectl works there:

    kubectl get nodes

    You 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
  • 9900 is the port every agent listens on in the cluster; brc finds it from the Service.
  • 9310 is the port you use on your side. Any free port works; the lab uses 9310.

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 to config.yaml:

mcp-servers:
- name: victoria-metrics
url: http://localhost:8085/mcp

If 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:

FieldHolds
queryThe question the user sent.
kpi_questionThe question your agent sends to the KPI agent.
kpi_answerThe answer of the KPI agent.
kpi_status, kpi_content, kpi_needs_inputProgress of the call to the KPI agent. CallAgentNode needs them; you don't use them.
responseThe 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_kpi is a CallAgentNode, a ready-made node of the BAT ADK. It sends the text in kpi_question to the agent named kpi (as written in config.yaml), waits for its answer and writes it in kpi_answer.
  • self.judge is the same ReActLoop as in Lab 2. Only its input_key changed: it now reads kpi_answer instead of query.
  • 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 seeWhat 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 nodekubectl has no working kubeconfig there. Ask your cluster administrator.
brc expose says ✘ <kpi-service> not installedThe Service name is wrong. Copy it again from kubectl get orelem -n trirematics -o wide.
brc expose says ✘ <kpi-service> not readyThe KPI agent is not running yet. Check it with kubectl get pods -n trirematics, then try again.
brc expose says cannot reach the clusterbrc has no working kubeconfig on this machine. Run it where kubectl get nodes works.
The answer is UNKNOWNThe 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-agents in config.yaml lists 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.