|
13 | 13 | # limitations under the License. |
14 | 14 |
|
15 | 15 | import json |
16 | | -import os |
17 | 16 | from typing import Any |
18 | 17 | from typing import Dict |
19 | 18 | from typing import Optional |
@@ -207,18 +206,87 @@ async def test_trace_call_llm_with_binary_content( |
207 | 206 | assert mock_span_fixture.set_attribute.call_count == 7 |
208 | 207 | mock_span_fixture.set_attribute.assert_has_calls(expected_calls) |
209 | 208 |
|
210 | | - # Verify binary content is replaced with '<not serializable>' in JSON |
| 209 | + # Verify binary values are properly serialized as base64 |
211 | 210 | llm_request_json_str = None |
212 | 211 | for call_obj in mock_span_fixture.set_attribute.call_args_list: |
213 | | - if call_obj.args[0] == 'gcp.vertex.agent.llm_request': |
214 | | - llm_request_json_str = call_obj.args[1] |
| 212 | + arg_name, arg_value = call_obj.args |
| 213 | + if arg_name == 'gcp.vertex.agent.llm_request': |
| 214 | + llm_request_json_str = arg_value |
| 215 | + break |
| 216 | + |
| 217 | + assert llm_request_json_str is not None |
| 218 | + |
| 219 | + # Verify bytes are base64 encoded (b'test_data' -> 'dGVzdF9kYXRh') |
| 220 | + assert 'dGVzdF9kYXRh' in llm_request_json_str |
| 221 | + |
| 222 | + # Verify no serialization failures |
| 223 | + assert '<not serializable>' not in llm_request_json_str |
| 224 | + |
| 225 | + |
| 226 | +@pytest.mark.asyncio |
| 227 | +async def test_trace_call_llm_with_thought_signature( |
| 228 | + monkeypatch, mock_span_fixture |
| 229 | +): |
| 230 | + """Test trace_call_llm handles thought_signature bytes correctly. |
| 231 | +
|
| 232 | + This test verifies that thought_signature bytes from Gemini 3.0 models |
| 233 | + are properly serialized as base64 in telemetry traces. |
| 234 | + """ |
| 235 | + monkeypatch.setattr( |
| 236 | + 'opentelemetry.trace.get_current_span', lambda: mock_span_fixture |
| 237 | + ) |
| 238 | + |
| 239 | + agent = LlmAgent(name='test_agent') |
| 240 | + invocation_context = await _create_invocation_context(agent) |
| 241 | + |
| 242 | + # multi-turn conversation where the model's response contains |
| 243 | + # thought_signature bytes |
| 244 | + thought_signature_bytes = b'thought_signature' |
| 245 | + llm_request = LlmRequest( |
| 246 | + model='gemini-3-pro-preview', |
| 247 | + contents=[ |
| 248 | + types.Content( |
| 249 | + role='user', |
| 250 | + parts=[types.Part(text='Hello')], |
| 251 | + ), |
| 252 | + types.Content( |
| 253 | + role='model', |
| 254 | + parts=[ |
| 255 | + types.Part( |
| 256 | + thought=True, |
| 257 | + thought_signature=thought_signature_bytes, |
| 258 | + ) |
| 259 | + ], |
| 260 | + ), |
| 261 | + types.Content( |
| 262 | + role='user', |
| 263 | + parts=[types.Part(text='Follow up question')], |
| 264 | + ), |
| 265 | + ], |
| 266 | + config=types.GenerateContentConfig(), |
| 267 | + ) |
| 268 | + llm_response = LlmResponse(turn_complete=True) |
| 269 | + |
| 270 | + # should not raise TypeError for bytes serialization |
| 271 | + trace_call_llm(invocation_context, 'test_event_id', llm_request, llm_response) |
| 272 | + |
| 273 | + llm_request_json_str = None |
| 274 | + for call_obj in mock_span_fixture.set_attribute.call_args_list: |
| 275 | + arg_name, arg_value = call_obj.args |
| 276 | + if arg_name == 'gcp.vertex.agent.llm_request': |
| 277 | + llm_request_json_str = arg_value |
215 | 278 | break |
216 | 279 |
|
217 | 280 | assert ( |
218 | 281 | llm_request_json_str is not None |
219 | 282 | ), "Attribute 'gcp.vertex.agent.llm_request' was not set on the span." |
220 | 283 |
|
221 | | - assert llm_request_json_str.count('<not serializable>') == 2 |
| 284 | + # no serialization failures |
| 285 | + assert '<not serializable>' not in llm_request_json_str |
| 286 | + # llm request is valid JSON |
| 287 | + parsed = json.loads(llm_request_json_str) |
| 288 | + assert parsed['model'] == 'gemini-3-pro-preview' |
| 289 | + assert len(parsed['contents']) == 3 |
222 | 290 |
|
223 | 291 |
|
224 | 292 | def test_trace_tool_call_with_scalar_response( |
@@ -407,15 +475,19 @@ async def test_call_llm_disabling_request_response_content( |
407 | 475 |
|
408 | 476 | # Assert |
409 | 477 | assert not any( |
410 | | - call_obj.args[0] == 'gcp.vertex.agent.llm_request' |
411 | | - and call_obj.args[1] != {} |
412 | | - for call_obj in mock_span_fixture.set_attribute.call_args_list |
| 478 | + arg_name == 'gcp.vertex.agent.llm_request' and arg_value != {} |
| 479 | + for arg_name, arg_value in ( |
| 480 | + call_obj.args |
| 481 | + for call_obj in mock_span_fixture.set_attribute.call_args_list |
| 482 | + ) |
413 | 483 | ), "Attribute 'gcp.vertex.agent.llm_request' was incorrectly set on the span." |
414 | 484 |
|
415 | 485 | assert not any( |
416 | | - call_obj.args[0] == 'gcp.vertex.agent.llm_response' |
417 | | - and call_obj.args[1] != {} |
418 | | - for call_obj in mock_span_fixture.set_attribute.call_args_list |
| 486 | + arg_name == 'gcp.vertex.agent.llm_response' and arg_value != {} |
| 487 | + for arg_name, arg_value in ( |
| 488 | + call_obj.args |
| 489 | + for call_obj in mock_span_fixture.set_attribute.call_args_list |
| 490 | + ) |
419 | 491 | ), ( |
420 | 492 | "Attribute 'gcp.vertex.agent.llm_response' was incorrectly set on the" |
421 | 493 | ' span.' |
@@ -466,18 +538,22 @@ def test_trace_tool_call_disabling_request_response_content( |
466 | 538 |
|
467 | 539 | # Assert |
468 | 540 | assert not any( |
469 | | - call_obj.args[0] == 'gcp.vertex.agent.tool_call_args' |
470 | | - and call_obj.args[1] != {} |
471 | | - for call_obj in mock_span_fixture.set_attribute.call_args_list |
| 541 | + arg_name == 'gcp.vertex.agent.tool_call_args' and arg_value != {} |
| 542 | + for arg_name, arg_value in ( |
| 543 | + call_obj.args |
| 544 | + for call_obj in mock_span_fixture.set_attribute.call_args_list |
| 545 | + ) |
472 | 546 | ), ( |
473 | 547 | "Attribute 'gcp.vertex.agent.tool_call_args' was incorrectly set on the" |
474 | 548 | ' span.' |
475 | 549 | ) |
476 | 550 |
|
477 | 551 | assert not any( |
478 | | - call_obj.args[0] == 'gcp.vertex.agent.tool_response' |
479 | | - and call_obj.args[1] != {} |
480 | | - for call_obj in mock_span_fixture.set_attribute.call_args_list |
| 552 | + arg_name == 'gcp.vertex.agent.tool_response' and arg_value != {} |
| 553 | + for arg_name, arg_value in ( |
| 554 | + call_obj.args |
| 555 | + for call_obj in mock_span_fixture.set_attribute.call_args_list |
| 556 | + ) |
481 | 557 | ), ( |
482 | 558 | "Attribute 'gcp.vertex.agent.tool_response' was incorrectly set on the" |
483 | 559 | ' span.' |
@@ -510,9 +586,11 @@ def test_trace_merged_tool_disabling_request_response_content( |
510 | 586 |
|
511 | 587 | # Assert |
512 | 588 | assert not any( |
513 | | - call_obj.args[0] == 'gcp.vertex.agent.tool_response' |
514 | | - and call_obj.args[1] != {} |
515 | | - for call_obj in mock_span_fixture.set_attribute.call_args_list |
| 589 | + arg_name == 'gcp.vertex.agent.tool_response' and arg_value != {} |
| 590 | + for arg_name, arg_value in ( |
| 591 | + call_obj.args |
| 592 | + for call_obj in mock_span_fixture.set_attribute.call_args_list |
| 593 | + ) |
516 | 594 | ), ( |
517 | 595 | "Attribute 'gcp.vertex.agent.tool_response' was incorrectly set on the" |
518 | 596 | ' span.' |
|
0 commit comments