Skip to content

Commit ef816e3

Browse files
committed
fix(models): keep text sent alongside a function response in ApigeeLlm
In the Chat Completions path, _content_to_messages returned only the tool messages (plus tool-attached media) when a Content held a function_response, silently dropping any text or media parts sent with it. Those parts now follow the tool messages in a single user message, matching LiteLlm. Fixes #7475
1 parent 3d11f9f commit ef816e3

2 files changed

Lines changed: 73 additions & 2 deletions

File tree

‎src/google/adk/models/apigee_llm.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -820,10 +820,16 @@ def _content_to_messages(
820820
_function_response_media_content_parts(part.function_response)
821821
)
822822
if function_responses:
823-
if response_media_parts:
823+
# Any other parts in the same content (e.g. text the user sent with the
824+
# tool result) cannot go in a tool-role message either, so they follow
825+
# the tool results together with the media.
826+
follow_up_parts = response_media_parts + content_parts
827+
if len(follow_up_parts) == 1 and follow_up_parts[0]['type'] == 'text':
824828
function_responses.append(
825-
{'role': 'user', 'content': response_media_parts}
829+
{'role': 'user', 'content': follow_up_parts[0]['text']}
826830
)
831+
elif follow_up_parts:
832+
function_responses.append({'role': 'user', 'content': follow_up_parts})
827833
return function_responses
828834

829835
message: dict[str, Any] = {'role': role}

‎tests/unittests/models/test_apigee_llm.py‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,71 @@ def test_content_conversion_carries_function_response_media() -> None:
12091209
]
12101210

12111211

1212+
def test_content_conversion_keeps_text_alongside_function_response() -> None:
1213+
"""Text sent with a tool result follows the tool message as a user turn."""
1214+
part = types.Part.from_function_response(
1215+
name='lookup', response={'status': 'ok'}
1216+
)
1217+
part.function_response.id = 'call_1'
1218+
content = types.Content(
1219+
role='user',
1220+
parts=[part, types.Part.from_text(text='Summarize it in French.')],
1221+
)
1222+
1223+
client = CompletionsHTTPClient(base_url='http://test')
1224+
messages = client._content_to_messages(content)
1225+
1226+
assert messages == [
1227+
{
1228+
'role': 'tool',
1229+
'tool_call_id': 'call_1',
1230+
'content': '{"status": "ok"}',
1231+
},
1232+
{'role': 'user', 'content': 'Summarize it in French.'},
1233+
]
1234+
1235+
1236+
def test_content_conversion_keeps_text_alongside_function_response_media() -> (
1237+
None
1238+
):
1239+
"""Tool media and accompanying text share one follow-up user message."""
1240+
part = types.Part.from_function_response(
1241+
name='draw_chart',
1242+
response={'title': 'Revenue'},
1243+
parts=[
1244+
types.FunctionResponsePart.from_bytes(
1245+
data=b'chart', mime_type='image/png'
1246+
)
1247+
],
1248+
)
1249+
part.function_response.id = 'call_1'
1250+
content = types.Content(
1251+
role='user',
1252+
parts=[part, types.Part.from_text(text='Describe the chart.')],
1253+
)
1254+
1255+
client = CompletionsHTTPClient(base_url='http://test')
1256+
messages = client._content_to_messages(content)
1257+
1258+
assert messages == [
1259+
{
1260+
'role': 'tool',
1261+
'tool_call_id': 'call_1',
1262+
'content': '{"title": "Revenue"}',
1263+
},
1264+
{
1265+
'role': 'user',
1266+
'content': [
1267+
{
1268+
'type': 'image_url',
1269+
'image_url': {'url': 'data:image/png;base64,Y2hhcnQ='},
1270+
},
1271+
{'type': 'text', 'text': 'Describe the chart.'},
1272+
],
1273+
},
1274+
]
1275+
1276+
12121277
def test_content_conversion_without_function_response_media() -> None:
12131278
"""A response carrying no media still converts to a lone tool message."""
12141279
part = types.Part.from_function_response(

0 commit comments

Comments
 (0)