|
14 | 14 |
|
15 | 15 | """Tests for interactions_utils.py conversion functions.""" |
16 | 16 |
|
| 17 | +import base64 |
17 | 18 | import json |
18 | 19 | from unittest.mock import MagicMock |
19 | 20 |
|
20 | 21 | from google.adk.models import interactions_utils |
21 | 22 | from google.adk.models.llm_request import LlmRequest |
22 | 23 | from google.genai import types |
23 | | -import pytest |
24 | 24 |
|
25 | 25 |
|
26 | 26 | class TestConvertPartToInteractionContent: |
@@ -61,6 +61,42 @@ def test_function_call_part_no_id(self): |
61 | 61 | assert result['id'] == '' |
62 | 62 | assert result['name'] == 'get_weather' |
63 | 63 |
|
| 64 | + def test_function_call_part_with_thought_signature(self): |
| 65 | + """Test converting a function call Part with thought_signature.""" |
| 66 | + part = types.Part( |
| 67 | + function_call=types.FunctionCall( |
| 68 | + id='call_456', |
| 69 | + name='my_tool', |
| 70 | + args={'doc': 'content'}, |
| 71 | + ), |
| 72 | + thought_signature=b'test_signature_bytes', |
| 73 | + ) |
| 74 | + result = interactions_utils.convert_part_to_interaction_content(part) |
| 75 | + assert result['type'] == 'function_call' |
| 76 | + assert result['id'] == 'call_456' |
| 77 | + assert result['name'] == 'my_tool' |
| 78 | + assert result['arguments'] == {'doc': 'content'} |
| 79 | + # thought_signature should be base64 encoded |
| 80 | + assert 'thought_signature' in result |
| 81 | + |
| 82 | + assert ( |
| 83 | + base64.b64decode(result['thought_signature']) == b'test_signature_bytes' |
| 84 | + ) |
| 85 | + |
| 86 | + def test_function_call_part_without_thought_signature(self): |
| 87 | + """Test converting a function call Part without thought_signature.""" |
| 88 | + part = types.Part( |
| 89 | + function_call=types.FunctionCall( |
| 90 | + id='call_789', |
| 91 | + name='other_tool', |
| 92 | + args={}, |
| 93 | + ) |
| 94 | + ) |
| 95 | + result = interactions_utils.convert_part_to_interaction_content(part) |
| 96 | + assert result['type'] == 'function_call' |
| 97 | + # thought_signature should not be present |
| 98 | + assert 'thought_signature' not in result |
| 99 | + |
64 | 100 | def test_function_response_dict(self): |
65 | 101 | """Test converting a function response Part with dict response.""" |
66 | 102 | part = types.Part( |
@@ -183,8 +219,6 @@ def test_text_with_thought_flag(self): |
183 | 219 |
|
184 | 220 | def test_thought_only_part(self): |
185 | 221 | """Test converting a thought-only Part with signature.""" |
186 | | - import base64 |
187 | | - |
188 | 222 | signature_bytes = b'test-thought-signature' |
189 | 223 | part = types.Part(thought=True, thought_signature=signature_bytes) |
190 | 224 | result = interactions_utils.convert_part_to_interaction_content(part) |
@@ -443,6 +477,39 @@ def test_function_call_output(self): |
443 | 477 | assert result.function_call.name == 'get_weather' |
444 | 478 | assert result.function_call.args == {'city': 'London'} |
445 | 479 |
|
| 480 | + def test_function_call_output_with_thought_signature(self): |
| 481 | + """Test converting function call output with thought_signature.""" |
| 482 | + output = MagicMock( |
| 483 | + spec=['type', 'id', 'name', 'arguments', 'thought_signature'] |
| 484 | + ) |
| 485 | + output.type = 'function_call' |
| 486 | + output.id = 'call_sig_123' |
| 487 | + output.name = 'gemini3_tool' |
| 488 | + output.arguments = {'content': 'hello'} |
| 489 | + # thought_signature is base64 encoded in the output |
| 490 | + output.thought_signature = base64.b64encode(b'gemini3_signature').decode( |
| 491 | + 'utf-8' |
| 492 | + ) |
| 493 | + result = interactions_utils.convert_interaction_output_to_part(output) |
| 494 | + assert result.function_call.id == 'call_sig_123' |
| 495 | + assert result.function_call.name == 'gemini3_tool' |
| 496 | + assert result.function_call.args == {'content': 'hello'} |
| 497 | + # thought_signature should be decoded back to bytes |
| 498 | + assert result.thought_signature == b'gemini3_signature' |
| 499 | + |
| 500 | + def test_function_call_output_without_thought_signature(self): |
| 501 | + """Test converting function call output without thought_signature.""" |
| 502 | + output = MagicMock(spec=['type', 'id', 'name', 'arguments']) |
| 503 | + output.type = 'function_call' |
| 504 | + output.id = 'call_no_sig' |
| 505 | + output.name = 'regular_tool' |
| 506 | + output.arguments = {} |
| 507 | + result = interactions_utils.convert_interaction_output_to_part(output) |
| 508 | + assert result.function_call.id == 'call_no_sig' |
| 509 | + assert result.function_call.name == 'regular_tool' |
| 510 | + # thought_signature should be None |
| 511 | + assert result.thought_signature is None |
| 512 | + |
446 | 513 | def test_function_result_output_with_items_list(self): |
447 | 514 | """Test converting function result output with items list. |
448 | 515 |
|
@@ -759,3 +826,132 @@ def test_full_conversation(self): |
759 | 826 | assert len(result) == 2 |
760 | 827 | assert result[0].parts[0].text == 'Great' |
761 | 828 | assert result[1].parts[0].text == 'Tell me more' |
| 829 | + |
| 830 | + |
| 831 | +class TestConvertInteractionEventToLlmResponse: |
| 832 | + """Tests for convert_interaction_event_to_llm_response.""" |
| 833 | + |
| 834 | + def test_text_delta_event(self): |
| 835 | + """Test converting a text delta event.""" |
| 836 | + event = MagicMock() |
| 837 | + event.event_type = 'content.delta' |
| 838 | + event.delta = MagicMock() |
| 839 | + event.delta.type = 'text' |
| 840 | + event.delta.text = 'Hello world' |
| 841 | + |
| 842 | + aggregated_parts = [] |
| 843 | + result = interactions_utils.convert_interaction_event_to_llm_response( |
| 844 | + event, aggregated_parts, interaction_id='int_123' |
| 845 | + ) |
| 846 | + |
| 847 | + assert result is not None |
| 848 | + assert result.partial |
| 849 | + assert result.content.parts[0].text == 'Hello world' |
| 850 | + assert result.interaction_id == 'int_123' |
| 851 | + assert len(aggregated_parts) == 1 |
| 852 | + |
| 853 | + def test_function_call_delta_with_thought_signature(self): |
| 854 | + """Test converting a function call delta with thought_signature.""" |
| 855 | + event = MagicMock() |
| 856 | + event.event_type = 'content.delta' |
| 857 | + event.delta = MagicMock( |
| 858 | + spec=['type', 'id', 'name', 'arguments', 'thought_signature'] |
| 859 | + ) |
| 860 | + event.delta.type = 'function_call' |
| 861 | + event.delta.id = 'fc_delta_123' |
| 862 | + event.delta.name = 'streaming_tool' |
| 863 | + event.delta.arguments = {'param': 'value'} |
| 864 | + # thought_signature is base64 encoded in the delta |
| 865 | + event.delta.thought_signature = base64.b64encode(b'delta_signature').decode( |
| 866 | + 'utf-8' |
| 867 | + ) |
| 868 | + |
| 869 | + aggregated_parts = [] |
| 870 | + result = interactions_utils.convert_interaction_event_to_llm_response( |
| 871 | + event, aggregated_parts, interaction_id='int_456' |
| 872 | + ) |
| 873 | + |
| 874 | + # Function calls return None (added to aggregated_parts only) |
| 875 | + assert result is None |
| 876 | + assert len(aggregated_parts) == 1 |
| 877 | + fc_part = aggregated_parts[0] |
| 878 | + assert fc_part.function_call.id == 'fc_delta_123' |
| 879 | + assert fc_part.function_call.name == 'streaming_tool' |
| 880 | + assert fc_part.function_call.args == {'param': 'value'} |
| 881 | + # thought_signature should be decoded back to bytes |
| 882 | + assert fc_part.thought_signature == b'delta_signature' |
| 883 | + |
| 884 | + def test_function_call_delta_without_thought_signature(self): |
| 885 | + """Test converting a function call delta without thought_signature.""" |
| 886 | + event = MagicMock() |
| 887 | + event.event_type = 'content.delta' |
| 888 | + event.delta = MagicMock(spec=['type', 'id', 'name', 'arguments']) |
| 889 | + event.delta.type = 'function_call' |
| 890 | + event.delta.id = 'fc_no_sig' |
| 891 | + event.delta.name = 'regular_tool' |
| 892 | + event.delta.arguments = {} |
| 893 | + |
| 894 | + aggregated_parts = [] |
| 895 | + result = interactions_utils.convert_interaction_event_to_llm_response( |
| 896 | + event, aggregated_parts, interaction_id='int_789' |
| 897 | + ) |
| 898 | + |
| 899 | + # Function calls return None |
| 900 | + assert result is None |
| 901 | + assert len(aggregated_parts) == 1 |
| 902 | + fc_part = aggregated_parts[0] |
| 903 | + assert fc_part.function_call.name == 'regular_tool' |
| 904 | + # thought_signature should be None |
| 905 | + assert fc_part.thought_signature is None |
| 906 | + |
| 907 | + def test_function_call_delta_without_name_skipped(self): |
| 908 | + """Test that function call delta without name is skipped.""" |
| 909 | + event = MagicMock() |
| 910 | + event.event_type = 'content.delta' |
| 911 | + event.delta = MagicMock(spec=['type', 'id', 'name', 'arguments']) |
| 912 | + event.delta.type = 'function_call' |
| 913 | + event.delta.id = 'fc_no_name' |
| 914 | + event.delta.name = None # No name |
| 915 | + event.delta.arguments = {} |
| 916 | + |
| 917 | + aggregated_parts = [] |
| 918 | + result = interactions_utils.convert_interaction_event_to_llm_response( |
| 919 | + event, aggregated_parts, interaction_id='int_000' |
| 920 | + ) |
| 921 | + |
| 922 | + # Should be skipped (no name) |
| 923 | + assert result is None |
| 924 | + assert not aggregated_parts |
| 925 | + |
| 926 | + def test_image_delta_with_data(self): |
| 927 | + """Test converting an image delta with inline data.""" |
| 928 | + event = MagicMock() |
| 929 | + event.event_type = 'content.delta' |
| 930 | + event.delta = MagicMock() |
| 931 | + event.delta.type = 'image' |
| 932 | + event.delta.data = b'image_bytes' |
| 933 | + event.delta.uri = None |
| 934 | + event.delta.mime_type = 'image/png' |
| 935 | + |
| 936 | + aggregated_parts = [] |
| 937 | + result = interactions_utils.convert_interaction_event_to_llm_response( |
| 938 | + event, aggregated_parts, interaction_id='int_img' |
| 939 | + ) |
| 940 | + |
| 941 | + assert result is not None |
| 942 | + assert not result.partial |
| 943 | + assert result.content.parts[0].inline_data.data == b'image_bytes' |
| 944 | + assert len(aggregated_parts) == 1 |
| 945 | + |
| 946 | + def test_unknown_event_type_returns_none(self): |
| 947 | + """Test that unknown event types return None.""" |
| 948 | + event = MagicMock() |
| 949 | + event.event_type = 'some_unknown_event' # Unknown event type |
| 950 | + |
| 951 | + aggregated_parts = [] |
| 952 | + result = interactions_utils.convert_interaction_event_to_llm_response( |
| 953 | + event, aggregated_parts, interaction_id='int_other' |
| 954 | + ) |
| 955 | + |
| 956 | + assert result is None |
| 957 | + assert not aggregated_parts |
0 commit comments