3333from google .genai .interactions import Interaction
3434from google .genai .interactions import InteractionCompletedEvent
3535from google .genai .interactions import InteractionCreatedEvent
36+ from google .genai .interactions import InteractionSseEventInteraction
3637from google .genai .interactions import ModelOutputStep
3738from google .genai .interactions import StepDelta
3839from google .genai .interactions import StepStart
@@ -110,9 +111,9 @@ def fc_step() -> FunctionCallStep:
110111
111112def _build_lifecycle_streamed_events (fc_step : FunctionCallStep ) -> list [object ]:
112113 """Build streamed events with lifecycle updates carrying the ID."""
113- now = datetime .now (timezone .utc )
114+ now = datetime .now (timezone .utc ). isoformat ()
114115
115- interaction = Interaction (
116+ interaction = InteractionSseEventInteraction (
116117 id = 'interaction_123' ,
117118 created = now ,
118119 updated = now ,
@@ -134,9 +135,9 @@ def _build_lifecycle_streamed_events(fc_step: FunctionCallStep) -> list[object]:
134135
135136def _build_complete_streamed_events (fc_step : FunctionCallStep ) -> list [object ]:
136137 """Build streamed events with the ID on an interaction.complete event."""
137- now = datetime .now (timezone .utc )
138+ now = datetime .now (timezone .utc ). isoformat ()
138139
139- interaction = Interaction (
140+ interaction = InteractionSseEventInteraction (
140141 id = 'interaction_complete_123' ,
141142 created = now ,
142143 updated = now ,
@@ -154,7 +155,7 @@ def _build_complete_streamed_events(fc_step: FunctionCallStep) -> list[object]:
154155
155156def _build_legacy_streamed_events (fc_step : FunctionCallStep ) -> list [object ]:
156157 """Build streamed events with the ID on the legacy interaction event."""
157- now = datetime .now (timezone .utc )
158+ now = datetime .now (timezone .utc ). isoformat ()
158159
159160 interaction = Interaction (
160161 id = 'interaction_legacy_123' ,
@@ -246,8 +247,8 @@ def test_function_call_part_no_id(self):
246247 assert result ['id' ] == ''
247248 assert result ['name' ] == 'get_weather'
248249
249- def test_function_call_part_with_thought_signature (self ):
250- """Test converting a function call Part with thought_signature ."""
250+ def test_function_call_part_thought_signature_dropped (self ):
251+ """Thought signatures are not sent on interactions function call steps ."""
251252 part = types .Part (
252253 function_call = types .FunctionCall (
253254 id = 'call_456' ,
@@ -257,14 +258,13 @@ def test_function_call_part_with_thought_signature(self):
257258 thought_signature = b'test_signature_bytes' ,
258259 )
259260 result = interactions_utils ._convert_part_to_interaction_content (part )
260- assert result ['type' ] == 'function_call'
261- assert result ['id' ] == 'call_456'
262- assert result ['name' ] == 'my_tool'
263- assert result ['arguments' ] == {'doc' : 'content' }
264- # signature should be base64 encoded
265- assert 'signature' in result
266-
267- assert base64 .b64decode (result ['signature' ]) == b'test_signature_bytes'
261+ assert result == {
262+ 'type' : 'function_call' ,
263+ 'id' : 'call_456' ,
264+ 'name' : 'my_tool' ,
265+ 'arguments' : {'doc' : 'content' },
266+ }
267+ assert 'signature' not in result
268268
269269 def test_function_call_part_without_thought_signature (self ):
270270 """Test converting a function call Part without thought_signature."""
@@ -765,23 +765,6 @@ def test_function_call_output(self):
765765 assert result .function_call .name == 'get_weather'
766766 assert result .function_call .args == {'city' : 'London' }
767767
768- def test_function_call_output_with_thought_signature (self ):
769- """Test converting function call output with thought_signature."""
770- output = FunctionCallStep (
771- type = 'function_call' ,
772- id = 'call_sig_123' ,
773- name = 'gemini3_tool' ,
774- arguments = {'content' : 'hello' },
775- signature = base64 .b64encode (b'gemini3_signature' ).decode ('utf-8' ),
776- )
777- result_list = interactions_utils ._convert_interaction_step_to_parts (output )
778- result = result_list [0 ] if result_list else None
779- assert result .function_call .id == 'call_sig_123'
780- assert result .function_call .name == 'gemini3_tool'
781- assert result .function_call .args == {'content' : 'hello' }
782- # thought_signature should be decoded back to bytes
783- assert result .thought_signature == b'gemini3_signature'
784-
785768 def test_function_call_output_without_thought_signature (self ):
786769 """Test converting function call output without thought_signature."""
787770 output = FunctionCallStep (
@@ -809,6 +792,41 @@ def test_function_result_output(self):
809792 assert result .function_response .id == 'call_123'
810793 assert result .function_response .response == {'weather' : 'Sunny' }
811794
795+ def test_function_result_output_preserves_none_values (self ):
796+ """None values in a dict result must not be dropped."""
797+ output = FunctionResultStep (
798+ type = 'function_result' ,
799+ call_id = 'call_none' ,
800+ result = {'data' : None , 'ok' : True },
801+ )
802+ result_list = interactions_utils ._convert_interaction_step_to_parts (output )
803+ result = result_list [0 ] if result_list else None
804+ assert result .function_response .response == {'data' : None , 'ok' : True }
805+
806+ def test_function_result_output_string (self ):
807+ """A plain string result is wrapped under a 'result' key."""
808+ output = FunctionResultStep (
809+ type = 'function_result' ,
810+ call_id = 'call_str' ,
811+ result = 'plain text' ,
812+ )
813+ result_list = interactions_utils ._convert_interaction_step_to_parts (output )
814+ result = result_list [0 ] if result_list else None
815+ assert result .function_response .response == {'result' : 'plain text' }
816+
817+ def test_function_result_output_list (self ):
818+ """A list result of content blocks is wrapped under a 'result' key."""
819+ output = FunctionResultStep (
820+ type = 'function_result' ,
821+ call_id = 'call_list' ,
822+ result = [{'type' : 'text' , 'text' : 'hi' }],
823+ )
824+ result_list = interactions_utils ._convert_interaction_step_to_parts (output )
825+ result = result_list [0 ] if result_list else None
826+ wrapped = result .function_response .response ['result' ]
827+ assert wrapped [0 ]['type' ] == 'text'
828+ assert wrapped [0 ]['text' ] == 'hi'
829+
812830 def test_image_output_with_data (self ):
813831 """Test converting image output with inline data."""
814832 output = ModelOutputStep (
@@ -909,8 +927,8 @@ def test_successful_text_response(self):
909927 interaction = Interaction (
910928 id = 'interaction_123' ,
911929 status = 'completed' ,
912- created = datetime .now (timezone .utc ),
913- updated = datetime .now (timezone .utc ),
930+ created = datetime .now (timezone .utc ). isoformat () ,
931+ updated = datetime .now (timezone .utc ). isoformat () ,
914932 steps = [
915933 ModelOutputStep (
916934 type = 'model_output' ,
@@ -933,8 +951,8 @@ def test_failed_response(self):
933951 interaction = Interaction (
934952 id = 'interaction_123' ,
935953 status = 'failed' ,
936- created = datetime .now (timezone .utc ),
937- updated = datetime .now (timezone .utc ),
954+ created = datetime .now (timezone .utc ). isoformat () ,
955+ updated = datetime .now (timezone .utc ). isoformat () ,
938956 steps = [],
939957 )
940958 interaction .error = MagicMock (code = 'INVALID_REQUEST' , message = 'Bad request' )
@@ -950,8 +968,8 @@ def test_requires_action_response(self):
950968 interaction = Interaction (
951969 id = 'interaction_123' ,
952970 status = 'requires_action' ,
953- created = datetime .now (timezone .utc ),
954- updated = datetime .now (timezone .utc ),
971+ created = datetime .now (timezone .utc ). isoformat () ,
972+ updated = datetime .now (timezone .utc ). isoformat () ,
955973 steps = [
956974 FunctionCallStep (
957975 type = 'function_call' ,
@@ -1205,6 +1223,23 @@ def test_unknown_event_type_returns_none(self):
12051223 assert result is None
12061224 assert not aggregated_parts
12071225
1226+ def test_completed_event_failed_partial_interaction (self ):
1227+ """A failed lifecycle event with a partial interaction does not crash."""
1228+ event = InteractionCompletedEvent (
1229+ event_type = 'interaction.completed' ,
1230+ interaction = InteractionSseEventInteraction (
1231+ id = 'int_failed' ,
1232+ status = 'failed' ,
1233+ steps = [],
1234+ ),
1235+ )
1236+ result = interactions_utils .convert_interaction_event_to_llm_response (
1237+ event , aggregated_parts = [], interaction_id = 'int_failed'
1238+ )
1239+ assert result is not None
1240+ assert result .error_code == 'UNKNOWN_ERROR'
1241+ assert result .interaction_id == 'int_failed'
1242+
12081243 def test_function_call_streaming_flow (self ):
12091244 """Test the complete streaming flow for function calls (Start, Delta, Stop)."""
12101245 # 1. StepStart
0 commit comments