Skip to content

Commit bc08f46

Browse files
haiyuan-eng-googlecopybara-github
authored andcommitted
fix(plugins): write BigQuery analytics rows when invocation agent is None
Workflow-driven invocations with deterministic nodes leave InvocationContext.agent as None, so reading ReadonlyContext.agent_name raised AttributeError and BigQueryAgentAnalyticsPlugin silently dropped the event row. Resolve the agent column defensively (running agent name, else the source Event.author, else null) so rows are written regardless of agent being None. Co-authored-by: Haiyuan Cao <haiyuan@google.com> PiperOrigin-RevId: 930097637
1 parent a890399 commit bc08f46

2 files changed

Lines changed: 115 additions & 1 deletion

File tree

src/google/adk/plugins/bigquery_agent_analytics_plugin.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2937,6 +2937,33 @@ def _extract_latency(
29372937
latency_json["time_to_first_token_ms"] = event_data.time_to_first_token_ms
29382938
return latency_json or None
29392939

2940+
@staticmethod
2941+
def _resolve_agent_label(
2942+
callback_context: CallbackContext,
2943+
source_event: Optional["Event"],
2944+
) -> Optional[str]:
2945+
"""Resolves the ``agent`` column without raising when no agent is set.
2946+
2947+
``CallbackContext.agent_name`` dereferences
2948+
``InvocationContext.agent.name`` with no None guard, but ``agent`` is
2949+
legitimately ``None`` for workflow-driven invocations with deterministic
2950+
nodes. Reading it at row-build time then raised ``AttributeError``, which
2951+
``@_safe_callback`` swallowed, silently dropping the row (issue #6063).
2952+
2953+
Resolution order:
2954+
2955+
* running agent present → ``agent.name``;
2956+
* no agent but a source Event → ``Event.author`` (the emitting node), a
2957+
more meaningful workflow label than a sentinel;
2958+
* callback-only row with neither → ``None`` (SQL NULL).
2959+
"""
2960+
agent = getattr(callback_context._invocation_context, "agent", None)
2961+
if agent is not None:
2962+
return getattr(agent, "name", None)
2963+
if source_event is not None:
2964+
return getattr(source_event, "author", None)
2965+
return None
2966+
29402967
def _build_adk_envelope(
29412968
self,
29422969
callback_context: CallbackContext,
@@ -3189,7 +3216,9 @@ async def _log_event(
31893216
row = {
31903217
"timestamp": timestamp,
31913218
"event_type": event_type,
3192-
"agent": callback_context.agent_name,
3219+
"agent": self._resolve_agent_label(
3220+
callback_context, event_data.source_event
3221+
),
31933222
"user_id": callback_context.user_id,
31943223
"session_id": callback_context.session.id,
31953224
"invocation_id": callback_context.invocation_id,

tests/unittests/plugins/test_bigquery_agent_analytics_plugin.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1802,6 +1802,91 @@ async def test_log_event_with_custom_tags(
18021802
attributes = json.loads(log_entry["attributes"])
18031803
assert attributes["custom_tags"] == custom_tags
18041804

1805+
def test_resolve_agent_label_prefers_running_agent(self, callback_context):
1806+
"""agent present → agent.name, regardless of any source event."""
1807+
event = event_lib.Event(author="WorkflowNodeA")
1808+
label = bigquery_agent_analytics_plugin.BigQueryAgentAnalyticsPlugin._resolve_agent_label(
1809+
callback_context, event
1810+
)
1811+
assert label == "MyTestAgent"
1812+
1813+
def test_resolve_agent_label_falls_back_to_event_author(
1814+
self, callback_context
1815+
):
1816+
"""No agent + source Event → Event.author (the emitting node)."""
1817+
callback_context._invocation_context.agent = None
1818+
event = event_lib.Event(author="WorkflowNodeA")
1819+
label = bigquery_agent_analytics_plugin.BigQueryAgentAnalyticsPlugin._resolve_agent_label(
1820+
callback_context, event
1821+
)
1822+
assert label == "WorkflowNodeA"
1823+
1824+
def test_resolve_agent_label_null_for_callback_only_row(
1825+
self, callback_context
1826+
):
1827+
"""No agent and no source Event → None (SQL NULL)."""
1828+
callback_context._invocation_context.agent = None
1829+
label = bigquery_agent_analytics_plugin.BigQueryAgentAnalyticsPlugin._resolve_agent_label(
1830+
callback_context, None
1831+
)
1832+
assert label is None
1833+
1834+
@pytest.mark.asyncio
1835+
async def test_log_event_survives_none_agent_with_event_author(
1836+
self,
1837+
bq_plugin_inst,
1838+
mock_write_client,
1839+
callback_context,
1840+
dummy_arrow_schema,
1841+
):
1842+
"""Regression for #6063: None agent falls back to source event author."""
1843+
# Workflow-driven invocations leave ``InvocationContext.agent`` as None.
1844+
# Reading ``callback_context.agent_name`` then raised ``AttributeError``,
1845+
# which ``@_safe_callback`` swallowed, silently dropping the BigQuery row.
1846+
# The row must now be written with the source Event's author as the label.
1847+
callback_context._invocation_context.agent = None
1848+
event = event_lib.Event(author="WorkflowNodeA")
1849+
1850+
await bq_plugin_inst._log_event(
1851+
"TEST_EVENT",
1852+
callback_context,
1853+
raw_content="test content",
1854+
event_data=bigquery_agent_analytics_plugin.EventData(
1855+
source_event=event
1856+
),
1857+
)
1858+
await asyncio.sleep(0.01)
1859+
log_entry = await _get_captured_event_dict_async(
1860+
mock_write_client, dummy_arrow_schema
1861+
)
1862+
1863+
assert log_entry["event_type"] == "TEST_EVENT"
1864+
assert log_entry["agent"] == "WorkflowNodeA"
1865+
1866+
@pytest.mark.asyncio
1867+
async def test_log_event_survives_none_agent_without_source_event(
1868+
self,
1869+
bq_plugin_inst,
1870+
mock_write_client,
1871+
callback_context,
1872+
dummy_arrow_schema,
1873+
):
1874+
"""Regression for #6063: callback-only row with no agent writes null."""
1875+
callback_context._invocation_context.agent = None
1876+
1877+
await bq_plugin_inst._log_event(
1878+
"TEST_EVENT",
1879+
callback_context,
1880+
raw_content="test content",
1881+
)
1882+
await asyncio.sleep(0.01)
1883+
log_entry = await _get_captured_event_dict_async(
1884+
mock_write_client, dummy_arrow_schema
1885+
)
1886+
1887+
assert log_entry["event_type"] == "TEST_EVENT"
1888+
assert log_entry["agent"] is None
1889+
18051890
@pytest.mark.asyncio
18061891
async def test_on_model_error_callback_logs_correctly(
18071892
self,

0 commit comments

Comments
 (0)