From 728ef46bdd918456b3f090f30d0e2242b2836094 Mon Sep 17 00:00:00 2001 From: StreetLamb Date: Thu, 3 Jul 2025 22:40:48 +0800 Subject: [PATCH 1/4] feat: Add MessageConverter protocol for multipart converters --- .../workflows/llm/augmented_llm_anthropic.py | 4 +-- .../workflows/llm/augmented_llm_azure.py | 2 +- .../workflows/llm/augmented_llm_bedrock.py | 2 +- .../workflows/llm/augmented_llm_google.py | 2 +- .../workflows/llm/augmented_llm_openai.py | 2 +- .../workflows/llm/multipart_converter.py | 25 ++++++++++++++ .../llm/multipart_converter_anthropic.py | 22 ++++++------- .../llm/multipart_converter_azure.py | 31 ++++++++++------- .../llm/multipart_converter_bedrock.py | 17 +++++----- .../llm/multipart_converter_google.py | 22 +++++++++---- .../llm/multipart_converter_openai.py | 33 ++++++++++++------- .../test_multipart_converter_anthropic.py | 12 +++---- tests/utils/test_multipart_converter_azure.py | 12 +++---- .../utils/test_multipart_converter_bedrock.py | 10 +++--- .../utils/test_multipart_converter_google.py | 12 +++---- .../utils/test_multipart_converter_openai.py | 10 +++--- uv.lock | 2 +- 17 files changed, 133 insertions(+), 87 deletions(-) create mode 100644 src/mcp_agent/workflows/llm/multipart_converter.py diff --git a/src/mcp_agent/workflows/llm/augmented_llm_anthropic.py b/src/mcp_agent/workflows/llm/augmented_llm_anthropic.py index 88db1698c..d71da6072 100644 --- a/src/mcp_agent/workflows/llm/augmented_llm_anthropic.py +++ b/src/mcp_agent/workflows/llm/augmented_llm_anthropic.py @@ -155,9 +155,7 @@ async def generate( if params.use_history: messages.extend(self.history.get()) - messages.extend( - AnthropicConverter.convert_mixed_messages_to_anthropic(message) - ) + messages.extend(AnthropicConverter.from_mixed_messages(message)) list_tools_result = await self.agent.list_tools() available_tools: List[ToolParam] = [ diff --git a/src/mcp_agent/workflows/llm/augmented_llm_azure.py b/src/mcp_agent/workflows/llm/augmented_llm_azure.py index 1a04353a2..792a382ca 100644 --- a/src/mcp_agent/workflows/llm/augmented_llm_azure.py +++ b/src/mcp_agent/workflows/llm/augmented_llm_azure.py @@ -153,7 +153,7 @@ async def generate(self, message, request_params: RequestParams | None = None): messages.append(SystemMessage(content=system_prompt)) span.set_attribute("system_prompt", system_prompt) - messages.extend(AzureConverter.convert_mixed_messages_to_azure(message)) + messages.extend(AzureConverter.from_mixed_messages(message)) response = await self.agent.list_tools() diff --git a/src/mcp_agent/workflows/llm/augmented_llm_bedrock.py b/src/mcp_agent/workflows/llm/augmented_llm_bedrock.py index 5f4fcaf81..1e34797f6 100644 --- a/src/mcp_agent/workflows/llm/augmented_llm_bedrock.py +++ b/src/mcp_agent/workflows/llm/augmented_llm_bedrock.py @@ -101,7 +101,7 @@ async def generate(self, message, request_params: RequestParams | None = None): if params.use_history: messages.extend(self.history.get()) - messages.extend(BedrockConverter.convert_mixed_messages_to_bedrock(message)) + messages.extend(BedrockConverter.from_mixed_messages(message)) response = await self.agent.list_tools() diff --git a/src/mcp_agent/workflows/llm/augmented_llm_google.py b/src/mcp_agent/workflows/llm/augmented_llm_google.py index 2d3783cb2..17f7a46f7 100644 --- a/src/mcp_agent/workflows/llm/augmented_llm_google.py +++ b/src/mcp_agent/workflows/llm/augmented_llm_google.py @@ -85,7 +85,7 @@ async def generate(self, message, request_params: RequestParams | None = None): if params.use_history: messages.extend(self.history.get()) - messages.extend(GoogleConverter.convert_mixed_messages_to_google(message)) + messages.extend(GoogleConverter.from_mixed_messages(message)) response = await self.agent.list_tools() diff --git a/src/mcp_agent/workflows/llm/augmented_llm_openai.py b/src/mcp_agent/workflows/llm/augmented_llm_openai.py index e082918c4..41282e0d5 100644 --- a/src/mcp_agent/workflows/llm/augmented_llm_openai.py +++ b/src/mcp_agent/workflows/llm/augmented_llm_openai.py @@ -182,7 +182,7 @@ async def generate( role="system", content=system_prompt ) ) - messages.extend((OpenAIConverter.convert_mixed_messages_to_openai(message))) + messages.extend((OpenAIConverter.from_mixed_messages(message))) response: ListToolsResult = await self.agent.list_tools() available_tools: List[ChatCompletionToolParam] = [ diff --git a/src/mcp_agent/workflows/llm/multipart_converter.py b/src/mcp_agent/workflows/llm/multipart_converter.py new file mode 100644 index 000000000..0bd78618d --- /dev/null +++ b/src/mcp_agent/workflows/llm/multipart_converter.py @@ -0,0 +1,25 @@ +from typing import Generic, Protocol + +from mcp_agent.utils.prompt_message_multipart import PromptMessageMultipart +from mcp.types import PromptMessage +from mcp_agent.workflows.llm.augmented_llm import MessageTypes +from mcp_agent.workflows.llm.augmented_llm import MessageParamT, MessageT + + +class MessageConverter(Protocol, Generic[MessageParamT, MessageT]): + @staticmethod + def from_prompt_message_multipart( + multipart_msg: PromptMessageMultipart, concatenate_text_blocks: bool = False + ) -> MessageParamT: + """Convert a PromptMessageMultipart to a Provider-compatible message param type""" + ... + + @staticmethod + def from_prompt_message(message: PromptMessage) -> MessageParamT: + """Convert a PromptMessage to a Provider-compatible message param type""" + ... + + @staticmethod + def from_mixed_messages(message: MessageTypes) -> list[MessageParamT]: + """Convert a mixed message type to a list of Provider-compatible message param types""" + ... diff --git a/src/mcp_agent/workflows/llm/multipart_converter_anthropic.py b/src/mcp_agent/workflows/llm/multipart_converter_anthropic.py index 2c4e60913..abe9f211d 100644 --- a/src/mcp_agent/workflows/llm/multipart_converter_anthropic.py +++ b/src/mcp_agent/workflows/llm/multipart_converter_anthropic.py @@ -12,6 +12,7 @@ ToolResultBlockParam, URLImageSourceParam, URLPDFSourceParam, + Message, ) from mcp.types import ( BlobResourceContents, @@ -40,6 +41,7 @@ from mcp_agent.utils.prompt_message_multipart import PromptMessageMultipart from mcp_agent.utils.resource_utils import extract_title_from_uri from mcp_agent.workflows.llm.augmented_llm import MessageTypes +from mcp_agent.workflows.llm.multipart_converter import MessageConverter _logger = get_logger("multipart_converter_anthropic") @@ -47,7 +49,7 @@ SUPPORTED_IMAGE_MIME_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"} -class AnthropicConverter: +class AnthropicConverter(MessageConverter[MessageParam, Message]): """Converts MCP message types to Anthropic API format.""" @staticmethod @@ -63,7 +65,9 @@ def _is_supported_image_type(mime_type: str) -> bool: return mime_type in SUPPORTED_IMAGE_MIME_TYPES @staticmethod - def convert_to_anthropic(multipart_msg: PromptMessageMultipart) -> MessageParam: + def from_prompt_message_multipart( + multipart_msg: PromptMessageMultipart, + ) -> MessageParam: """ Convert a PromptMessageMultipart message to Anthropic API format. @@ -100,7 +104,7 @@ def convert_to_anthropic(multipart_msg: PromptMessageMultipart) -> MessageParam: return MessageParam(role=role, content=anthropic_blocks) @staticmethod - def convert_prompt_message_to_anthropic(message: PromptMessage) -> MessageParam: + def from_prompt_message(message: PromptMessage) -> MessageParam: """ Convert a standard PromptMessage to Anthropic API format. @@ -114,7 +118,7 @@ def convert_prompt_message_to_anthropic(message: PromptMessage) -> MessageParam: multipart = PromptMessageMultipart(role=message.role, content=[message.content]) # Use the existing conversion method - return AnthropicConverter.convert_to_anthropic(multipart) + return AnthropicConverter.from_prompt_message_multipart(multipart) @staticmethod def _convert_content_items( @@ -482,7 +486,7 @@ def create_tool_results_message( return MessageParam(role="user", content=content_blocks) @staticmethod - def convert_mixed_messages_to_anthropic( + def from_mixed_messages( message: MessageTypes, ) -> List[MessageParam]: """ @@ -499,15 +503,11 @@ def convert_mixed_messages_to_anthropic( if isinstance(message, str): messages.append(MessageParam(role="user", content=message)) elif isinstance(message, PromptMessage): - messages.append( - AnthropicConverter.convert_prompt_message_to_anthropic(message) - ) + messages.append(AnthropicConverter.from_prompt_message(message)) elif isinstance(message, list): for m in message: if isinstance(m, PromptMessage): - messages.append( - AnthropicConverter.convert_prompt_message_to_anthropic(m) - ) + messages.append(AnthropicConverter.from_prompt_message(m)) elif isinstance(m, str): messages.append(MessageParam(role="user", content=m)) else: diff --git a/src/mcp_agent/workflows/llm/multipart_converter_azure.py b/src/mcp_agent/workflows/llm/multipart_converter_azure.py index 3785fbf1e..1e9264b40 100644 --- a/src/mcp_agent/workflows/llm/multipart_converter_azure.py +++ b/src/mcp_agent/workflows/llm/multipart_converter_azure.py @@ -11,6 +11,7 @@ AssistantMessage, ToolMessage, DeveloperMessage, + ChatResponseMessage, ) from mcp.types import ( BlobResourceContents, @@ -38,13 +39,23 @@ from mcp_agent.utils.prompt_message_multipart import PromptMessageMultipart from mcp_agent.utils.resource_utils import extract_title_from_uri from mcp_agent.workflows.llm.augmented_llm import MessageTypes +from mcp_agent.workflows.llm.multipart_converter import MessageConverter _logger = get_logger("multipart_converter_azure") +AzureMessageParam = Union[ + SystemMessage, UserMessage, AssistantMessage, ToolMessage, DeveloperMessage +] + SUPPORTED_IMAGE_MIME_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"} -class AzureConverter: +class AzureConverter( + MessageConverter[ + AzureMessageParam, + ChatResponseMessage, + ] +): """Converts MCP message types to Azure API format.""" @staticmethod @@ -52,7 +63,7 @@ def _is_supported_image_type(mime_type: str) -> bool: return mime_type in SUPPORTED_IMAGE_MIME_TYPES @staticmethod - def convert_to_azure( + def from_prompt_message_multipart( multipart_msg: PromptMessageMultipart, ) -> UserMessage | AssistantMessage: """ @@ -92,7 +103,7 @@ def convert_to_azure( return UserMessage(content=content) @staticmethod - def convert_prompt_message_to_azure( + def from_prompt_message( message: PromptMessage, ) -> UserMessage | AssistantMessage: """ @@ -105,7 +116,7 @@ def convert_prompt_message_to_azure( An Azure UserMessage or AssistantMessage object """ multipart = PromptMessageMultipart(role=message.role, content=[message.content]) - return AzureConverter.convert_to_azure(multipart) + return AzureConverter.from_prompt_message_multipart(multipart) @staticmethod def _convert_content_items( @@ -329,13 +340,9 @@ def create_tool_results_message( return tool_messages @staticmethod - def convert_mixed_messages_to_azure( + def from_mixed_messages( message: MessageTypes, - ) -> List[ - Union[ - SystemMessage, UserMessage, AssistantMessage, ToolMessage, DeveloperMessage - ] - ]: + ) -> List[AzureMessageParam]: """ Convert a list of mixed messages to a list of Azure-compatible messages. @@ -351,11 +358,11 @@ def convert_mixed_messages_to_azure( if isinstance(message, str): messages.append(UserMessage(content=message)) elif isinstance(message, PromptMessage): - messages.append(AzureConverter.convert_prompt_message_to_azure(message)) + messages.append(AzureConverter.from_prompt_message(message)) elif isinstance(message, list): for m in message: if isinstance(m, PromptMessage): - messages.append(AzureConverter.convert_prompt_message_to_azure(m)) + messages.append(AzureConverter.from_prompt_message(m)) elif isinstance(m, str): messages.append(UserMessage(content=m)) else: diff --git a/src/mcp_agent/workflows/llm/multipart_converter_bedrock.py b/src/mcp_agent/workflows/llm/multipart_converter_bedrock.py index 86d7969a2..f8b7711f5 100644 --- a/src/mcp_agent/workflows/llm/multipart_converter_bedrock.py +++ b/src/mcp_agent/workflows/llm/multipart_converter_bedrock.py @@ -27,6 +27,7 @@ from mcp_agent.utils.prompt_message_multipart import PromptMessageMultipart from mcp_agent.utils.resource_utils import extract_title_from_uri from mcp_agent.workflows.llm.augmented_llm import MessageTypes +from mcp_agent.workflows.llm.multipart_converter import MessageConverter if TYPE_CHECKING: from mypy_boto3_bedrock_runtime.type_defs import ( @@ -44,7 +45,7 @@ SUPPORTED_IMAGE_MIME_TYPES = {"image/jpeg", "image/png"} -class BedrockConverter: +class BedrockConverter(MessageConverter[MessageUnionTypeDef, MessageUnionTypeDef]): """Converts MCP message types to Amazon Bedrock API format.""" @staticmethod @@ -53,7 +54,7 @@ def _is_supported_image_type(mime_type: str) -> bool: return mime_type in SUPPORTED_IMAGE_MIME_TYPES @staticmethod - def convert_to_bedrock( + def from_prompt_message_multipart( multipart_msg: PromptMessageMultipart, ) -> MessageUnionTypeDef: """ @@ -68,14 +69,14 @@ def convert_to_bedrock( return {"role": role, "content": bedrock_blocks} @staticmethod - def convert_prompt_message_to_bedrock( + def from_prompt_message( message: PromptMessage, ) -> MessageUnionTypeDef: """ Convert a standard PromptMessage to Bedrock API format. """ multipart = PromptMessageMultipart(role=message.role, content=[message.content]) - return BedrockConverter.convert_to_bedrock(multipart) + return BedrockConverter.from_prompt_message_multipart(multipart) @staticmethod def _convert_content_items( @@ -268,7 +269,7 @@ def create_tool_results_message( return {"role": "user", "content": content_blocks} @staticmethod - def convert_mixed_messages_to_bedrock( + def from_mixed_messages( message: MessageTypes, ) -> List[MessageUnionTypeDef]: """ @@ -286,13 +287,11 @@ def convert_mixed_messages_to_bedrock( if isinstance(message, str): messages.append({"role": "user", "content": [{"text": message}]}) elif isinstance(message, PromptMessage): - messages.append(BedrockConverter.convert_prompt_message_to_bedrock(message)) + messages.append(BedrockConverter.from_prompt_message(message)) elif isinstance(message, list): for m in message: if isinstance(m, PromptMessage): - messages.append( - BedrockConverter.convert_prompt_message_to_bedrock(m) - ) + messages.append(BedrockConverter.from_prompt_message(m)) elif isinstance(m, str): messages.append({"role": "user", "content": [{"text": m}]}) else: diff --git a/src/mcp_agent/workflows/llm/multipart_converter_google.py b/src/mcp_agent/workflows/llm/multipart_converter_google.py index 13ab1565b..aecc769ef 100644 --- a/src/mcp_agent/workflows/llm/multipart_converter_google.py +++ b/src/mcp_agent/workflows/llm/multipart_converter_google.py @@ -29,6 +29,7 @@ from mcp_agent.utils.prompt_message_multipart import PromptMessageMultipart from mcp_agent.utils.resource_utils import extract_title_from_uri from mcp_agent.workflows.llm.augmented_llm import MessageTypes +from mcp_agent.workflows.llm.multipart_converter import MessageConverter _logger = get_logger("multipart_converter_google") @@ -36,7 +37,12 @@ SUPPORTED_IMAGE_MIME_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"} -class GoogleConverter: +class GoogleConverter( + MessageConverter[ + types.Content, + types.Content, + ] +): """Converts MCP message types to Google API format.""" @staticmethod @@ -52,7 +58,9 @@ def _is_supported_image_type(mime_type: str) -> bool: return mime_type in SUPPORTED_IMAGE_MIME_TYPES @staticmethod - def convert_to_google(multipart_msg: PromptMessageMultipart) -> types.Content: + def from_prompt_message_multipart( + multipart_msg: PromptMessageMultipart, + ) -> types.Content: """ Convert a PromptMessageMultipart message to Google API format. @@ -73,7 +81,7 @@ def convert_to_google(multipart_msg: PromptMessageMultipart) -> types.Content: return types.Content(role=role, parts=google_parts) @staticmethod - def convert_prompt_message_to_google(message: PromptMessage) -> types.Content: + def from_prompt_message(message: PromptMessage) -> types.Content: """ Convert a standard PromptMessage to Google API format. @@ -84,7 +92,7 @@ def convert_prompt_message_to_google(message: PromptMessage) -> types.Content: A Google API Content object """ multipart = PromptMessageMultipart(role=message.role, content=[message.content]) - return GoogleConverter.convert_to_google(multipart) + return GoogleConverter.from_prompt_message_multipart(multipart) @staticmethod def _convert_content_items( @@ -337,7 +345,7 @@ def create_tool_results_message( return types.Content(role="user", parts=parts) @staticmethod - def convert_mixed_messages_to_google( + def from_mixed_messages( message: MessageTypes, ) -> List[types.Content]: """ @@ -357,11 +365,11 @@ def convert_mixed_messages_to_google( types.Content(role="user", parts=[types.Part.from_text(text=message)]) ) elif isinstance(message, PromptMessage): - messages.append(GoogleConverter.convert_prompt_message_to_google(message)) + messages.append(GoogleConverter.from_prompt_message(message)) elif isinstance(message, list): for m in message: if isinstance(m, PromptMessage): - messages.append(GoogleConverter.convert_prompt_message_to_google(m)) + messages.append(GoogleConverter.from_prompt_message(m)) elif isinstance(m, str): messages.append( types.Content(role="user", parts=[types.Part.from_text(text=m)]) diff --git a/src/mcp_agent/workflows/llm/multipart_converter_openai.py b/src/mcp_agent/workflows/llm/multipart_converter_openai.py index b817c52e4..1a4454bd7 100644 --- a/src/mcp_agent/workflows/llm/multipart_converter_openai.py +++ b/src/mcp_agent/workflows/llm/multipart_converter_openai.py @@ -7,7 +7,11 @@ PromptMessage, TextContent, ) -from openai.types.chat import ChatCompletionMessageParam, ChatCompletionUserMessageParam +from openai.types.chat import ( + ChatCompletionMessageParam, + ChatCompletionUserMessageParam, + ChatCompletionMessage, +) from mcp_agent.logging.logger import get_logger from mcp_agent.utils.content_utils import ( @@ -26,15 +30,18 @@ from mcp_agent.utils.prompt_message_multipart import PromptMessageMultipart from mcp_agent.utils.resource_utils import extract_title_from_uri from mcp_agent.workflows.llm.augmented_llm import MessageTypes +from mcp_agent.workflows.llm.multipart_converter import MessageConverter _logger = get_logger("multipart_converter_openai") # Define type aliases for content blocks ContentBlock = Dict[str, Any] -OpenAIMessage = Dict[str, Any] +OpenAIMessage = Dict[str, str | ContentBlock | List[ContentBlock]] -class OpenAIConverter: +class OpenAIConverter( + MessageConverter[ChatCompletionMessageParam, ChatCompletionMessage] +): """Converts MCP message types to OpenAI API format.""" @staticmethod @@ -55,9 +62,9 @@ def _is_supported_image_type(mime_type: str) -> bool: ) @staticmethod - def convert_to_openai( + def from_prompt_message_multipart( multipart_msg: PromptMessageMultipart, concatenate_text_blocks: bool = False - ) -> Dict[str, str | ContentBlock | List[ContentBlock]]: + ) -> ChatCompletionMessageParam: """ Convert a PromptMessageMultipart message to OpenAI API format. @@ -158,7 +165,7 @@ def _concatenate_text_blocks(blocks: List[ContentBlock]) -> List[ContentBlock]: return combined_blocks @staticmethod - def convert_prompt_message_to_openai( + def from_prompt_message( message: PromptMessage, concatenate_text_blocks: bool = False ) -> ChatCompletionMessageParam: """ @@ -175,7 +182,9 @@ def convert_prompt_message_to_openai( multipart = PromptMessageMultipart(role=message.role, content=[message.content]) # Use the existing conversion method with the specified concatenation option - return OpenAIConverter.convert_to_openai(multipart, concatenate_text_blocks) + return OpenAIConverter.from_prompt_message_multipart( + multipart, concatenate_text_blocks + ) @staticmethod def _convert_image_content(content: ImageContent) -> ContentBlock: @@ -392,7 +401,7 @@ def convert_tool_result_to_openai( if text_content: # Convert text content to OpenAI format temp_multipart = PromptMessageMultipart(role="user", content=text_content) - converted = OpenAIConverter.convert_to_openai( + converted = OpenAIConverter.from_prompt_message_multipart( temp_multipart, concatenate_text_blocks=concatenate_text_blocks ) @@ -421,7 +430,7 @@ def convert_tool_result_to_openai( ) # Convert to OpenAI format - user_message = OpenAIConverter.convert_to_openai(non_text_multipart) + user_message = OpenAIConverter.from_prompt_message_multipart(non_text_multipart) # We need to add tool_call_id manually user_message["tool_call_id"] = tool_call_id @@ -464,7 +473,7 @@ def convert_function_results_to_openai( return messages @staticmethod - def convert_mixed_messages_to_openai( + def from_mixed_messages( message: MessageTypes, ) -> List[ChatCompletionMessageParam]: """ @@ -483,11 +492,11 @@ def convert_mixed_messages_to_openai( ChatCompletionUserMessageParam(role="user", content=message) ) elif isinstance(message, PromptMessage): - messages.append(OpenAIConverter.convert_prompt_message_to_openai(message)) + messages.append(OpenAIConverter.from_prompt_message(message)) elif isinstance(message, list): for m in message: if isinstance(m, PromptMessage): - messages.append(OpenAIConverter.convert_prompt_message_to_openai(m)) + messages.append(OpenAIConverter.from_prompt_message(m)) elif isinstance(m, str): messages.append( ChatCompletionUserMessageParam(role="user", content=m) diff --git a/tests/utils/test_multipart_converter_anthropic.py b/tests/utils/test_multipart_converter_anthropic.py index 2e6b00904..25c178e76 100644 --- a/tests/utils/test_multipart_converter_anthropic.py +++ b/tests/utils/test_multipart_converter_anthropic.py @@ -28,7 +28,7 @@ def test_is_supported_image_type_unsupported(self): def test_convert_to_anthropic_empty_content(self): multipart = PromptMessageMultipart(role="user", content=[]) - result = AnthropicConverter.convert_to_anthropic(multipart) + result = AnthropicConverter.from_prompt_message_multipart(multipart) assert result["role"] == "user" assert result["content"] == [] @@ -36,7 +36,7 @@ def test_convert_to_anthropic_empty_content(self): def test_convert_to_anthropic_text_content(self): content = [TextContent(type="text", text="Hello, world!")] multipart = PromptMessageMultipart(role="user", content=content) - result = AnthropicConverter.convert_to_anthropic(multipart) + result = AnthropicConverter.from_prompt_message_multipart(multipart) assert result["role"] == "user" assert len(result["content"]) == 1 @@ -46,7 +46,7 @@ def test_convert_to_anthropic_text_content(self): def test_convert_to_anthropic_image_content_supported(self): content = [ImageContent(type="image", data="base64data", mimeType="image/png")] multipart = PromptMessageMultipart(role="user", content=content) - result = AnthropicConverter.convert_to_anthropic(multipart) + result = AnthropicConverter.from_prompt_message_multipart(multipart) assert result["role"] == "user" assert len(result["content"]) == 1 @@ -58,7 +58,7 @@ def test_convert_to_anthropic_image_content_supported(self): def test_convert_to_anthropic_image_content_unsupported(self): content = [ImageContent(type="image", data="base64data", mimeType="image/bmp")] multipart = PromptMessageMultipart(role="user", content=content) - result = AnthropicConverter.convert_to_anthropic(multipart) + result = AnthropicConverter.from_prompt_message_multipart(multipart) assert result["role"] == "user" assert len(result["content"]) == 1 @@ -71,7 +71,7 @@ def test_convert_to_anthropic_assistant_filters_non_text(self): ImageContent(type="image", data="base64data", mimeType="image/png"), ] multipart = PromptMessageMultipart(role="assistant", content=content) - result = AnthropicConverter.convert_to_anthropic(multipart) + result = AnthropicConverter.from_prompt_message_multipart(multipart) assert result["role"] == "assistant" assert len(result["content"]) == 1 @@ -82,7 +82,7 @@ def test_convert_prompt_message_to_anthropic(self): message = PromptMessage( role="user", content=TextContent(type="text", text="Hello") ) - result = AnthropicConverter.convert_prompt_message_to_anthropic(message) + result = AnthropicConverter.from_prompt_message(message) assert result["role"] == "user" assert len(result["content"]) == 1 diff --git a/tests/utils/test_multipart_converter_azure.py b/tests/utils/test_multipart_converter_azure.py index e50dba204..8b4dfd5f0 100644 --- a/tests/utils/test_multipart_converter_azure.py +++ b/tests/utils/test_multipart_converter_azure.py @@ -28,7 +28,7 @@ def test_is_supported_image_type_unsupported(self): def test_convert_to_azure_empty_content(self): multipart = PromptMessageMultipart(role="user", content=[]) - result = AzureConverter.convert_to_azure(multipart) + result = AzureConverter.from_prompt_message_multipart(multipart) assert result.role == "user" assert result.content == "" @@ -36,7 +36,7 @@ def test_convert_to_azure_empty_content(self): def test_convert_to_azure_text_content(self): content = [TextContent(type="text", text="Hello, world!")] multipart = PromptMessageMultipart(role="user", content=content) - result = AzureConverter.convert_to_azure(multipart) + result = AzureConverter.from_prompt_message_multipart(multipart) assert result.role == "user" assert isinstance(result.content, list) @@ -45,7 +45,7 @@ def test_convert_to_azure_text_content(self): def test_convert_to_azure_image_content_supported(self): content = [ImageContent(type="image", data="base64data", mimeType="image/png")] multipart = PromptMessageMultipart(role="user", content=content) - result = AzureConverter.convert_to_azure(multipart) + result = AzureConverter.from_prompt_message_multipart(multipart) assert result.role == "user" assert isinstance(result.content, list) @@ -54,7 +54,7 @@ def test_convert_to_azure_image_content_supported(self): def test_convert_to_azure_image_content_unsupported(self): content = [ImageContent(type="image", data="base64data", mimeType="image/bmp")] multipart = PromptMessageMultipart(role="user", content=content) - result = AzureConverter.convert_to_azure(multipart) + result = AzureConverter.from_prompt_message_multipart(multipart) assert result.role == "user" assert isinstance(result.content, list) @@ -66,7 +66,7 @@ def test_convert_to_azure_assistant_filters_non_text(self): ImageContent(type="image", data="base64data", mimeType="image/png"), ] multipart = PromptMessageMultipart(role="assistant", content=content) - result = AzureConverter.convert_to_azure(multipart) + result = AzureConverter.from_prompt_message_multipart(multipart) assert result.role == "assistant" assert result.content == "Hello" @@ -75,7 +75,7 @@ def test_convert_prompt_message_to_azure(self): message = PromptMessage( role="user", content=TextContent(type="text", text="Hello") ) - result = AzureConverter.convert_prompt_message_to_azure(message) + result = AzureConverter.from_prompt_message(message) assert result.role == "user" assert isinstance(result.content, list) diff --git a/tests/utils/test_multipart_converter_bedrock.py b/tests/utils/test_multipart_converter_bedrock.py index 3bd835f15..981697000 100644 --- a/tests/utils/test_multipart_converter_bedrock.py +++ b/tests/utils/test_multipart_converter_bedrock.py @@ -28,7 +28,7 @@ def test_is_supported_image_type_unsupported(self): def test_convert_to_bedrock_empty_content(self): multipart = PromptMessageMultipart(role="user", content=[]) - result = BedrockConverter.convert_to_bedrock(multipart) + result = BedrockConverter.from_prompt_message_multipart(multipart) assert result["role"] == "user" assert result["content"] == [] @@ -36,7 +36,7 @@ def test_convert_to_bedrock_empty_content(self): def test_convert_to_bedrock_text_content(self): content = [TextContent(type="text", text="Hello, world!")] multipart = PromptMessageMultipart(role="user", content=content) - result = BedrockConverter.convert_to_bedrock(multipart) + result = BedrockConverter.from_prompt_message_multipart(multipart) assert result["role"] == "user" assert len(result["content"]) == 1 @@ -45,7 +45,7 @@ def test_convert_to_bedrock_text_content(self): def test_convert_to_bedrock_image_content_supported(self): content = [ImageContent(type="image", data="base64data", mimeType="image/png")] multipart = PromptMessageMultipart(role="user", content=content) - result = BedrockConverter.convert_to_bedrock(multipart) + result = BedrockConverter.from_prompt_message_multipart(multipart) assert result["role"] == "user" assert len(result["content"]) == 1 @@ -56,7 +56,7 @@ def test_convert_to_bedrock_image_content_supported(self): def test_convert_to_bedrock_image_content_unsupported(self): content = [ImageContent(type="image", data="base64data", mimeType="image/gif")] multipart = PromptMessageMultipart(role="user", content=content) - result = BedrockConverter.convert_to_bedrock(multipart) + result = BedrockConverter.from_prompt_message_multipart(multipart) assert result["role"] == "user" assert len(result["content"]) == 1 @@ -67,7 +67,7 @@ def test_convert_prompt_message_to_bedrock(self): message = PromptMessage( role="user", content=TextContent(type="text", text="Hello") ) - result = BedrockConverter.convert_prompt_message_to_bedrock(message) + result = BedrockConverter.from_prompt_message(message) assert result["role"] == "user" assert len(result["content"]) == 1 diff --git a/tests/utils/test_multipart_converter_google.py b/tests/utils/test_multipart_converter_google.py index 5dc555e60..88a4f9b2b 100644 --- a/tests/utils/test_multipart_converter_google.py +++ b/tests/utils/test_multipart_converter_google.py @@ -28,7 +28,7 @@ def test_is_supported_image_type_unsupported(self): def test_convert_to_google_empty_content(self): multipart = PromptMessageMultipart(role="user", content=[]) - result = GoogleConverter.convert_to_google(multipart) + result = GoogleConverter.from_prompt_message_multipart(multipart) assert result.role == "user" assert result.parts == [] @@ -44,7 +44,7 @@ def test_convert_to_google_text_content(self): mock_types.Part.from_text.return_value = mock_part mock_types.Content.return_value = Mock(role="user", parts=[mock_part]) - GoogleConverter.convert_to_google(multipart) + GoogleConverter.from_prompt_message_multipart(multipart) mock_types.Part.from_text.assert_called_once_with(text="Hello, world!") @@ -61,7 +61,7 @@ def test_convert_to_google_image_content_supported(self): mock_types.Part.from_bytes.return_value = mock_part mock_types.Content.return_value = Mock(role="user", parts=[mock_part]) - GoogleConverter.convert_to_google(multipart) + GoogleConverter.from_prompt_message_multipart(multipart) # Should call from_bytes with decoded data mock_types.Part.from_bytes.assert_called_once_with( @@ -80,7 +80,7 @@ def test_convert_to_google_image_content_unsupported(self): mock_types.Part.from_text.return_value = mock_part mock_types.Content.return_value = Mock(role="user", parts=[mock_part]) - GoogleConverter.convert_to_google(multipart) + GoogleConverter.from_prompt_message_multipart(multipart) # Should call from_text with fallback message args, kwargs = mock_types.Part.from_text.call_args @@ -97,7 +97,7 @@ def test_convert_to_google_image_content_missing_data(self): mock_types.Part.from_text.return_value = mock_part mock_types.Content.return_value = Mock(role="user", parts=[mock_part]) - GoogleConverter.convert_to_google(multipart) + GoogleConverter.from_prompt_message_multipart(multipart) # Should call from_text with fallback message args, kwargs = mock_types.Part.from_text.call_args @@ -115,7 +115,7 @@ def test_convert_prompt_message_to_google(self): mock_types.Part.from_text.return_value = mock_part mock_types.Content.return_value = Mock(role="user", parts=[mock_part]) - GoogleConverter.convert_prompt_message_to_google(message) + GoogleConverter.from_prompt_message(message) mock_types.Part.from_text.assert_called_once_with(text="Hello") diff --git a/tests/utils/test_multipart_converter_openai.py b/tests/utils/test_multipart_converter_openai.py index 7c6385656..fb7e038c8 100644 --- a/tests/utils/test_multipart_converter_openai.py +++ b/tests/utils/test_multipart_converter_openai.py @@ -27,7 +27,7 @@ def test_is_supported_image_type_unsupported(self): def test_convert_to_openai_empty_content(self): multipart = PromptMessageMultipart(role="user", content=[]) - result = OpenAIConverter.convert_to_openai(multipart) + result = OpenAIConverter.from_prompt_message_multipart(multipart) assert result["role"] == "user" assert result["content"] == "" @@ -35,7 +35,7 @@ def test_convert_to_openai_empty_content(self): def test_convert_to_openai_single_text_content(self): content = [TextContent(type="text", text="Hello, world!")] multipart = PromptMessageMultipart(role="user", content=content) - result = OpenAIConverter.convert_to_openai(multipart) + result = OpenAIConverter.from_prompt_message_multipart(multipart) assert result["role"] == "user" assert result["content"] == "Hello, world!" @@ -46,7 +46,7 @@ def test_convert_to_openai_multiple_content_blocks(self): ImageContent(type="image", data="base64data", mimeType="image/png"), ] multipart = PromptMessageMultipart(role="user", content=content) - result = OpenAIConverter.convert_to_openai(multipart) + result = OpenAIConverter.from_prompt_message_multipart(multipart) assert result["role"] == "user" assert isinstance(result["content"], list) @@ -69,7 +69,7 @@ def test_convert_to_openai_concatenate_text_blocks(self): TextContent(type="text", text="World"), ] multipart = PromptMessageMultipart(role="user", content=content) - result = OpenAIConverter.convert_to_openai( + result = OpenAIConverter.from_prompt_message_multipart( multipart, concatenate_text_blocks=True ) @@ -104,7 +104,7 @@ def test_convert_prompt_message_to_openai(self): message = PromptMessage( role="user", content=TextContent(type="text", text="Hello") ) - result = OpenAIConverter.convert_prompt_message_to_openai(message) + result = OpenAIConverter.from_prompt_message(message) assert result["role"] == "user" assert result["content"] == "Hello" diff --git a/uv.lock b/uv.lock index 69f877cf2..ae3574193 100644 --- a/uv.lock +++ b/uv.lock @@ -1824,7 +1824,7 @@ wheels = [ [[package]] name = "mcp-agent" -version = "0.1.4" +version = "0.1.5" source = { editable = "." } dependencies = [ { name = "aiohttp" }, From faf71974016cd1053784d6acd6a40fb449f0b6b0 Mon Sep 17 00:00:00 2001 From: StreetLamb Date: Thu, 3 Jul 2025 23:20:32 +0800 Subject: [PATCH 2/4] refactor: Update OpenAIConverter to use better types --- .../llm/multipart_converter_openai.py | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/mcp_agent/workflows/llm/multipart_converter_openai.py b/src/mcp_agent/workflows/llm/multipart_converter_openai.py index 1a4454bd7..04a997631 100644 --- a/src/mcp_agent/workflows/llm/multipart_converter_openai.py +++ b/src/mcp_agent/workflows/llm/multipart_converter_openai.py @@ -11,6 +11,7 @@ ChatCompletionMessageParam, ChatCompletionUserMessageParam, ChatCompletionMessage, + ChatCompletionToolMessageParam, ) from mcp_agent.logging.logger import get_logger @@ -362,7 +363,10 @@ def convert_tool_result_to_openai( tool_result: CallToolResult, tool_call_id: str, concatenate_text_blocks: bool = False, - ) -> Union[Dict[str, Any], Tuple[Dict[str, Any], List[Dict[str, Any]]]]: + ) -> Union[ + ChatCompletionToolMessageParam, + Tuple[ChatCompletionToolMessageParam, list[ChatCompletionMessageParam]], + ]: """ Convert a CallToolResult to an OpenAI tool message. @@ -380,15 +384,15 @@ def convert_tool_result_to_openai( """ # Handle empty content case if not tool_result.content: - return { - "role": "tool", - "tool_call_id": tool_call_id, - "content": "[No content in tool result]", - } + return ChatCompletionToolMessageParam( + role="tool", + tool_call_id=tool_call_id, + content="[No content in tool result]", + ) # Separate text and non-text content - text_content = [] - non_text_content = [] + text_content: list[TextContent] = [] + non_text_content: list[ContentBlock] = [] for item in tool_result.content: if isinstance(item, TextContent): @@ -414,11 +418,9 @@ def convert_tool_result_to_openai( tool_message_content = "[Tool returned non-text content]" # Create the tool message with just the text - tool_message = { - "role": "tool", - "tool_call_id": tool_call_id, - "content": tool_message_content, - } + tool_message = ChatCompletionToolMessageParam( + role="tool", tool_call_id=tool_call_id, content=tool_message_content + ) # If there's no non-text content, return just the tool message if not non_text_content: @@ -441,7 +443,7 @@ def convert_tool_result_to_openai( def convert_function_results_to_openai( results: List[Tuple[str, CallToolResult]], concatenate_text_blocks: bool = False, - ) -> List[Dict[str, Any]]: + ) -> list[ChatCompletionMessageParam]: """ Convert a list of function call results to OpenAI messages. @@ -452,7 +454,7 @@ def convert_function_results_to_openai( Returns: List of OpenAI API messages for tool responses """ - messages = [] + messages: list[ChatCompletionMessageParam] = [] for tool_call_id, result in results: converted = OpenAIConverter.convert_tool_result_to_openai( From cfdbb21229fe8a970ccde6b0bfa2692db8f63da5 Mon Sep 17 00:00:00 2001 From: StreetLamb Date: Thu, 3 Jul 2025 23:26:06 +0800 Subject: [PATCH 3/4] feat: Add from_tool_results method in MessageConverter protocol --- src/mcp_agent/workflows/llm/multipart_converter.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/mcp_agent/workflows/llm/multipart_converter.py b/src/mcp_agent/workflows/llm/multipart_converter.py index 0bd78618d..87e0300b0 100644 --- a/src/mcp_agent/workflows/llm/multipart_converter.py +++ b/src/mcp_agent/workflows/llm/multipart_converter.py @@ -1,7 +1,7 @@ from typing import Generic, Protocol from mcp_agent.utils.prompt_message_multipart import PromptMessageMultipart -from mcp.types import PromptMessage +from mcp.types import PromptMessage, CallToolResult from mcp_agent.workflows.llm.augmented_llm import MessageTypes from mcp_agent.workflows.llm.augmented_llm import MessageParamT, MessageT @@ -16,10 +16,17 @@ def from_prompt_message_multipart( @staticmethod def from_prompt_message(message: PromptMessage) -> MessageParamT: - """Convert a PromptMessage to a Provider-compatible message param type""" + """Convert a MCP PromptMessage to a Provider-compatible message param type""" ... @staticmethod def from_mixed_messages(message: MessageTypes) -> list[MessageParamT]: """Convert a mixed message type to a list of Provider-compatible message param types""" ... + + @staticmethod + def from_tool_results( + tool_results: list[tuple[str, CallToolResult]], + ) -> MessageParamT | list[MessageParamT]: + """Convert a list of MCP CallToolResult to Provider-compatible message param type""" + ... From e9a778d0fb6bbe73a510c058fa0ee676b74f5887 Mon Sep 17 00:00:00 2001 From: StreetLamb Date: Thu, 3 Jul 2025 23:26:23 +0800 Subject: [PATCH 4/4] refactor: Rename conversion methods to from_tool_result and from_tool_results for consistency --- .../llm/multipart_converter_anthropic.py | 4 ++-- .../llm/multipart_converter_azure.py | 10 +++------ .../llm/multipart_converter_bedrock.py | 4 ++-- .../llm/multipart_converter_google.py | 8 +++---- .../llm/multipart_converter_openai.py | 6 ++--- .../test_multipart_converter_anthropic.py | 10 +++------ tests/utils/test_multipart_converter_azure.py | 18 +++++---------- .../utils/test_multipart_converter_bedrock.py | 22 +++++-------------- .../utils/test_multipart_converter_google.py | 14 +++++------- .../utils/test_multipart_converter_openai.py | 10 ++++----- 10 files changed, 38 insertions(+), 68 deletions(-) diff --git a/src/mcp_agent/workflows/llm/multipart_converter_anthropic.py b/src/mcp_agent/workflows/llm/multipart_converter_anthropic.py index abe9f211d..218aa960c 100644 --- a/src/mcp_agent/workflows/llm/multipart_converter_anthropic.py +++ b/src/mcp_agent/workflows/llm/multipart_converter_anthropic.py @@ -368,7 +368,7 @@ def _create_fallback_text( return TextBlockParam(type="text", text=f"[{message}]") @staticmethod - def convert_tool_result_to_anthropic( + def from_tool_result( tool_result: CallToolResult, tool_use_id: str ) -> ToolResultBlockParam: """ @@ -413,7 +413,7 @@ def convert_tool_result_to_anthropic( ) @staticmethod - def create_tool_results_message( + def from_tool_results( tool_results: List[tuple[str, CallToolResult]], ) -> MessageParam: """ diff --git a/src/mcp_agent/workflows/llm/multipart_converter_azure.py b/src/mcp_agent/workflows/llm/multipart_converter_azure.py index 1e9264b40..f580dd2d7 100644 --- a/src/mcp_agent/workflows/llm/multipart_converter_azure.py +++ b/src/mcp_agent/workflows/llm/multipart_converter_azure.py @@ -259,9 +259,7 @@ def _create_fallback_text( return TextContentItem(text=f"[{message}]") @staticmethod - def convert_tool_result_to_azure( - tool_result: CallToolResult, tool_use_id: str - ) -> ToolMessage: + def from_tool_result(tool_result: CallToolResult, tool_use_id: str) -> ToolMessage: """ Convert an MCP CallToolResult to an Azure ToolMessage. @@ -319,7 +317,7 @@ def _extract_text_from_azure_content_blocks( return "\n".join(texts) @staticmethod - def create_tool_results_message( + def from_tool_results( tool_results: List[tuple[str, CallToolResult]], ) -> List[ToolMessage]: """ @@ -333,9 +331,7 @@ def create_tool_results_message( """ tool_messages = [] for tool_use_id, result in tool_results: - tool_message = AzureConverter.convert_tool_result_to_azure( - result, tool_use_id - ) + tool_message = AzureConverter.from_tool_result(result, tool_use_id) tool_messages.append(tool_message) return tool_messages diff --git a/src/mcp_agent/workflows/llm/multipart_converter_bedrock.py b/src/mcp_agent/workflows/llm/multipart_converter_bedrock.py index f8b7711f5..b98f10470 100644 --- a/src/mcp_agent/workflows/llm/multipart_converter_bedrock.py +++ b/src/mcp_agent/workflows/llm/multipart_converter_bedrock.py @@ -228,7 +228,7 @@ def _create_fallback_text( return {"text": f"[{message}]"} @staticmethod - def convert_tool_result_to_bedrock( + def from_tool_result( tool_result: CallToolResult, tool_use_id: str ) -> ToolResultBlockTypeDef: """ @@ -246,7 +246,7 @@ def convert_tool_result_to_bedrock( } @staticmethod - def create_tool_results_message( + def from_tool_results( tool_results: List[tuple[str, CallToolResult]], ) -> MessageUnionTypeDef: """ diff --git a/src/mcp_agent/workflows/llm/multipart_converter_google.py b/src/mcp_agent/workflows/llm/multipart_converter_google.py index aecc769ef..bde145d9d 100644 --- a/src/mcp_agent/workflows/llm/multipart_converter_google.py +++ b/src/mcp_agent/workflows/llm/multipart_converter_google.py @@ -284,9 +284,7 @@ def _create_fallback_text( return types.Part.from_text(text=f"[{message}]") @staticmethod - def convert_tool_result_to_google( - tool_result: CallToolResult, tool_use_id: str - ) -> types.Part: + def from_tool_result(tool_result: CallToolResult, tool_use_id: str) -> types.Part: """ Convert an MCP CallToolResult to a Google function response part. @@ -324,7 +322,7 @@ def convert_tool_result_to_google( ) @staticmethod - def create_tool_results_message( + def from_tool_results( tool_results: List[tuple[str, CallToolResult]], ) -> types.Content: """ @@ -339,7 +337,7 @@ def create_tool_results_message( parts = [] for tool_use_id, result in tool_results: - part = GoogleConverter.convert_tool_result_to_google(result, tool_use_id) + part = GoogleConverter.from_tool_result(result, tool_use_id) parts.append(part) return types.Content(role="user", parts=parts) diff --git a/src/mcp_agent/workflows/llm/multipart_converter_openai.py b/src/mcp_agent/workflows/llm/multipart_converter_openai.py index 04a997631..f257e67b6 100644 --- a/src/mcp_agent/workflows/llm/multipart_converter_openai.py +++ b/src/mcp_agent/workflows/llm/multipart_converter_openai.py @@ -359,7 +359,7 @@ def _extract_text_from_content_blocks( ) @staticmethod - def convert_tool_result_to_openai( + def from_tool_result( tool_result: CallToolResult, tool_call_id: str, concatenate_text_blocks: bool = False, @@ -440,7 +440,7 @@ def convert_tool_result_to_openai( return (tool_message, [user_message]) @staticmethod - def convert_function_results_to_openai( + def from_tool_results( results: List[Tuple[str, CallToolResult]], concatenate_text_blocks: bool = False, ) -> list[ChatCompletionMessageParam]: @@ -457,7 +457,7 @@ def convert_function_results_to_openai( messages: list[ChatCompletionMessageParam] = [] for tool_call_id, result in results: - converted = OpenAIConverter.convert_tool_result_to_openai( + converted = OpenAIConverter.from_tool_result( tool_result=result, tool_call_id=tool_call_id, concatenate_text_blocks=concatenate_text_blocks, diff --git a/tests/utils/test_multipart_converter_anthropic.py b/tests/utils/test_multipart_converter_anthropic.py index 25c178e76..303a0f8b9 100644 --- a/tests/utils/test_multipart_converter_anthropic.py +++ b/tests/utils/test_multipart_converter_anthropic.py @@ -234,9 +234,7 @@ def test_convert_tool_result_to_anthropic(self): content = [TextContent(type="text", text="Tool result")] tool_result = CallToolResult(content=content, isError=False) - result = AnthropicConverter.convert_tool_result_to_anthropic( - tool_result, "tool_use_123" - ) + result = AnthropicConverter.from_tool_result(tool_result, "tool_use_123") assert result["type"] == "tool_result" assert result["tool_use_id"] == "tool_use_123" @@ -248,9 +246,7 @@ def test_convert_tool_result_to_anthropic(self): def test_convert_tool_result_to_anthropic_empty_content(self): tool_result = CallToolResult(content=[], isError=False) - result = AnthropicConverter.convert_tool_result_to_anthropic( - tool_result, "tool_use_123" - ) + result = AnthropicConverter.from_tool_result(tool_result, "tool_use_123") assert result["type"] == "tool_result" assert result["tool_use_id"] == "tool_use_123" @@ -266,7 +262,7 @@ def test_create_tool_results_message(self): tool_results = [("tool_1", result1), ("tool_2", result2)] - message = AnthropicConverter.create_tool_results_message(tool_results) + message = AnthropicConverter.from_tool_results(tool_results) assert message["role"] == "user" assert len(message["content"]) == 2 diff --git a/tests/utils/test_multipart_converter_azure.py b/tests/utils/test_multipart_converter_azure.py index 8b4dfd5f0..084763d80 100644 --- a/tests/utils/test_multipart_converter_azure.py +++ b/tests/utils/test_multipart_converter_azure.py @@ -235,9 +235,7 @@ def test_convert_tool_result_to_azure(self): content = [TextContent(type="text", text="Tool result")] tool_result = CallToolResult(content=content, isError=False) - result = AzureConverter.convert_tool_result_to_azure( - tool_result, "tool_use_123" - ) + result = AzureConverter.from_tool_result(tool_result, "tool_use_123") assert result.role == "tool" assert isinstance(result.content, str) @@ -246,9 +244,7 @@ def test_convert_tool_result_to_azure(self): def test_convert_tool_result_to_azure_empty_content(self): tool_result = CallToolResult(content=[], isError=False) - result = AzureConverter.convert_tool_result_to_azure( - tool_result, "tool_use_123" - ) + result = AzureConverter.from_tool_result(tool_result, "tool_use_123") assert result.role == "tool" assert isinstance(result.content, str) @@ -263,7 +259,7 @@ def test_create_tool_results_message(self): tool_results = [("tool_1", result1), ("tool_2", result2)] - messages = AzureConverter.create_tool_results_message(tool_results) + messages = AzureConverter.from_tool_results(tool_results) assert isinstance(messages, list) assert len(messages) == 2 @@ -282,9 +278,7 @@ def test_convert_tool_result_with_embedded_resource(self): content = [embedded] tool_result = CallToolResult(content=content, isError=False) - result = AzureConverter.convert_tool_result_to_azure( - tool_result, "tool_use_123" - ) + result = AzureConverter.from_tool_result(tool_result, "tool_use_123") assert result.role == "tool" assert isinstance(result.content, str) @@ -297,9 +291,7 @@ def test_convert_tool_result_with_mixed_content(self): ] tool_result = CallToolResult(content=content, isError=False) - result = AzureConverter.convert_tool_result_to_azure( - tool_result, "tool_use_123" - ) + result = AzureConverter.from_tool_result(tool_result, "tool_use_123") assert result.role == "tool" assert isinstance(result.content, str) diff --git a/tests/utils/test_multipart_converter_bedrock.py b/tests/utils/test_multipart_converter_bedrock.py index 981697000..1a6723526 100644 --- a/tests/utils/test_multipart_converter_bedrock.py +++ b/tests/utils/test_multipart_converter_bedrock.py @@ -259,9 +259,7 @@ def test_convert_tool_result_to_bedrock(self): content = [TextContent(type="text", text="Tool result")] tool_result = CallToolResult(content=content, isError=False) - result = BedrockConverter.convert_tool_result_to_bedrock( - tool_result, "tool_use_123" - ) + result = BedrockConverter.from_tool_result(tool_result, "tool_use_123") assert "toolResult" in result assert result["toolResult"]["toolUseId"] == "tool_use_123" @@ -273,9 +271,7 @@ def test_convert_tool_result_to_bedrock_error(self): content = [TextContent(type="text", text="Error occurred")] tool_result = CallToolResult(content=content, isError=True) - result = BedrockConverter.convert_tool_result_to_bedrock( - tool_result, "tool_use_123" - ) + result = BedrockConverter.from_tool_result(tool_result, "tool_use_123") assert "toolResult" in result assert result["toolResult"]["toolUseId"] == "tool_use_123" @@ -286,9 +282,7 @@ def test_convert_tool_result_to_bedrock_error(self): def test_convert_tool_result_to_bedrock_empty_content(self): tool_result = CallToolResult(content=[], isError=False) - result = BedrockConverter.convert_tool_result_to_bedrock( - tool_result, "tool_use_123" - ) + result = BedrockConverter.from_tool_result(tool_result, "tool_use_123") assert "toolResult" in result assert result["toolResult"]["toolUseId"] == "tool_use_123" @@ -307,7 +301,7 @@ def test_create_tool_results_message(self): tool_results = [("tool_1", result1), ("tool_2", result2)] - message = BedrockConverter.create_tool_results_message(tool_results) + message = BedrockConverter.from_tool_results(tool_results) assert message["role"] == "user" assert len(message["content"]) == 2 @@ -330,9 +324,7 @@ def test_convert_tool_result_with_embedded_resource(self): content = [embedded] tool_result = CallToolResult(content=content, isError=False) - result = BedrockConverter.convert_tool_result_to_bedrock( - tool_result, "tool_use_123" - ) + result = BedrockConverter.from_tool_result(tool_result, "tool_use_123") assert "toolResult" in result assert result["toolResult"]["toolUseId"] == "tool_use_123" @@ -347,9 +339,7 @@ def test_convert_tool_result_with_image_content(self): ] tool_result = CallToolResult(content=content, isError=False) - result = BedrockConverter.convert_tool_result_to_bedrock( - tool_result, "tool_use_123" - ) + result = BedrockConverter.from_tool_result(tool_result, "tool_use_123") assert "toolResult" in result assert result["toolResult"]["toolUseId"] == "tool_use_123" diff --git a/tests/utils/test_multipart_converter_google.py b/tests/utils/test_multipart_converter_google.py index 88a4f9b2b..f6b5da32c 100644 --- a/tests/utils/test_multipart_converter_google.py +++ b/tests/utils/test_multipart_converter_google.py @@ -403,9 +403,7 @@ def test_convert_tool_result_to_google(self): # Make from_function_response return a sentinel value mock_part = mock_types.Part.from_function_response.return_value - part = GoogleConverter.convert_tool_result_to_google( - tool_result, "tool_use_123" - ) + part = GoogleConverter.from_tool_result(tool_result, "tool_use_123") assert part == mock_part mock_types.Part.from_function_response.assert_called_once_with( @@ -423,7 +421,7 @@ def test_convert_tool_result_to_google_error(self): mock_part = Mock() mock_types.Part.from_function_response.return_value = mock_part - GoogleConverter.convert_tool_result_to_google(tool_result, "tool_use_123") + GoogleConverter.from_tool_result(tool_result, "tool_use_123") # Error case should have different response format args, kwargs = mock_types.Part.from_function_response.call_args @@ -441,7 +439,7 @@ def test_convert_tool_result_to_google_empty_content(self): mock_types.Part.from_function_response.return_value = mock_part mock_types.Part.from_text.return_value = Mock() - GoogleConverter.convert_tool_result_to_google(tool_result, "tool_use_123") + GoogleConverter.from_tool_result(tool_result, "tool_use_123") # Should add fallback text and call function response mock_types.Part.from_text.assert_called_once_with( @@ -466,7 +464,7 @@ def test_create_tool_results_message(self): mock_content = Mock() mock_types.Content.return_value = mock_content - GoogleConverter.create_tool_results_message(tool_results) + GoogleConverter.from_tool_results(tool_results) # Should call Content with user role and 2 parts mock_types.Content.assert_called_once_with( @@ -488,7 +486,7 @@ def test_convert_tool_result_with_embedded_resource(self): mock_types.Part.from_text.return_value = mock_part mock_types.Part.from_function_response.return_value = mock_part - GoogleConverter.convert_tool_result_to_google(tool_result, "tool_use_123") + GoogleConverter.from_tool_result(tool_result, "tool_use_123") # Should process embedded resource as text mock_types.Part.from_text.assert_called_once_with(text="Resource content") @@ -511,7 +509,7 @@ def test_convert_tool_result_with_image_content(self): mock_types.Part.from_bytes.return_value = mock_part mock_types.Part.from_function_response.return_value = mock_part - GoogleConverter.convert_tool_result_to_google(tool_result, "tool_use_123") + GoogleConverter.from_tool_result(tool_result, "tool_use_123") # Should process both text and image content mock_types.Part.from_text.assert_called_once_with(text="Text content") diff --git a/tests/utils/test_multipart_converter_openai.py b/tests/utils/test_multipart_converter_openai.py index fb7e038c8..63951d47f 100644 --- a/tests/utils/test_multipart_converter_openai.py +++ b/tests/utils/test_multipart_converter_openai.py @@ -289,7 +289,7 @@ def test_convert_tool_result_to_openai_text_only(self): content = [TextContent(type="text", text="Tool result")] tool_result = CallToolResult(content=content, isError=False) - result = OpenAIConverter.convert_tool_result_to_openai(tool_result, "call_123") + result = OpenAIConverter.from_tool_result(tool_result, "call_123") assert result["role"] == "tool" assert result["tool_call_id"] == "call_123" @@ -298,7 +298,7 @@ def test_convert_tool_result_to_openai_text_only(self): def test_convert_tool_result_to_openai_empty_content(self): tool_result = CallToolResult(content=[], isError=False) - result = OpenAIConverter.convert_tool_result_to_openai(tool_result, "call_123") + result = OpenAIConverter.from_tool_result(tool_result, "call_123") assert result["role"] == "tool" assert result["tool_call_id"] == "call_123" @@ -311,7 +311,7 @@ def test_convert_tool_result_to_openai_mixed_content(self): ] tool_result = CallToolResult(content=content, isError=False) - result = OpenAIConverter.convert_tool_result_to_openai(tool_result, "call_123") + result = OpenAIConverter.from_tool_result(tool_result, "call_123") # Should return tuple with tool message and additional user message assert isinstance(result, tuple) @@ -334,7 +334,7 @@ def test_convert_function_results_to_openai(self): results = [("call_1", result1), ("call_2", result2)] - messages = OpenAIConverter.convert_function_results_to_openai(results) + messages = OpenAIConverter.from_tool_results(results) assert len(messages) == 2 assert messages[0]["role"] == "tool" @@ -353,7 +353,7 @@ def test_convert_function_results_to_openai_mixed_content(self): tool_result = CallToolResult(content=content, isError=False) results = [("call_1", tool_result)] - messages = OpenAIConverter.convert_function_results_to_openai(results) + messages = OpenAIConverter.from_tool_results(results) # Should get tool message + additional user message assert len(messages) == 2