diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index 9fb02d865d..d5d5d6b071 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -450,19 +450,27 @@ async def _content_to_message_param( ) -> Union[Message, list[Message]]: """Converts a types.Content to a litellm Message or list of Messages. - Handles multipart function responses by returning a list of - ChatCompletionToolMessage objects if multiple function_response parts exist. + This function processes a `types.Content` object, which may contain multiple + parts, and converts them into a format suitable for LiteLLM. It handles + mixed content, such as tool responses alongside text or other media, by + generating a list of messages. + + - `function_response` parts are converted into `tool` role messages. + - Other parts (text, images, etc.) are grouped and converted into a + single `user` or `assistant` message. Args: content: The content to convert. provider: The LLM provider name (e.g., "openai", "azure"). Returns: - A litellm Message, a list of litellm Messages. + A single litellm Message, a list of litellm Messages, or an empty list if + the content is empty. """ - - tool_messages: list[Message] = [] - non_tool_parts: list[types.Part] = [] + tool_messages = [] + other_parts = [] + + # 1. Separate function responses from other content (Text, Images, etc.) for part in content.parts: if part.function_response: response = part.function_response.response @@ -479,82 +487,88 @@ async def _content_to_message_param( ) ) else: - non_tool_parts.append(part) + other_parts.append(part) - if tool_messages and not non_tool_parts: + # 2. If ONLY tools are present, return them immediately (Original behavior) + if tool_messages and not other_parts: return tool_messages if len(tool_messages) > 1 else tool_messages[0] - if tool_messages and non_tool_parts: - follow_up = await _content_to_message_param( - types.Content(role=content.role, parts=non_tool_parts), - provider=provider, - ) - follow_up_messages = ( - follow_up if isinstance(follow_up, list) else [follow_up] - ) - return tool_messages + follow_up_messages - - # Handle user or assistant messages - role = _to_litellm_role(content.role) - - if role == "user": - user_parts = [part for part in content.parts if not part.thought] - message_content = await _get_content(user_parts, provider=provider) or None - return ChatCompletionUserMessage(role="user", content=message_content) - else: # assistant/model - tool_calls = [] - content_parts: list[types.Part] = [] - reasoning_parts: list[types.Part] = [] - for part in content.parts: - if part.function_call: - tool_calls.append( - ChatCompletionAssistantToolCall( - type="function", - id=part.function_call.id, - function=Function( - name=part.function_call.name, - arguments=_safe_json_serialize(part.function_call.args), - ), - ) - ) - elif part.thought: - reasoning_parts.append(part) - else: - content_parts.append(part) + # 3. Handle user or assistant messages for any non-tool parts + extra_message = None + if other_parts: + role = _to_litellm_role(content.role) + + if role == "user": + # Original logic for User messages + user_parts = [part for part in other_parts if not part.thought] + message_content = await _get_content(user_parts, provider=provider) or None + extra_message = ChatCompletionUserMessage(role="user", content=message_content) + + else: # assistant/model + # Original logic for Assistant messages (Tool calls + Reasoning) + tool_calls = [] + content_parts: list[types.Part] = [] + reasoning_parts: list[types.Part] = [] + for part in other_parts: + if part.function_call: + tool_calls.append( + ChatCompletionAssistantToolCall( + type="function", + id=part.function_call.id, + function=Function( + name=part.function_call.name, + arguments=_safe_json_serialize(part.function_call.args), + ), + ) + ) + elif part.thought: + reasoning_parts.append(part) + else: + content_parts.append(part) - final_content = ( - await _get_content(content_parts, provider=provider) - if content_parts - else None - ) - if final_content and isinstance(final_content, list): - # when the content is a single text object, we can use it directly. - # this is needed for ollama_chat provider which fails if content is a list final_content = ( - final_content[0].get("text", "") - if final_content[0].get("type", None) == "text" - else final_content + await _get_content(content_parts, provider=provider) + if content_parts + else None ) + + # Handle Ollama specific formatting from original code + if final_content and isinstance(final_content, list): + final_content = ( + final_content[0].get("text", "") + if final_content[0].get("type", None) == "text" + else final_content + ) - reasoning_texts = [] - for part in reasoning_parts: - if part.text: - reasoning_texts.append(part.text) - elif ( - part.inline_data - and part.inline_data.data - and part.inline_data.mime_type - and part.inline_data.mime_type.startswith("text/") - ): - reasoning_texts.append(_decode_inline_text_data(part.inline_data.data)) - - reasoning_content = _NEW_LINE.join(text for text in reasoning_texts if text) - return ChatCompletionAssistantMessage( - role=role, - content=final_content, - tool_calls=tool_calls or None, - reasoning_content=reasoning_content or None, - ) + # Handle reasoning/thoughts using original logic + reasoning_texts = [] + for part in reasoning_parts: + if part.text: + reasoning_texts.append(part.text) + elif ( + part.inline_data + and part.inline_data.data + and part.inline_data.mime_type + and part.inline_data.mime_type.startswith("text/") + ): + reasoning_texts.append(_decode_inline_text_data(part.inline_data.data)) + + reasoning_content = _NEW_LINE.join(text for text in reasoning_texts if text) + + extra_message = ChatCompletionAssistantMessage( + role=role, + content=final_content, + tool_calls=tool_calls or None, + reasoning_content=reasoning_content or None, + ) + + # 4. Final step: Combine tool results and the extra message (Original logic fix) + final_result = tool_messages + ([extra_message] if extra_message else []) + + if not final_result: + return [] + + return final_result if len(final_result) > 1 else final_result[0] def _ensure_tool_results(messages: List[Message]) -> List[Message]: