diff --git a/src/google/adk/models/apigee_llm.py b/src/google/adk/models/apigee_llm.py index 458c208522..94d368bf07 100644 --- a/src/google/adk/models/apigee_llm.py +++ b/src/google/adk/models/apigee_llm.py @@ -820,10 +820,16 @@ def _content_to_messages( _function_response_media_content_parts(part.function_response) ) if function_responses: - if response_media_parts: + # Any other parts in the same content (e.g. text the user sent with the + # tool result) cannot go in a tool-role message either, so they follow + # the tool results together with the media. + follow_up_parts = response_media_parts + content_parts + if len(follow_up_parts) == 1 and follow_up_parts[0]['type'] == 'text': function_responses.append( - {'role': 'user', 'content': response_media_parts} + {'role': 'user', 'content': follow_up_parts[0]['text']} ) + elif follow_up_parts: + function_responses.append({'role': 'user', 'content': follow_up_parts}) return function_responses message: dict[str, Any] = {'role': role} diff --git a/tests/unittests/models/test_apigee_llm.py b/tests/unittests/models/test_apigee_llm.py index 527b46c2a5..9177fdf705 100644 --- a/tests/unittests/models/test_apigee_llm.py +++ b/tests/unittests/models/test_apigee_llm.py @@ -1209,6 +1209,71 @@ def test_content_conversion_carries_function_response_media() -> None: ] +def test_content_conversion_keeps_text_alongside_function_response() -> None: + """Text sent with a tool result follows the tool message as a user turn.""" + part = types.Part.from_function_response( + name='lookup', response={'status': 'ok'} + ) + part.function_response.id = 'call_1' + content = types.Content( + role='user', + parts=[part, types.Part.from_text(text='Summarize it in French.')], + ) + + client = CompletionsHTTPClient(base_url='http://test') + messages = client._content_to_messages(content) + + assert messages == [ + { + 'role': 'tool', + 'tool_call_id': 'call_1', + 'content': '{"status": "ok"}', + }, + {'role': 'user', 'content': 'Summarize it in French.'}, + ] + + +def test_content_conversion_keeps_text_alongside_function_response_media() -> ( + None +): + """Tool media and accompanying text share one follow-up user message.""" + part = types.Part.from_function_response( + name='draw_chart', + response={'title': 'Revenue'}, + parts=[ + types.FunctionResponsePart.from_bytes( + data=b'chart', mime_type='image/png' + ) + ], + ) + part.function_response.id = 'call_1' + content = types.Content( + role='user', + parts=[part, types.Part.from_text(text='Describe the chart.')], + ) + + client = CompletionsHTTPClient(base_url='http://test') + messages = client._content_to_messages(content) + + assert messages == [ + { + 'role': 'tool', + 'tool_call_id': 'call_1', + 'content': '{"title": "Revenue"}', + }, + { + 'role': 'user', + 'content': [ + { + 'type': 'image_url', + 'image_url': {'url': 'data:image/png;base64,Y2hhcnQ='}, + }, + {'type': 'text', 'text': 'Describe the chart.'}, + ], + }, + ] + + def test_content_conversion_without_function_response_media() -> None: """A response carrying no media still converts to a lone tool message.""" part = types.Part.from_function_response(