mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
This change introduces a shutdown lifecycle hook for plugins. The `PluginManager` now has an `async def shutdown()` method that will call `await plugin.shutdown()` on any registered plugins that implement the method. This is called from `Runner.close()`, allowing plugins to perform cleanup tasks like flushing logs or closing connections when the runner instance is being closed. This improves the reliability of plugins that perform background operations. PiperOrigin-RevId: 831037737
754 lines
25 KiB
Python
754 lines
25 KiB
Python
# Copyright 2025 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import asyncio
|
|
import datetime
|
|
import json
|
|
import logging
|
|
from unittest import mock
|
|
|
|
from google.adk.agents import base_agent
|
|
from google.adk.agents import callback_context as callback_context_lib
|
|
from google.adk.agents import invocation_context as invocation_context_lib
|
|
from google.adk.events import event as event_lib
|
|
from google.adk.models import llm_request as llm_request_lib
|
|
from google.adk.models import llm_response as llm_response_lib
|
|
from google.adk.plugins import bigquery_agent_analytics_plugin
|
|
from google.adk.plugins import plugin_manager as plugin_manager_lib
|
|
from google.adk.sessions import base_session_service as base_session_service_lib
|
|
from google.adk.sessions import session as session_lib
|
|
from google.adk.tools import base_tool as base_tool_lib
|
|
from google.adk.tools import tool_context as tool_context_lib
|
|
import google.auth
|
|
from google.auth import exceptions as auth_exceptions
|
|
import google.auth.credentials
|
|
from google.cloud import bigquery
|
|
from google.cloud.bigquery_storage_v1 import types as bq_storage_types
|
|
from google.genai import types
|
|
import pyarrow as pa
|
|
import pytest
|
|
|
|
BigQueryLoggerConfig = bigquery_agent_analytics_plugin.BigQueryLoggerConfig
|
|
|
|
PROJECT_ID = "test-gcp-project"
|
|
DATASET_ID = "adk_logs"
|
|
TABLE_ID = "agent_events"
|
|
DEFAULT_STREAM_NAME = (
|
|
f"projects/{PROJECT_ID}/datasets/{DATASET_ID}/tables/{TABLE_ID}/_default"
|
|
)
|
|
|
|
# --- Pytest Fixtures ---
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_session():
|
|
mock_s = mock.create_autospec(
|
|
session_lib.Session, instance=True, spec_set=True
|
|
)
|
|
type(mock_s).id = mock.PropertyMock(return_value="session-123")
|
|
type(mock_s).user_id = mock.PropertyMock(return_value="user-456")
|
|
type(mock_s).app_name = mock.PropertyMock(return_value="test_app")
|
|
type(mock_s).state = mock.PropertyMock(return_value={})
|
|
return mock_s
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_agent():
|
|
mock_a = mock.create_autospec(
|
|
base_agent.BaseAgent, instance=True, spec_set=True
|
|
)
|
|
# Mock the 'name' property
|
|
type(mock_a).name = mock.PropertyMock(return_value="MyTestAgent")
|
|
return mock_a
|
|
|
|
|
|
@pytest.fixture
|
|
def invocation_context(mock_agent, mock_session):
|
|
mock_session_service = mock.create_autospec(
|
|
base_session_service_lib.BaseSessionService, instance=True, spec_set=True
|
|
)
|
|
mock_plugin_manager = mock.create_autospec(
|
|
plugin_manager_lib.PluginManager, instance=True, spec_set=True
|
|
)
|
|
return invocation_context_lib.InvocationContext(
|
|
agent=mock_agent,
|
|
session=mock_session,
|
|
invocation_id="inv-789",
|
|
session_service=mock_session_service,
|
|
plugin_manager=mock_plugin_manager,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def callback_context(invocation_context):
|
|
return callback_context_lib.CallbackContext(
|
|
invocation_context=invocation_context
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def tool_context(invocation_context):
|
|
return tool_context_lib.ToolContext(invocation_context=invocation_context)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_auth_default():
|
|
mock_creds = mock.create_autospec(
|
|
google.auth.credentials.Credentials, instance=True, spec_set=True
|
|
)
|
|
with mock.patch.object(
|
|
google.auth,
|
|
"default",
|
|
autospec=True,
|
|
return_value=(mock_creds, PROJECT_ID),
|
|
) as mock_auth:
|
|
yield mock_auth
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_bq_client():
|
|
with mock.patch.object(bigquery, "Client", autospec=True) as mock_cls:
|
|
yield mock_cls.return_value
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_write_client():
|
|
with mock.patch.object(
|
|
bigquery_agent_analytics_plugin, "BigQueryWriteAsyncClient", autospec=True
|
|
) as mock_cls:
|
|
mock_client = mock_cls.return_value
|
|
mock_client.transport = mock.AsyncMock()
|
|
|
|
async def fake_append_rows(requests, **kwargs):
|
|
# This function is now async, so `await client.append_rows` works.
|
|
mock_append_rows_response = mock.MagicMock()
|
|
mock_append_rows_response.row_errors = []
|
|
mock_append_rows_response.error = mock.MagicMock()
|
|
mock_append_rows_response.error.code = 0 # OK status
|
|
# This a gen is what's returned *after* the await.
|
|
return _async_gen(mock_append_rows_response)
|
|
|
|
mock_client.append_rows.side_effect = fake_append_rows
|
|
yield mock_client
|
|
|
|
|
|
@pytest.fixture
|
|
def dummy_arrow_schema():
|
|
return pa.schema([
|
|
pa.field("timestamp", pa.timestamp("us", tz="UTC")),
|
|
pa.field("event_type", pa.string()),
|
|
pa.field("agent", pa.string()),
|
|
pa.field("session_id", pa.string()),
|
|
pa.field("invocation_id", pa.string()),
|
|
pa.field("user_id", pa.string()),
|
|
pa.field("content", pa.string()),
|
|
pa.field("error_message", pa.string()),
|
|
])
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_to_arrow_schema(dummy_arrow_schema):
|
|
with mock.patch.object(
|
|
bigquery_agent_analytics_plugin,
|
|
"to_arrow_schema",
|
|
autospec=True,
|
|
return_value=dummy_arrow_schema,
|
|
) as mock_func:
|
|
yield mock_func
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_asyncio_to_thread():
|
|
async def fake_to_thread(func, *args, **kwargs):
|
|
return func(*args, **kwargs)
|
|
|
|
with mock.patch(
|
|
"asyncio.to_thread", side_effect=fake_to_thread
|
|
) as mock_async:
|
|
yield mock_async
|
|
|
|
|
|
@pytest.fixture
|
|
async def bq_plugin_inst(
|
|
mock_auth_default,
|
|
mock_bq_client,
|
|
mock_write_client,
|
|
mock_to_arrow_schema,
|
|
mock_asyncio_to_thread,
|
|
):
|
|
plugin = bigquery_agent_analytics_plugin.BigQueryAgentAnalyticsPlugin(
|
|
project_id=PROJECT_ID,
|
|
dataset_id=DATASET_ID,
|
|
table_id=TABLE_ID,
|
|
)
|
|
await plugin._ensure_init() # Ensure clients are initialized
|
|
mock_write_client.append_rows.reset_mock()
|
|
return plugin
|
|
|
|
|
|
# --- Helper Functions ---
|
|
|
|
|
|
async def _async_gen(val):
|
|
yield val
|
|
|
|
|
|
def _get_captured_event_dict(mock_write_client, expected_schema):
|
|
"""Helper to get the event_dict passed to append_rows."""
|
|
mock_write_client.append_rows.assert_called_once()
|
|
call_args = mock_write_client.append_rows.call_args
|
|
requests_iter = call_args.args[0]
|
|
requests = list(requests_iter)
|
|
assert len(requests) == 1
|
|
request = requests[0]
|
|
assert request.write_stream == DEFAULT_STREAM_NAME
|
|
|
|
arrow_rows = request.arrow_rows
|
|
message = pa.ipc.read_message(arrow_rows.rows.serialized_record_batch)
|
|
batch = pa.ipc.read_record_batch(message, schema=expected_schema)
|
|
table = pa.Table.from_batches([batch])
|
|
assert table.schema.equals(
|
|
expected_schema
|
|
), f"Schema mismatch: Expected {expected_schema}, got {table.schema}"
|
|
pydict = table.to_pydict()
|
|
return {k: v[0] for k, v in pydict.items()}
|
|
|
|
|
|
def _assert_common_fields(log_entry, event_type, agent="MyTestAgent"):
|
|
assert log_entry["event_type"] == event_type
|
|
assert log_entry["agent"] == agent
|
|
assert log_entry["session_id"] == "session-123"
|
|
assert log_entry["invocation_id"] == "inv-789"
|
|
assert log_entry["user_id"] == "user-456"
|
|
assert "timestamp" in log_entry
|
|
assert isinstance(log_entry["timestamp"], datetime.datetime)
|
|
|
|
|
|
# --- Test Class ---
|
|
|
|
|
|
class TestBigQueryAgentAnalyticsPlugin:
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_plugin_disabled(
|
|
self,
|
|
mock_auth_default,
|
|
mock_bq_client,
|
|
mock_write_client,
|
|
invocation_context,
|
|
):
|
|
config = BigQueryLoggerConfig(enabled=False)
|
|
plugin = bigquery_agent_analytics_plugin.BigQueryAgentAnalyticsPlugin(
|
|
project_id=PROJECT_ID,
|
|
dataset_id=DATASET_ID,
|
|
table_id=TABLE_ID,
|
|
config=config,
|
|
)
|
|
# user_message = types.Content(parts=[types.Part(text="Test")])
|
|
|
|
await plugin.on_user_message_callback(
|
|
invocation_context=invocation_context,
|
|
user_message=types.Content(parts=[types.Part(text="Test")]),
|
|
)
|
|
mock_auth_default.assert_not_called()
|
|
mock_bq_client.assert_not_called()
|
|
mock_write_client.append_rows.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_event_allowlist(
|
|
self,
|
|
mock_write_client,
|
|
callback_context,
|
|
invocation_context,
|
|
mock_auth_default,
|
|
mock_bq_client,
|
|
mock_to_arrow_schema,
|
|
dummy_arrow_schema,
|
|
mock_asyncio_to_thread,
|
|
):
|
|
config = BigQueryLoggerConfig(event_allowlist=["LLM_REQUEST"])
|
|
plugin = bigquery_agent_analytics_plugin.BigQueryAgentAnalyticsPlugin(
|
|
PROJECT_ID, DATASET_ID, TABLE_ID, config
|
|
)
|
|
await plugin._ensure_init()
|
|
mock_write_client.append_rows.reset_mock()
|
|
|
|
llm_request = llm_request_lib.LlmRequest(
|
|
model="gemini-pro",
|
|
contents=[types.Content(parts=[types.Part(text="Prompt")])],
|
|
)
|
|
await plugin.before_model_callback(
|
|
callback_context=callback_context, llm_request=llm_request
|
|
)
|
|
await asyncio.sleep(0.01) # Allow background task to run
|
|
mock_write_client.append_rows.assert_called_once()
|
|
mock_write_client.append_rows.reset_mock()
|
|
|
|
user_message = types.Content(parts=[types.Part(text="What is up?")])
|
|
await plugin.on_user_message_callback(
|
|
invocation_context=invocation_context, user_message=user_message
|
|
)
|
|
await asyncio.sleep(0.01) # Allow background task to run
|
|
mock_write_client.append_rows.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_event_denylist(
|
|
self,
|
|
mock_write_client,
|
|
invocation_context,
|
|
mock_auth_default,
|
|
mock_bq_client,
|
|
mock_to_arrow_schema,
|
|
dummy_arrow_schema,
|
|
mock_asyncio_to_thread,
|
|
):
|
|
config = BigQueryLoggerConfig(event_denylist=["USER_MESSAGE_RECEIVED"])
|
|
plugin = bigquery_agent_analytics_plugin.BigQueryAgentAnalyticsPlugin(
|
|
PROJECT_ID, DATASET_ID, TABLE_ID, config
|
|
)
|
|
await plugin._ensure_init()
|
|
mock_write_client.append_rows.reset_mock()
|
|
|
|
user_message = types.Content(parts=[types.Part(text="What is up?")])
|
|
await plugin.on_user_message_callback(
|
|
invocation_context=invocation_context, user_message=user_message
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
mock_write_client.append_rows.assert_not_called()
|
|
|
|
await plugin.before_run_callback(invocation_context=invocation_context)
|
|
await asyncio.sleep(0.01)
|
|
mock_write_client.append_rows.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_content_formatter(
|
|
self,
|
|
mock_write_client,
|
|
invocation_context,
|
|
mock_auth_default,
|
|
mock_bq_client,
|
|
mock_to_arrow_schema,
|
|
dummy_arrow_schema,
|
|
mock_asyncio_to_thread,
|
|
):
|
|
def redact_content(content):
|
|
return "[REDACTED]"
|
|
|
|
config = BigQueryLoggerConfig(content_formatter=redact_content)
|
|
plugin = bigquery_agent_analytics_plugin.BigQueryAgentAnalyticsPlugin(
|
|
PROJECT_ID, DATASET_ID, TABLE_ID, config
|
|
)
|
|
await plugin._ensure_init()
|
|
mock_write_client.append_rows.reset_mock()
|
|
|
|
user_message = types.Content(parts=[types.Part(text="Secret message")])
|
|
await plugin.on_user_message_callback(
|
|
invocation_context=invocation_context, user_message=user_message
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
mock_write_client.append_rows.assert_called_once()
|
|
log_entry = _get_captured_event_dict(mock_write_client, dummy_arrow_schema)
|
|
assert log_entry["content"] == "User Content: [REDACTED]"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_content_formatter_error(
|
|
self,
|
|
mock_write_client,
|
|
invocation_context,
|
|
mock_auth_default,
|
|
mock_bq_client,
|
|
mock_to_arrow_schema,
|
|
dummy_arrow_schema,
|
|
mock_asyncio_to_thread,
|
|
):
|
|
def error_formatter(content):
|
|
raise ValueError("Formatter failed")
|
|
|
|
config = BigQueryLoggerConfig(content_formatter=error_formatter)
|
|
plugin = bigquery_agent_analytics_plugin.BigQueryAgentAnalyticsPlugin(
|
|
PROJECT_ID, DATASET_ID, TABLE_ID, config
|
|
)
|
|
await plugin._ensure_init()
|
|
mock_write_client.append_rows.reset_mock()
|
|
|
|
user_message = types.Content(parts=[types.Part(text="Secret message")])
|
|
await plugin.on_user_message_callback(
|
|
invocation_context=invocation_context, user_message=user_message
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
mock_write_client.append_rows.assert_called_once()
|
|
log_entry = _get_captured_event_dict(mock_write_client, dummy_arrow_schema)
|
|
assert log_entry["content"] == "User Content: [FORMATTING FAILED]"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_on_user_message_callback_logs_correctly(
|
|
self,
|
|
bq_plugin_inst,
|
|
mock_write_client,
|
|
invocation_context,
|
|
dummy_arrow_schema,
|
|
):
|
|
user_message = types.Content(parts=[types.Part(text="What is up?")])
|
|
await bq_plugin_inst.on_user_message_callback(
|
|
invocation_context=invocation_context, user_message=user_message
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
log_entry = _get_captured_event_dict(mock_write_client, dummy_arrow_schema)
|
|
_assert_common_fields(log_entry, "USER_MESSAGE_RECEIVED")
|
|
assert log_entry["content"] == "User Content: text: 'What is up?'"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_on_event_callback_tool_call(
|
|
self,
|
|
bq_plugin_inst,
|
|
mock_write_client,
|
|
invocation_context,
|
|
dummy_arrow_schema,
|
|
):
|
|
tool_fc = types.FunctionCall(name="get_weather", args={"location": "Paris"})
|
|
event = event_lib.Event(
|
|
author="MyTestAgent",
|
|
content=types.Content(parts=[types.Part(function_call=tool_fc)]),
|
|
timestamp=datetime.datetime(
|
|
2025, 10, 22, 10, 0, 0, tzinfo=datetime.timezone.utc
|
|
).timestamp(),
|
|
)
|
|
await bq_plugin_inst.on_event_callback(
|
|
invocation_context=invocation_context, event=event
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
log_entry = _get_captured_event_dict(mock_write_client, dummy_arrow_schema)
|
|
_assert_common_fields(log_entry, "TOOL_CALL", agent="MyTestAgent")
|
|
assert '"name": "get_weather"' in log_entry["content"]
|
|
assert log_entry["timestamp"] == datetime.datetime(
|
|
2025, 10, 22, 10, 0, 0, tzinfo=datetime.timezone.utc
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_on_event_callback_model_response(
|
|
self,
|
|
bq_plugin_inst,
|
|
mock_write_client,
|
|
invocation_context,
|
|
dummy_arrow_schema,
|
|
):
|
|
event = event_lib.Event(
|
|
author="MyTestAgent",
|
|
content=types.Content(parts=[types.Part(text="Hello there!")]),
|
|
timestamp=datetime.datetime(
|
|
2025, 10, 22, 11, 0, 0, tzinfo=datetime.timezone.utc
|
|
).timestamp(),
|
|
)
|
|
await bq_plugin_inst.on_event_callback(
|
|
invocation_context=invocation_context, event=event
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
log_entry = _get_captured_event_dict(mock_write_client, dummy_arrow_schema)
|
|
_assert_common_fields(log_entry, "MODEL_RESPONSE", agent="MyTestAgent")
|
|
assert '"text": "Hello there!"' in log_entry["content"]
|
|
assert log_entry["timestamp"] == datetime.datetime(
|
|
2025, 10, 22, 11, 0, 0, tzinfo=datetime.timezone.utc
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bigquery_client_initialization_failure(
|
|
self,
|
|
mock_auth_default,
|
|
mock_write_client,
|
|
invocation_context,
|
|
mock_asyncio_to_thread,
|
|
):
|
|
mock_auth_default.side_effect = auth_exceptions.GoogleAuthError(
|
|
"Auth failed"
|
|
)
|
|
plugin_with_fail = (
|
|
bigquery_agent_analytics_plugin.BigQueryAgentAnalyticsPlugin(
|
|
project_id=PROJECT_ID,
|
|
dataset_id=DATASET_ID,
|
|
table_id=TABLE_ID,
|
|
)
|
|
)
|
|
with mock.patch.object(logging, "error") as mock_log_error:
|
|
await plugin_with_fail.on_user_message_callback(
|
|
invocation_context=invocation_context,
|
|
user_message=types.Content(parts=[types.Part(text="Test")]),
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
mock_log_error.assert_any_call("BQ Init Failed: Auth failed")
|
|
mock_write_client.append_rows.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bigquery_insert_error_does_not_raise(
|
|
self, bq_plugin_inst, mock_write_client, invocation_context
|
|
):
|
|
|
|
async def fake_append_rows_with_error(requests, **kwargs):
|
|
mock_append_rows_response = mock.MagicMock()
|
|
mock_append_rows_response.row_errors = [] # No row errors
|
|
mock_append_rows_response.error = mock.MagicMock()
|
|
mock_append_rows_response.error.code = 3 # INVALID_ARGUMENT
|
|
mock_append_rows_response.error.message = "Test BQ Error"
|
|
return _async_gen(mock_append_rows_response)
|
|
|
|
mock_write_client.append_rows.side_effect = fake_append_rows_with_error
|
|
|
|
with mock.patch.object(logging, "error") as mock_log_error:
|
|
await bq_plugin_inst.on_user_message_callback(
|
|
invocation_context=invocation_context,
|
|
user_message=types.Content(parts=[types.Part(text="Test")]),
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
mock_log_error.assert_called_with("BQ Write Error: Test BQ Error")
|
|
mock_write_client.append_rows.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_close(self, bq_plugin_inst, mock_bq_client, mock_write_client):
|
|
await bq_plugin_inst.close()
|
|
mock_write_client.transport.close.assert_called_once()
|
|
mock_bq_client.close.assert_called_once()
|
|
|
|
# ... other tests remain the same ...
|
|
@pytest.mark.asyncio
|
|
async def test_before_run_callback_logs_correctly(
|
|
self,
|
|
bq_plugin_inst,
|
|
mock_write_client,
|
|
invocation_context,
|
|
dummy_arrow_schema,
|
|
):
|
|
await bq_plugin_inst.before_run_callback(
|
|
invocation_context=invocation_context
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
log_entry = _get_captured_event_dict(mock_write_client, dummy_arrow_schema)
|
|
_assert_common_fields(log_entry, "INVOCATION_STARTING")
|
|
assert log_entry["content"] is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_after_run_callback_logs_correctly(
|
|
self,
|
|
bq_plugin_inst,
|
|
mock_write_client,
|
|
invocation_context,
|
|
dummy_arrow_schema,
|
|
):
|
|
await bq_plugin_inst.after_run_callback(
|
|
invocation_context=invocation_context
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
log_entry = _get_captured_event_dict(mock_write_client, dummy_arrow_schema)
|
|
_assert_common_fields(log_entry, "INVOCATION_COMPLETED")
|
|
assert log_entry["content"] is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_before_agent_callback_logs_correctly(
|
|
self,
|
|
bq_plugin_inst,
|
|
mock_write_client,
|
|
mock_agent,
|
|
callback_context,
|
|
dummy_arrow_schema,
|
|
):
|
|
await bq_plugin_inst.before_agent_callback(
|
|
agent=mock_agent, callback_context=callback_context
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
log_entry = _get_captured_event_dict(mock_write_client, dummy_arrow_schema)
|
|
_assert_common_fields(log_entry, "AGENT_STARTING")
|
|
assert log_entry["content"] == "Agent Name: MyTestAgent"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_after_agent_callback_logs_correctly(
|
|
self,
|
|
bq_plugin_inst,
|
|
mock_write_client,
|
|
mock_agent,
|
|
callback_context,
|
|
dummy_arrow_schema,
|
|
):
|
|
await bq_plugin_inst.after_agent_callback(
|
|
agent=mock_agent, callback_context=callback_context
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
log_entry = _get_captured_event_dict(mock_write_client, dummy_arrow_schema)
|
|
_assert_common_fields(log_entry, "AGENT_COMPLETED")
|
|
assert log_entry["content"] == "Agent Name: MyTestAgent"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_before_model_callback_logs_correctly(
|
|
self,
|
|
bq_plugin_inst,
|
|
mock_write_client,
|
|
callback_context,
|
|
dummy_arrow_schema,
|
|
):
|
|
llm_request = llm_request_lib.LlmRequest(
|
|
model="gemini-pro",
|
|
contents=[types.Content(parts=[types.Part(text="Prompt")])],
|
|
)
|
|
await bq_plugin_inst.before_model_callback(
|
|
callback_context=callback_context, llm_request=llm_request
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
log_entry = _get_captured_event_dict(mock_write_client, dummy_arrow_schema)
|
|
_assert_common_fields(log_entry, "LLM_REQUEST")
|
|
assert log_entry["content"] == "Model: gemini-pro | System Prompt: Empty"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_after_model_callback_text_response(
|
|
self,
|
|
bq_plugin_inst,
|
|
mock_write_client,
|
|
callback_context,
|
|
dummy_arrow_schema,
|
|
):
|
|
llm_response = llm_response_lib.LlmResponse(
|
|
content=types.Content(parts=[types.Part(text="Model response")]),
|
|
usage_metadata=types.UsageMetadata(
|
|
prompt_token_count=10, total_token_count=15
|
|
),
|
|
)
|
|
await bq_plugin_inst.after_model_callback(
|
|
callback_context=callback_context, llm_response=llm_response
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
log_entry = _get_captured_event_dict(mock_write_client, dummy_arrow_schema)
|
|
_assert_common_fields(log_entry, "LLM_RESPONSE")
|
|
assert (
|
|
"Tool Name: text_response, text: 'Model response'"
|
|
in log_entry["content"]
|
|
)
|
|
assert "Token Usage:" in log_entry["content"]
|
|
assert "prompt: 10" in log_entry["content"]
|
|
assert "total: 15" in log_entry["content"]
|
|
assert log_entry["error_message"] is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_after_model_callback_tool_call(
|
|
self,
|
|
bq_plugin_inst,
|
|
mock_write_client,
|
|
callback_context,
|
|
dummy_arrow_schema,
|
|
):
|
|
tool_fc = types.FunctionCall(name="get_weather", args={"location": "Paris"})
|
|
llm_response = llm_response_lib.LlmResponse(
|
|
content=types.Content(parts=[types.Part(function_call=tool_fc)]),
|
|
usage_metadata=types.UsageMetadata(
|
|
prompt_token_count=10, total_token_count=15
|
|
),
|
|
)
|
|
await bq_plugin_inst.after_model_callback(
|
|
callback_context=callback_context, llm_response=llm_response
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
log_entry = _get_captured_event_dict(mock_write_client, dummy_arrow_schema)
|
|
_assert_common_fields(log_entry, "LLM_RESPONSE")
|
|
assert "Tool Name: get_weather" in log_entry["content"]
|
|
assert "Token Usage:" in log_entry["content"]
|
|
assert "prompt: 10" in log_entry["content"]
|
|
assert "total: 15" in log_entry["content"]
|
|
assert log_entry["error_message"] is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_before_tool_callback_logs_correctly(
|
|
self, bq_plugin_inst, mock_write_client, tool_context, dummy_arrow_schema
|
|
):
|
|
mock_tool = mock.create_autospec(
|
|
base_tool_lib.BaseTool, instance=True, spec_set=True
|
|
)
|
|
type(mock_tool).name = mock.PropertyMock(return_value="MyTool")
|
|
type(mock_tool).description = mock.PropertyMock(return_value="Description")
|
|
await bq_plugin_inst.before_tool_callback(
|
|
tool=mock_tool, tool_args={"param": "value"}, tool_context=tool_context
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
log_entry = _get_captured_event_dict(mock_write_client, dummy_arrow_schema)
|
|
_assert_common_fields(log_entry, "TOOL_STARTING")
|
|
assert (
|
|
log_entry["content"]
|
|
== 'Tool Name: MyTool, Description: Description, Arguments: {"param":'
|
|
' "value"}'
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_after_tool_callback_logs_correctly(
|
|
self, bq_plugin_inst, mock_write_client, tool_context, dummy_arrow_schema
|
|
):
|
|
mock_tool = mock.create_autospec(
|
|
base_tool_lib.BaseTool, instance=True, spec_set=True
|
|
)
|
|
type(mock_tool).name = mock.PropertyMock(return_value="MyTool")
|
|
type(mock_tool).description = mock.PropertyMock(return_value="Description")
|
|
await bq_plugin_inst.after_tool_callback(
|
|
tool=mock_tool,
|
|
tool_args={},
|
|
tool_context=tool_context,
|
|
result={"status": "success"},
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
log_entry = _get_captured_event_dict(mock_write_client, dummy_arrow_schema)
|
|
_assert_common_fields(log_entry, "TOOL_COMPLETED")
|
|
assert (
|
|
log_entry["content"]
|
|
== 'Tool Name: MyTool, Result: {"status": "success"}'
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_on_model_error_callback_logs_correctly(
|
|
self,
|
|
bq_plugin_inst,
|
|
mock_write_client,
|
|
callback_context,
|
|
dummy_arrow_schema,
|
|
):
|
|
llm_request = llm_request_lib.LlmRequest(
|
|
model="gemini-pro",
|
|
contents=[types.Content(parts=[types.Part(text="Prompt")])],
|
|
)
|
|
error = ValueError("LLM failed")
|
|
await bq_plugin_inst.on_model_error_callback(
|
|
callback_context=callback_context, llm_request=llm_request, error=error
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
log_entry = _get_captured_event_dict(mock_write_client, dummy_arrow_schema)
|
|
_assert_common_fields(log_entry, "LLM_ERROR")
|
|
assert log_entry["content"] is None
|
|
assert log_entry["error_message"] == "LLM failed"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_on_tool_error_callback_logs_correctly(
|
|
self, bq_plugin_inst, mock_write_client, tool_context, dummy_arrow_schema
|
|
):
|
|
mock_tool = mock.create_autospec(
|
|
base_tool_lib.BaseTool, instance=True, spec_set=True
|
|
)
|
|
type(mock_tool).name = mock.PropertyMock(return_value="MyTool")
|
|
type(mock_tool).description = mock.PropertyMock(return_value="Description")
|
|
error = TimeoutError("Tool timed out")
|
|
await bq_plugin_inst.on_tool_error_callback(
|
|
tool=mock_tool,
|
|
tool_args={"param": "value"},
|
|
tool_context=tool_context,
|
|
error=error,
|
|
)
|
|
await asyncio.sleep(0.01)
|
|
log_entry = _get_captured_event_dict(mock_write_client, dummy_arrow_schema)
|
|
_assert_common_fields(log_entry, "TOOL_ERROR")
|
|
assert (
|
|
log_entry["content"]
|
|
== 'Tool Name: MyTool, Arguments: {"param": "value"}'
|
|
)
|
|
assert log_entry["error_message"] == "Tool timed out"
|