Skip to content

Commit f8eb68c

Browse files
vertex-sdk-botcopybara-github
authored andcommitted
feat: Forward per-request labels to RunConfig in streaming_agent_run_with_events
PiperOrigin-RevId: 955431145
1 parent eb75e14 commit f8eb68c

6 files changed

Lines changed: 207 additions & 0 deletions

File tree

agentplatform/agent_engines/templates/adk.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,11 @@ def __init__(self, **kwargs):
236236
)
237237
# The session ID.
238238

239+
self.labels: Optional[Dict[str, str]] = kwargs.get("labels")
240+
# Per-request user labels (e.g. for billing/attribution) to attach to
241+
# the RunConfig for this invocation, so they are propagated onto the
242+
# downstream Vertex API requests.
243+
239244

240245
class _StreamingRunResponse:
241246
"""Response object for `streaming_agent_run_with_events` method.
@@ -1414,12 +1419,23 @@ async def streaming_agent_run_with_events(self, request_json: str):
14141419

14151420
# Run the agent
14161421
message_for_agent = types.Content(**request.message)
1422+
# Propagate per-request user labels (e.g. billing/attribution) onto a
1423+
# RunConfig so the ADK flow forwards them to downstream Vertex requests.
1424+
# Only attach them if the installed google-adk RunConfig supports the
1425+
# `labels` field; older versions forbid unknown fields.
1426+
run_config = None
1427+
if request.labels:
1428+
from google.adk.agents.run_config import RunConfig
1429+
1430+
if "labels" in RunConfig.model_fields:
1431+
run_config = RunConfig(labels=request.labels)
14171432
try:
14181433
async for event in runner.run_async(
14191434
user_id=request.user_id,
14201435
session_id=session.id,
14211436
new_message=message_for_agent,
14221437
state_delta=state_delta,
1438+
run_config=run_config,
14231439
):
14241440
converted_event = await self._convert_response_events(
14251441
user_id=request.user_id,

tests/unit/agentplatform/frameworks/test_frameworks_adk.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,67 @@ async def test_streaming_agent_run_with_events(
655655
events.append(event)
656656
assert len(events) == 1
657657

658+
@pytest.mark.asyncio
659+
async def test_streaming_agent_run_with_events_propagates_labels(
660+
self,
661+
default_instrumentor_builder_mock: mock.Mock,
662+
get_project_id_mock: mock.Mock,
663+
):
664+
from google.adk.agents.run_config import RunConfig
665+
666+
if "labels" not in RunConfig.model_fields:
667+
pytest.skip("Installed google-adk RunConfig does not support labels.")
668+
669+
app = adk_template.AdkApp(agent=_TEST_AGENT)
670+
app.set_up()
671+
672+
# Pre-create a session in the real in-memory session service.
673+
await app.async_create_session(
674+
user_id=_TEST_USER_ID, session_id="test_session_id"
675+
)
676+
677+
runner_mock = mock.Mock()
678+
679+
async def mock_run_async(*args, **kwargs):
680+
from google.adk.events import event
681+
682+
yield event.Event(
683+
**{
684+
"author": "currency_exchange_agent",
685+
"content": {"parts": [{"text": "Sweden"}], "role": "model"},
686+
"id": "9aaItGK9",
687+
"invocation_id": "e-6543c213-6417-484b-9551-b67915d1d5f7",
688+
}
689+
)
690+
691+
spy = mock.MagicMock(side_effect=mock_run_async)
692+
runner_mock.run_async = spy
693+
app._tmpl_attrs["runner"] = runner_mock
694+
695+
labels = {"goog-originating-logical-product-id": "prod1"}
696+
request_json = json.dumps(
697+
{
698+
"user_id": _TEST_USER_ID,
699+
"session_id": "test_session_id",
700+
"message": {
701+
"parts": [{"text": "What is the exchange rate from USD to SEK?"}],
702+
"role": "user",
703+
},
704+
"labels": labels,
705+
}
706+
)
707+
708+
async for _ in app.streaming_agent_run_with_events(
709+
request_json=request_json,
710+
):
711+
pass
712+
713+
# The labels are surfaced to run_async via a RunConfig.
714+
spy.assert_called_once()
715+
run_config = spy.call_args.kwargs["run_config"]
716+
assert run_config is not None
717+
assert run_config.labels == labels
718+
658719
@pytest.mark.asyncio
659720
@mock.patch.dict(
660721
os.environ,

tests/unit/vertex_adk/test_agent_engine_templates_adk.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,8 +572,70 @@ async def mock_run_async(*args, **kwargs):
572572
session_id="test_session_id",
573573
new_message=mock.ANY,
574574
state_delta={"test_user_id1": "test_access_token"},
575+
run_config=None,
575576
)
576577

578+
@pytest.mark.asyncio
579+
async def test_streaming_agent_run_with_events_propagates_labels(
580+
self,
581+
default_instrumentor_builder_mock: mock.Mock,
582+
get_project_id_mock: mock.Mock,
583+
):
584+
from google.adk.agents.run_config import RunConfig
585+
586+
if "labels" not in RunConfig.model_fields:
587+
pytest.skip("Installed google-adk RunConfig does not support labels.")
588+
589+
app = agent_engines.AdkApp(agent=_TEST_AGENT)
590+
app.set_up()
591+
592+
# Pre-create a session in the real in-memory session service.
593+
await app.async_create_session(
594+
user_id=_TEST_USER_ID, session_id="test_session_id"
595+
)
596+
597+
runner_mock = mock.Mock()
598+
599+
async def mock_run_async(*args, **kwargs):
600+
from google.adk.events import event
601+
602+
yield event.Event(
603+
**{
604+
"author": "currency_exchange_agent",
605+
"content": {"parts": [{"text": "Sweden"}], "role": "model"},
606+
"id": "9aaItGK9",
607+
"invocation_id": "e-6543c213-6417-484b-9551-b67915d1d5f7",
608+
}
609+
)
610+
611+
spy = mock.MagicMock(side_effect=mock_run_async)
612+
runner_mock.run_async = spy
613+
app._tmpl_attrs["runner"] = runner_mock
614+
615+
labels = {"goog-originating-logical-product-id": "prod1"}
616+
request_json = json.dumps(
617+
{
618+
"user_id": _TEST_USER_ID,
619+
"session_id": "test_session_id",
620+
"message": {
621+
"parts": [{"text": "What is the exchange rate from USD to SEK?"}],
622+
"role": "user",
623+
},
624+
"labels": labels,
625+
}
626+
)
627+
628+
async for _ in app.streaming_agent_run_with_events(
629+
request_json=request_json,
630+
):
631+
pass
632+
633+
# The labels are surfaced to run_async via a RunConfig.
634+
spy.assert_called_once()
635+
run_config = spy.call_args.kwargs["run_config"]
636+
assert run_config is not None
637+
assert run_config.labels == labels
638+
577639
@pytest.mark.asyncio
578640
@mock.patch.dict(
579641
os.environ,

tests/unit/vertex_adk/test_reasoning_engine_templates_adk.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,42 @@ def test_streaming_agent_run_with_events(self):
601601
events = list(app.streaming_agent_run_with_events(request_json=request_json))
602602
assert len(events) == 1
603603

604+
def test_streaming_agent_run_with_events_propagates_labels(self):
605+
from google.adk.agents.run_config import RunConfig
606+
607+
if "labels" not in RunConfig.model_fields:
608+
pytest.skip("Installed google-adk RunConfig does not support labels.")
609+
610+
captured = {}
611+
612+
class _LabelCapturingRunner(_MockRunner):
613+
def run(self, *args, **kwargs):
614+
captured["run_config"] = kwargs.get("run_config")
615+
yield from super().run(*args, **kwargs)
616+
617+
app = reasoning_engines.AdkApp(
618+
agent=Agent(name=_TEST_AGENT_NAME, model=_TEST_MODEL)
619+
)
620+
app.set_up()
621+
app._tmpl_attrs["in_memory_runner"] = _LabelCapturingRunner()
622+
623+
labels = {"goog-originating-logical-product-id": "prod1"}
624+
request_json = json.dumps(
625+
{
626+
"user_id": _TEST_USER_ID,
627+
"message": {
628+
"parts": [{"text": "What is the exchange rate from USD to SEK?"}],
629+
"role": "user",
630+
},
631+
"labels": labels,
632+
}
633+
)
634+
events = list(app.streaming_agent_run_with_events(request_json=request_json))
635+
636+
assert len(events) == 1
637+
assert captured["run_config"] is not None
638+
assert captured["run_config"].labels == labels
639+
604640
@pytest.mark.asyncio
605641
@mock.patch.dict(
606642
os.environ,

vertexai/agent_engines/templates/adk.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,11 @@ def __init__(self, **kwargs):
220220
)
221221
# The session ID.
222222

223+
self.labels: Optional[Dict[str, str]] = kwargs.get("labels")
224+
# Per-request user labels (e.g. for billing/attribution) to attach to
225+
# the RunConfig for this invocation, so they are propagated onto the
226+
# downstream Vertex API requests.
227+
223228

224229
class _StreamingRunResponse:
225230
"""Response object for `streaming_agent_run_with_events` method.
@@ -1374,12 +1379,23 @@ async def streaming_agent_run_with_events(self, request_json: str):
13741379

