@@ -1451,7 +1451,161 @@ async def test_before_model_callback_with_params_and_tools(
14511451 assert attributes ["llm_config" ]["temperature" ] == 0.5
14521452 assert attributes ["llm_config" ]["top_p" ] == 0.9
14531453 assert attributes ["llm_config" ]["top_p" ] == 0.9
1454- assert attributes ["tools" ] == ["tool1" , "tool2" ]
1454+ # Tools without a name/description/declaration fall back to just the key.
1455+ assert attributes ["tools" ] == [{"name" : "tool1" }, {"name" : "tool2" }]
1456+
1457+ @pytest .mark .asyncio
1458+ async def test_before_model_callback_logs_tool_declarations (
1459+ self ,
1460+ bq_plugin_inst ,
1461+ mock_write_client ,
1462+ callback_context ,
1463+ dummy_arrow_schema ,
1464+ ):
1465+ """LLM_REQUEST tools carry name, description, and parameter schema."""
1466+
1467+ class _FakeTool (base_tool_lib .BaseTool ):
1468+
1469+ def __init__ (self , name , description , declaration ):
1470+ super ().__init__ (name = name , description = description )
1471+ self ._declaration = declaration
1472+
1473+ def _get_declaration (self ):
1474+ return self ._declaration
1475+
1476+ execute_sql = _FakeTool (
1477+ name = "execute_sql" ,
1478+ description = "Run a SQL query against BigQuery." ,
1479+ declaration = types .FunctionDeclaration (
1480+ name = "execute_sql" ,
1481+ description = "Run a SQL query against BigQuery." ,
1482+ parameters = types .Schema (
1483+ type = types .Type .OBJECT ,
1484+ properties = {
1485+ "query" : types .Schema (
1486+ type = types .Type .STRING ,
1487+ description = "The SQL query to run." ,
1488+ )
1489+ },
1490+ required = ["query" ],
1491+ ),
1492+ ),
1493+ )
1494+ # A tool without a declaration still contributes name + description.
1495+ list_datasets = _FakeTool (
1496+ name = "list_dataset_ids" ,
1497+ description = "List available datasets." ,
1498+ declaration = None ,
1499+ )
1500+
1501+ llm_request = llm_request_lib .LlmRequest (
1502+ model = "gemini-pro" ,
1503+ contents = [types .Content (role = "user" , parts = [types .Part (text = "hi" )])],
1504+ )
1505+ llm_request .tools_dict = {
1506+ "execute_sql" : execute_sql ,
1507+ "list_dataset_ids" : list_datasets ,
1508+ }
1509+ bigquery_agent_analytics_plugin .TraceManager .push_span (callback_context )
1510+ await bq_plugin_inst .before_model_callback (
1511+ callback_context = callback_context , llm_request = llm_request
1512+ )
1513+ await asyncio .sleep (0.01 )
1514+ log_entry = await _get_captured_event_dict_async (
1515+ mock_write_client , dummy_arrow_schema
1516+ )
1517+ _assert_common_fields (log_entry , "LLM_REQUEST" )
1518+ attributes = json .loads (log_entry ["attributes" ])
1519+ tools_by_name = {t ["name" ]: t for t in attributes ["tools" ]}
1520+
1521+ assert tools_by_name ["execute_sql" ]["description" ] == (
1522+ "Run a SQL query against BigQuery."
1523+ )
1524+ params = tools_by_name ["execute_sql" ]["parameters" ]
1525+ assert params ["type" ] == "OBJECT"
1526+ assert params ["properties" ]["query" ]["type" ] == "STRING"
1527+ assert params ["required" ] == ["query" ]
1528+
1529+ assert tools_by_name ["list_dataset_ids" ]["description" ] == (
1530+ "List available datasets."
1531+ )
1532+ assert "parameters" not in tools_by_name ["list_dataset_ids" ]
1533+
1534+ def test_extract_tool_declarations_declaration_error_is_isolated (self ):
1535+ """A tool whose _get_declaration raises still yields name + description."""
1536+
1537+ class _RaisingTool (base_tool_lib .BaseTool ):
1538+
1539+ def _get_declaration (self ):
1540+ raise ValueError ("boom" )
1541+
1542+ class _OkTool (base_tool_lib .BaseTool ):
1543+
1544+ def _get_declaration (self ):
1545+ return None
1546+
1547+ result = bigquery_agent_analytics_plugin ._extract_tool_declarations ({
1548+ "raiser" : _RaisingTool (name = "raiser" , description = "Raises." ),
1549+ "ok" : _OkTool (name = "ok" , description = "Fine." ),
1550+ })
1551+ by_name = {t ["name" ]: t for t in result }
1552+
1553+ # The raising tool is not dropped; other tools are unaffected.
1554+ assert by_name ["raiser" ] == {"name" : "raiser" , "description" : "Raises." }
1555+ assert by_name ["ok" ] == {"name" : "ok" , "description" : "Fine." }
1556+
1557+ def test_extract_tool_declarations_parameters_serialization_error (self ):
1558+ """A parameters object that fails to serialize is dropped, not fatal."""
1559+
1560+ class _BadParams :
1561+
1562+ def model_dump (self , * args , ** kwargs ):
1563+ raise ValueError ("cannot serialize" )
1564+
1565+ class _BadDecl :
1566+ description = None
1567+ parameters = _BadParams ()
1568+
1569+ class _BadParamTool (base_tool_lib .BaseTool ):
1570+
1571+ def _get_declaration (self ):
1572+ return _BadDecl ()
1573+
1574+ result = bigquery_agent_analytics_plugin ._extract_tool_declarations (
1575+ {"bad_params" : _BadParamTool (name = "bad_params" , description = "Bad." )}
1576+ )
1577+
1578+ # Name + description survive; the unserializable parameters key is omitted.
1579+ assert result == [{"name" : "bad_params" , "description" : "Bad." }]
1580+
1581+ def test_extract_tool_declarations_uses_parameters_json_schema (self ):
1582+ """Declarations exposing parameters_json_schema log that raw schema."""
1583+
1584+ json_schema = {
1585+ "type" : "object" ,
1586+ "properties" : {"path" : {"type" : "string" }},
1587+ "required" : ["path" ],
1588+ }
1589+
1590+ class _JsonSchemaTool (base_tool_lib .BaseTool ):
1591+
1592+ def _get_declaration (self ):
1593+ return types .FunctionDeclaration (
1594+ name = "read_file" ,
1595+ description = "Read a file." ,
1596+ parameters_json_schema = json_schema ,
1597+ )
1598+
1599+ result = bigquery_agent_analytics_plugin ._extract_tool_declarations (
1600+ {"read_file" : _JsonSchemaTool (name = "read_file" , description = "Read." )}
1601+ )
1602+
1603+ # parameters_json_schema is logged verbatim (preferred over `parameters`).
1604+ assert result == [{
1605+ "name" : "read_file" ,
1606+ "description" : "Read." ,
1607+ "parameters" : json_schema ,
1608+ }]
14551609
14561610 @pytest .mark .asyncio
14571611 async def test_before_model_callback_with_full_config (
0 commit comments