13751380
# Run the agent
13761381
message_for_agent = types.Content(**request.message)
1382+
# Propagate per-request user labels (e.g. billing/attribution) onto a
1383+
# RunConfig so the ADK flow forwards them to downstream Vertex requests.
1384+
# Only attach them if the installed google-adk RunConfig supports the
1385+
# `labels` field; older versions forbid unknown fields.
1386+
run_config = None
1387+
if request.labels:
1388+
from google.adk.agents.run_config import RunConfig
1389+
1390+
if "labels" in RunConfig.model_fields:
1391+
run_config = RunConfig(labels=request.labels)
13771392
try:
13781393
async for event in runner.run_async(
13791394
user_id=request.user_id,
13801395
session_id=session.id,
13811396
new_message=message_for_agent,
13821397
state_delta=state_delta,
1398+
run_config=run_config,
13831399
):
13841400
converted_event = await self._convert_response_events(
13851401
user_id=request.user_id,

vertexai/preview/reasoning_engines/templates/adk.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,11 @@ def __init__(self, **kwargs):
223223
)
224224
# The session ID.
225225

226+
self.labels: Optional[Dict[str, str]] = kwargs.get("labels")
227+
# Per-request user labels (e.g. for billing/attribution) to attach to
228+
# the RunConfig for this invocation, so they are propagated onto the
229+
# downstream Vertex API requests.
230+
226231

227232
class _StreamingRunResponse:
228233
"""Response object for `streaming_agent_run_with_events` method.
@@ -1196,11 +1201,22 @@ async def _invoke_agent_async():
11961201
raise RuntimeError("Session initialization failed.")
11971202
# Run the agent.
11981203
message_for_agent = types.Content(**request.message)
1204+
# Propagate per-request user labels (e.g. billing/attribution) onto a
1205+
# RunConfig so the ADK flow forwards them to downstream Vertex
1206+
# requests. Only attach them if the installed google-adk RunConfig
1207+
# supports the `labels` field; older versions forbid unknown fields.
1208+
run_config = None
1209+
if request.labels:
1210+
from google.adk.agents.run_config import RunConfig
1211+
1212+
if "labels" in RunConfig.model_fields:
1213+
run_config = RunConfig(labels=request.labels)
11991214
try:
12001215
for event in runner.run(
12011216
user_id=request.user_id,
12021217
session_id=session.id,
12031218
new_message=message_for_agent,
1219+
run_config=run_config,
12041220
):
12051221
converted_event = await self._convert_response_events(
12061222
user_id=request.user_id,

0 commit comments

Comments
 (0)