|  | @@ -1,8 +1,13 @@
 | 
	
		
			
				|  |  | +import base64
 | 
	
		
			
				|  |  | +import os
 | 
	
		
			
				|  |  | +import tempfile
 | 
	
		
			
				|  |  | +import uuid
 | 
	
		
			
				|  |  |  from collections.abc import Generator
 | 
	
		
			
				|  |  | -from typing import Optional, Union
 | 
	
		
			
				|  |  | +from http import HTTPStatus
 | 
	
		
			
				|  |  | +from typing import Optional, Union, cast
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -from dashscope import get_tokenizer
 | 
	
		
			
				|  |  | -from dashscope.api_entities.dashscope_response import DashScopeAPIResponse
 | 
	
		
			
				|  |  | +from dashscope import Generation, MultiModalConversation, get_tokenizer
 | 
	
		
			
				|  |  | +from dashscope.api_entities.dashscope_response import GenerationResponse
 | 
	
		
			
				|  |  |  from dashscope.common.error import (
 | 
	
		
			
				|  |  |      AuthenticationError,
 | 
	
		
			
				|  |  |      InvalidParameter,
 | 
	
	
		
			
				|  | @@ -11,17 +16,21 @@ from dashscope.common.error import (
 | 
	
		
			
				|  |  |      UnsupportedHTTPMethod,
 | 
	
		
			
				|  |  |      UnsupportedModel,
 | 
	
		
			
				|  |  |  )
 | 
	
		
			
				|  |  | -from langchain.llms.tongyi import generate_with_retry, stream_generate_with_retry
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  from core.model_runtime.callbacks.base_callback import Callback
 | 
	
		
			
				|  |  |  from core.model_runtime.entities.llm_entities import LLMMode, LLMResult, LLMResultChunk, LLMResultChunkDelta
 | 
	
		
			
				|  |  |  from core.model_runtime.entities.message_entities import (
 | 
	
		
			
				|  |  |      AssistantPromptMessage,
 | 
	
		
			
				|  |  | +    ImagePromptMessageContent,
 | 
	
		
			
				|  |  |      PromptMessage,
 | 
	
		
			
				|  |  | +    PromptMessageContentType,
 | 
	
		
			
				|  |  |      PromptMessageTool,
 | 
	
		
			
				|  |  |      SystemPromptMessage,
 | 
	
		
			
				|  |  | +    TextPromptMessageContent,
 | 
	
		
			
				|  |  | +    ToolPromptMessage,
 | 
	
		
			
				|  |  |      UserPromptMessage,
 | 
	
		
			
				|  |  |  )
 | 
	
		
			
				|  |  | +from core.model_runtime.entities.model_entities import ModelFeature
 | 
	
		
			
				|  |  |  from core.model_runtime.errors.invoke import (
 | 
	
		
			
				|  |  |      InvokeAuthorizationError,
 | 
	
		
			
				|  |  |      InvokeBadRequestError,
 | 
	
	
		
			
				|  | @@ -33,10 +42,9 @@ from core.model_runtime.errors.invoke import (
 | 
	
		
			
				|  |  |  from core.model_runtime.errors.validate import CredentialsValidateFailedError
 | 
	
		
			
				|  |  |  from core.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -from ._client import EnhanceTongyi
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  class TongyiLargeLanguageModel(LargeLanguageModel):
 | 
	
		
			
				|  |  | +    tokenizers = {}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      def _invoke(self, model: str, credentials: dict,
 | 
	
		
			
				|  |  |                  prompt_messages: list[PromptMessage], model_parameters: dict,
 | 
	
	
		
			
				|  | @@ -57,13 +65,13 @@ class TongyiLargeLanguageModel(LargeLanguageModel):
 | 
	
		
			
				|  |  |          :return: full response or stream response chunk generator result
 | 
	
		
			
				|  |  |          """
 | 
	
		
			
				|  |  |          # invoke model
 | 
	
		
			
				|  |  | -        return self._generate(model, credentials, prompt_messages, model_parameters, stop, stream, user)
 | 
	
		
			
				|  |  | -    
 | 
	
		
			
				|  |  | -    def _code_block_mode_wrapper(self, model: str, credentials: dict, 
 | 
	
		
			
				|  |  | -                                 prompt_messages: list[PromptMessage], model_parameters: dict, 
 | 
	
		
			
				|  |  | -                                 tools: list[PromptMessageTool] | None = None, stop: list[str] | None = None, 
 | 
	
		
			
				|  |  | +        return self._generate(model, credentials, prompt_messages, model_parameters, tools, stop, stream, user)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def _code_block_mode_wrapper(self, model: str, credentials: dict,
 | 
	
		
			
				|  |  | +                                 prompt_messages: list[PromptMessage], model_parameters: dict,
 | 
	
		
			
				|  |  | +                                 tools: list[PromptMessageTool] | None = None, stop: list[str] | None = None,
 | 
	
		
			
				|  |  |                                   stream: bool = True, user: str | None = None, callbacks: list[Callback] = None) \
 | 
	
		
			
				|  |  | -                            -> LLMResult | Generator:
 | 
	
		
			
				|  |  | +            -> LLMResult | Generator:
 | 
	
		
			
				|  |  |          """
 | 
	
		
			
				|  |  |          Wrapper for code block mode
 | 
	
		
			
				|  |  |          """
 | 
	
	
		
			
				|  | @@ -88,7 +96,7 @@ if you are not sure about the structure.
 | 
	
		
			
				|  |  |                  stream=stream,
 | 
	
		
			
				|  |  |                  user=user
 | 
	
		
			
				|  |  |              )
 | 
	
		
			
				|  |  | -        
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |          model_parameters.pop("response_format")
 | 
	
		
			
				|  |  |          stop = stop or []
 | 
	
		
			
				|  |  |          stop.extend(["\n```", "```\n"])
 | 
	
	
		
			
				|  | @@ -99,13 +107,13 @@ if you are not sure about the structure.
 | 
	
		
			
				|  |  |              # override the system message
 | 
	
		
			
				|  |  |              prompt_messages[0] = SystemPromptMessage(
 | 
	
		
			
				|  |  |                  content=block_prompts
 | 
	
		
			
				|  |  | -                    .replace("{{instructions}}", prompt_messages[0].content)
 | 
	
		
			
				|  |  | +                .replace("{{instructions}}", prompt_messages[0].content)
 | 
	
		
			
				|  |  |              )
 | 
	
		
			
				|  |  |          else:
 | 
	
		
			
				|  |  |              # insert the system message
 | 
	
		
			
				|  |  |              prompt_messages.insert(0, SystemPromptMessage(
 | 
	
		
			
				|  |  |                  content=block_prompts
 | 
	
		
			
				|  |  | -                    .replace("{{instructions}}", f"Please output a valid {code_block} object.")
 | 
	
		
			
				|  |  | +                .replace("{{instructions}}", f"Please output a valid {code_block} object.")
 | 
	
		
			
				|  |  |              ))
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          mode = self.get_model_mode(model, credentials)
 | 
	
	
		
			
				|  | @@ -138,7 +146,7 @@ if you are not sure about the structure.
 | 
	
		
			
				|  |  |                  prompt_messages=prompt_messages,
 | 
	
		
			
				|  |  |                  input_generator=response
 | 
	
		
			
				|  |  |              )
 | 
	
		
			
				|  |  | -        
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |          return response
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      def get_num_tokens(self, model: str, credentials: dict, prompt_messages: list[PromptMessage],
 | 
	
	
		
			
				|  | @@ -152,7 +160,14 @@ if you are not sure about the structure.
 | 
	
		
			
				|  |  |          :param tools: tools for tool calling
 | 
	
		
			
				|  |  |          :return:
 | 
	
		
			
				|  |  |          """
 | 
	
		
			
				|  |  | -        tokenizer = get_tokenizer(model)
 | 
	
		
			
				|  |  | +        if model in ['qwen-turbo-chat', 'qwen-plus-chat']:
 | 
	
		
			
				|  |  | +            model = model.replace('-chat', '')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        if model in self.tokenizers:
 | 
	
		
			
				|  |  | +            tokenizer = self.tokenizers[model]
 | 
	
		
			
				|  |  | +        else:
 | 
	
		
			
				|  |  | +            tokenizer = get_tokenizer(model)
 | 
	
		
			
				|  |  | +            self.tokenizers[model] = tokenizer
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          # convert string to token ids
 | 
	
		
			
				|  |  |          tokens = tokenizer.encode(self._convert_messages_to_prompt(prompt_messages))
 | 
	
	
		
			
				|  | @@ -184,6 +199,7 @@ if you are not sure about the structure.
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      def _generate(self, model: str, credentials: dict,
 | 
	
		
			
				|  |  |                    prompt_messages: list[PromptMessage], model_parameters: dict,
 | 
	
		
			
				|  |  | +                  tools: Optional[list[PromptMessageTool]] = None,
 | 
	
		
			
				|  |  |                    stop: Optional[list[str]] = None, stream: bool = True,
 | 
	
		
			
				|  |  |                    user: Optional[str] = None) -> Union[LLMResult, Generator]:
 | 
	
		
			
				|  |  |          """
 | 
	
	
		
			
				|  | @@ -192,24 +208,27 @@ if you are not sure about the structure.
 | 
	
		
			
				|  |  |          :param model: model name
 | 
	
		
			
				|  |  |          :param credentials: credentials
 | 
	
		
			
				|  |  |          :param prompt_messages: prompt messages
 | 
	
		
			
				|  |  | +        :param tools: tools for tool calling
 | 
	
		
			
				|  |  |          :param model_parameters: model parameters
 | 
	
		
			
				|  |  |          :param stop: stop words
 | 
	
		
			
				|  |  |          :param stream: is stream response
 | 
	
		
			
				|  |  |          :param user: unique user id
 | 
	
		
			
				|  |  |          :return: full response or stream response chunk generator result
 | 
	
		
			
				|  |  |          """
 | 
	
		
			
				|  |  | -        extra_model_kwargs = {}
 | 
	
		
			
				|  |  | -        if stop:
 | 
	
		
			
				|  |  | -            extra_model_kwargs['stop'] = stop
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |          # transform credentials to kwargs for model instance
 | 
	
		
			
				|  |  |          credentials_kwargs = self._to_credential_kwargs(credentials)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        client = EnhanceTongyi(
 | 
	
		
			
				|  |  | -            model_name=model,
 | 
	
		
			
				|  |  | -            streaming=stream,
 | 
	
		
			
				|  |  | -            dashscope_api_key=credentials_kwargs['api_key'],
 | 
	
		
			
				|  |  | -        )
 | 
	
		
			
				|  |  | +        mode = self.get_model_mode(model, credentials)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        if model in ['qwen-turbo-chat', 'qwen-plus-chat']:
 | 
	
		
			
				|  |  | +            model = model.replace('-chat', '')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        extra_model_kwargs = {}
 | 
	
		
			
				|  |  | +        if tools:
 | 
	
		
			
				|  |  | +            extra_model_kwargs['tools'] = self._convert_tools(tools)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        if stop:
 | 
	
		
			
				|  |  | +            extra_model_kwargs['stop'] = stop
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          params = {
 | 
	
		
			
				|  |  |              'model': model,
 | 
	
	
		
			
				|  | @@ -218,30 +237,27 @@ if you are not sure about the structure.
 | 
	
		
			
				|  |  |              **extra_model_kwargs,
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        mode = self.get_model_mode(model, credentials)
 | 
	
		
			
				|  |  | +        model_schema = self.get_model_schema(model, credentials)
 | 
	
		
			
				|  |  | +        if ModelFeature.VISION in (model_schema.features or []):
 | 
	
		
			
				|  |  | +            params['messages'] = self._convert_prompt_messages_to_tongyi_messages(prompt_messages, rich_content=True)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        if mode == LLMMode.CHAT:
 | 
	
		
			
				|  |  | -            params['messages'] = self._convert_prompt_messages_to_tongyi_messages(prompt_messages)
 | 
	
		
			
				|  |  | +            response = MultiModalConversation.call(**params, stream=stream)
 | 
	
		
			
				|  |  |          else:
 | 
	
		
			
				|  |  | -            params['prompt'] = self._convert_messages_to_prompt(prompt_messages)
 | 
	
		
			
				|  |  | +            if mode == LLMMode.CHAT:
 | 
	
		
			
				|  |  | +                params['messages'] = self._convert_prompt_messages_to_tongyi_messages(prompt_messages)
 | 
	
		
			
				|  |  | +            else:
 | 
	
		
			
				|  |  | +                params['prompt'] = prompt_messages[0].content.rstrip()
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        if stream:
 | 
	
		
			
				|  |  | -            responses = stream_generate_with_retry(
 | 
	
		
			
				|  |  | -                client, 
 | 
	
		
			
				|  |  | -                stream=True,
 | 
	
		
			
				|  |  | -                incremental_output=True,
 | 
	
		
			
				|  |  | -                **params
 | 
	
		
			
				|  |  | -            )
 | 
	
		
			
				|  |  | +            response = Generation.call(**params,
 | 
	
		
			
				|  |  | +                                       result_format='message',
 | 
	
		
			
				|  |  | +                                       stream=stream)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            return self._handle_generate_stream_response(model, credentials, responses, prompt_messages)
 | 
	
		
			
				|  |  | +        if stream:
 | 
	
		
			
				|  |  | +            return self._handle_generate_stream_response(model, credentials, response, prompt_messages)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        response = generate_with_retry(
 | 
	
		
			
				|  |  | -            client,
 | 
	
		
			
				|  |  | -            **params,
 | 
	
		
			
				|  |  | -        )
 | 
	
		
			
				|  |  |          return self._handle_generate_response(model, credentials, response, prompt_messages)
 | 
	
		
			
				|  |  | -        
 | 
	
		
			
				|  |  | -    def _handle_generate_response(self, model: str, credentials: dict, response: DashScopeAPIResponse,
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def _handle_generate_response(self, model: str, credentials: dict, response: GenerationResponse,
 | 
	
		
			
				|  |  |                                    prompt_messages: list[PromptMessage]) -> LLMResult:
 | 
	
		
			
				|  |  |          """
 | 
	
		
			
				|  |  |          Handle llm response
 | 
	
	
		
			
				|  | @@ -254,7 +270,7 @@ if you are not sure about the structure.
 | 
	
		
			
				|  |  |          """
 | 
	
		
			
				|  |  |          # transform assistant message to prompt message
 | 
	
		
			
				|  |  |          assistant_prompt_message = AssistantPromptMessage(
 | 
	
		
			
				|  |  | -            content=response.output.text
 | 
	
		
			
				|  |  | +            content=response.output.choices[0].message.content,
 | 
	
		
			
				|  |  |          )
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          # transform usage
 | 
	
	
		
			
				|  | @@ -270,32 +286,65 @@ if you are not sure about the structure.
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          return result
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    def _handle_generate_stream_response(self, model: str, credentials: dict, responses: Generator,
 | 
	
		
			
				|  |  | +    def _handle_generate_stream_response(self, model: str, credentials: dict,
 | 
	
		
			
				|  |  | +                                         responses: Generator[GenerationResponse, None, None],
 | 
	
		
			
				|  |  |                                           prompt_messages: list[PromptMessage]) -> Generator:
 | 
	
		
			
				|  |  |          """
 | 
	
		
			
				|  |  |          Handle llm stream response
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          :param model: model name
 | 
	
		
			
				|  |  |          :param credentials: credentials
 | 
	
		
			
				|  |  | -        :param response: response
 | 
	
		
			
				|  |  | +        :param responses: response
 | 
	
		
			
				|  |  |          :param prompt_messages: prompt messages
 | 
	
		
			
				|  |  |          :return: llm response chunk generator result
 | 
	
		
			
				|  |  |          """
 | 
	
		
			
				|  |  | +        full_text = ''
 | 
	
		
			
				|  |  | +        tool_calls = []
 | 
	
		
			
				|  |  |          for index, response in enumerate(responses):
 | 
	
		
			
				|  |  | -            resp_finish_reason = response.output.finish_reason
 | 
	
		
			
				|  |  | -            resp_content = response.output.text
 | 
	
		
			
				|  |  | -            usage = response.usage
 | 
	
		
			
				|  |  | +            if response.status_code != 200 and response.status_code != HTTPStatus.OK:
 | 
	
		
			
				|  |  | +                raise ServiceUnavailableError(
 | 
	
		
			
				|  |  | +                    f"Failed to invoke model {model}, status code: {response.status_code}, "
 | 
	
		
			
				|  |  | +                    f"message: {response.message}"
 | 
	
		
			
				|  |  | +                )
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            if resp_finish_reason is None and (resp_content is None or resp_content == ''):
 | 
	
		
			
				|  |  | -                continue
 | 
	
		
			
				|  |  | +            resp_finish_reason = response.output.choices[0].finish_reason
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            # transform assistant message to prompt message
 | 
	
		
			
				|  |  | -            assistant_prompt_message = AssistantPromptMessage(
 | 
	
		
			
				|  |  | -                content=resp_content if resp_content else '',
 | 
	
		
			
				|  |  | -            )
 | 
	
		
			
				|  |  | +            if resp_finish_reason is not None and resp_finish_reason != 'null':
 | 
	
		
			
				|  |  | +                resp_content = response.output.choices[0].message.content
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                assistant_prompt_message = AssistantPromptMessage(
 | 
	
		
			
				|  |  | +                    content='',
 | 
	
		
			
				|  |  | +                )
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                if 'tool_calls' in response.output.choices[0].message:
 | 
	
		
			
				|  |  | +                    tool_calls = response.output.choices[0].message['tool_calls']
 | 
	
		
			
				|  |  | +                elif resp_content:
 | 
	
		
			
				|  |  | +                    # special for qwen-vl
 | 
	
		
			
				|  |  | +                    if isinstance(resp_content, list):
 | 
	
		
			
				|  |  | +                        resp_content = resp_content[0]['text']
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                    # transform assistant message to prompt message
 | 
	
		
			
				|  |  | +                    assistant_prompt_message.content = resp_content.replace(full_text, '', 1)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                    full_text = resp_content
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                if tool_calls:
 | 
	
		
			
				|  |  | +                    message_tool_calls = []
 | 
	
		
			
				|  |  | +                    for tool_call_obj in tool_calls:
 | 
	
		
			
				|  |  | +                        message_tool_call = AssistantPromptMessage.ToolCall(
 | 
	
		
			
				|  |  | +                            id=tool_call_obj['function']['name'],
 | 
	
		
			
				|  |  | +                            type='function',
 | 
	
		
			
				|  |  | +                            function=AssistantPromptMessage.ToolCall.ToolCallFunction(
 | 
	
		
			
				|  |  | +                                name=tool_call_obj['function']['name'],
 | 
	
		
			
				|  |  | +                                arguments=tool_call_obj['function']['arguments']
 | 
	
		
			
				|  |  | +                            )
 | 
	
		
			
				|  |  | +                        )
 | 
	
		
			
				|  |  | +                        message_tool_calls.append(message_tool_call)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                    assistant_prompt_message.tool_calls = message_tool_calls
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -            if resp_finish_reason is not None:
 | 
	
		
			
				|  |  |                  # transform usage
 | 
	
		
			
				|  |  | +                usage = response.usage
 | 
	
		
			
				|  |  |                  usage = self._calc_response_usage(model, credentials, usage.input_tokens, usage.output_tokens)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |                  yield LLMResultChunk(
 | 
	
	
		
			
				|  | @@ -309,6 +358,23 @@ if you are not sure about the structure.
 | 
	
		
			
				|  |  |                      )
 | 
	
		
			
				|  |  |                  )
 | 
	
		
			
				|  |  |              else:
 | 
	
		
			
				|  |  | +                resp_content = response.output.choices[0].message.content
 | 
	
		
			
				|  |  | +                if not resp_content:
 | 
	
		
			
				|  |  | +                    if 'tool_calls' in response.output.choices[0].message:
 | 
	
		
			
				|  |  | +                        tool_calls = response.output.choices[0].message['tool_calls']
 | 
	
		
			
				|  |  | +                    continue
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                # special for qwen-vl
 | 
	
		
			
				|  |  | +                if isinstance(resp_content, list):
 | 
	
		
			
				|  |  | +                    resp_content = resp_content[0]['text']
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                # transform assistant message to prompt message
 | 
	
		
			
				|  |  | +                assistant_prompt_message = AssistantPromptMessage(
 | 
	
		
			
				|  |  | +                    content=resp_content.replace(full_text, '', 1),
 | 
	
		
			
				|  |  | +                )
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                full_text = resp_content
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |                  yield LLMResultChunk(
 | 
	
		
			
				|  |  |                      model=model,
 | 
	
		
			
				|  |  |                      prompt_messages=prompt_messages,
 | 
	
	
		
			
				|  | @@ -343,11 +409,20 @@ if you are not sure about the structure.
 | 
	
		
			
				|  |  |          content = message.content
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          if isinstance(message, UserPromptMessage):
 | 
	
		
			
				|  |  | -            message_text = f"{human_prompt} {content}"
 | 
	
		
			
				|  |  | +            if isinstance(content, str):
 | 
	
		
			
				|  |  | +                message_text = f"{human_prompt} {content}"
 | 
	
		
			
				|  |  | +            else:
 | 
	
		
			
				|  |  | +                message_text = ""
 | 
	
		
			
				|  |  | +                for sub_message in content:
 | 
	
		
			
				|  |  | +                    if sub_message.type == PromptMessageContentType.TEXT:
 | 
	
		
			
				|  |  | +                        message_text = f"{human_prompt} {sub_message.data}"
 | 
	
		
			
				|  |  | +                        break
 | 
	
		
			
				|  |  |          elif isinstance(message, AssistantPromptMessage):
 | 
	
		
			
				|  |  |              message_text = f"{ai_prompt} {content}"
 | 
	
		
			
				|  |  |          elif isinstance(message, SystemPromptMessage):
 | 
	
		
			
				|  |  |              message_text = content
 | 
	
		
			
				|  |  | +        elif isinstance(message, ToolPromptMessage):
 | 
	
		
			
				|  |  | +            message_text = content
 | 
	
		
			
				|  |  |          else:
 | 
	
		
			
				|  |  |              raise ValueError(f"Got unknown type {message}")
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -370,7 +445,8 @@ if you are not sure about the structure.
 | 
	
		
			
				|  |  |          # trim off the trailing ' ' that might come from the "Assistant: "
 | 
	
		
			
				|  |  |          return text.rstrip()
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    def _convert_prompt_messages_to_tongyi_messages(self, prompt_messages: list[PromptMessage]) -> list[dict]:
 | 
	
		
			
				|  |  | +    def _convert_prompt_messages_to_tongyi_messages(self, prompt_messages: list[PromptMessage],
 | 
	
		
			
				|  |  | +                                                    rich_content: bool = False) -> list[dict]:
 | 
	
		
			
				|  |  |          """
 | 
	
		
			
				|  |  |          Convert prompt messages to tongyi messages
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -382,23 +458,118 @@ if you are not sure about the structure.
 | 
	
		
			
				|  |  |              if isinstance(prompt_message, SystemPromptMessage):
 | 
	
		
			
				|  |  |                  tongyi_messages.append({
 | 
	
		
			
				|  |  |                      'role': 'system',
 | 
	
		
			
				|  |  | -                    'content': prompt_message.content,
 | 
	
		
			
				|  |  | +                    'content': prompt_message.content if not rich_content else [{"text": prompt_message.content}],
 | 
	
		
			
				|  |  |                  })
 | 
	
		
			
				|  |  |              elif isinstance(prompt_message, UserPromptMessage):
 | 
	
		
			
				|  |  | -                tongyi_messages.append({
 | 
	
		
			
				|  |  | -                    'role': 'user',
 | 
	
		
			
				|  |  | -                    'content': prompt_message.content,
 | 
	
		
			
				|  |  | -                })
 | 
	
		
			
				|  |  | +                if isinstance(prompt_message.content, str):
 | 
	
		
			
				|  |  | +                    tongyi_messages.append({
 | 
	
		
			
				|  |  | +                        'role': 'user',
 | 
	
		
			
				|  |  | +                        'content': prompt_message.content if not rich_content else [{"text": prompt_message.content}],
 | 
	
		
			
				|  |  | +                    })
 | 
	
		
			
				|  |  | +                else:
 | 
	
		
			
				|  |  | +                    sub_messages = []
 | 
	
		
			
				|  |  | +                    for message_content in prompt_message.content:
 | 
	
		
			
				|  |  | +                        if message_content.type == PromptMessageContentType.TEXT:
 | 
	
		
			
				|  |  | +                            message_content = cast(TextPromptMessageContent, message_content)
 | 
	
		
			
				|  |  | +                            sub_message_dict = {
 | 
	
		
			
				|  |  | +                                "text": message_content.data
 | 
	
		
			
				|  |  | +                            }
 | 
	
		
			
				|  |  | +                            sub_messages.append(sub_message_dict)
 | 
	
		
			
				|  |  | +                        elif message_content.type == PromptMessageContentType.IMAGE:
 | 
	
		
			
				|  |  | +                            message_content = cast(ImagePromptMessageContent, message_content)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                            image_url = message_content.data
 | 
	
		
			
				|  |  | +                            if message_content.data.startswith("data:"):
 | 
	
		
			
				|  |  | +                                # convert image base64 data to file in /tmp
 | 
	
		
			
				|  |  | +                                image_url = self._save_base64_image_to_file(message_content.data)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                            sub_message_dict = {
 | 
	
		
			
				|  |  | +                                "image": image_url
 | 
	
		
			
				|  |  | +                            }
 | 
	
		
			
				|  |  | +                            sub_messages.append(sub_message_dict)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                    # resort sub_messages to ensure text is always at last
 | 
	
		
			
				|  |  | +                    sub_messages = sorted(sub_messages, key=lambda x: 'text' in x)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                    tongyi_messages.append({
 | 
	
		
			
				|  |  | +                        'role': 'user',
 | 
	
		
			
				|  |  | +                        'content': sub_messages
 | 
	
		
			
				|  |  | +                    })
 | 
	
		
			
				|  |  |              elif isinstance(prompt_message, AssistantPromptMessage):
 | 
	
		
			
				|  |  | +                content = prompt_message.content
 | 
	
		
			
				|  |  | +                if not content:
 | 
	
		
			
				|  |  | +                    content = ' '
 | 
	
		
			
				|  |  |                  tongyi_messages.append({
 | 
	
		
			
				|  |  |                      'role': 'assistant',
 | 
	
		
			
				|  |  | -                    'content': prompt_message.content,
 | 
	
		
			
				|  |  | +                    'content': content if not rich_content else [{"text": content}],
 | 
	
		
			
				|  |  | +                })
 | 
	
		
			
				|  |  | +            elif isinstance(prompt_message, ToolPromptMessage):
 | 
	
		
			
				|  |  | +                tongyi_messages.append({
 | 
	
		
			
				|  |  | +                    "role": "tool",
 | 
	
		
			
				|  |  | +                    "content": prompt_message.content,
 | 
	
		
			
				|  |  | +                    "name": prompt_message.tool_call_id
 | 
	
		
			
				|  |  |                  })
 | 
	
		
			
				|  |  |              else:
 | 
	
		
			
				|  |  |                  raise ValueError(f"Got unknown type {prompt_message}")
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          return tongyi_messages
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    def _save_base64_image_to_file(self, base64_image: str) -> str:
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        Save base64 image to file
 | 
	
		
			
				|  |  | +        'data:{upload_file.mime_type};base64,{encoded_string}'
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        :param base64_image: base64 image data
 | 
	
		
			
				|  |  | +        :return: image file path
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        # get mime type and encoded string
 | 
	
		
			
				|  |  | +        mime_type, encoded_string = base64_image.split(',')[0].split(';')[0].split(':')[1], base64_image.split(',')[1]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        # save image to file
 | 
	
		
			
				|  |  | +        temp_dir = tempfile.gettempdir()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        file_path = os.path.join(temp_dir, f"{uuid.uuid4()}.{mime_type.split('/')[1]}")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        with open(file_path, "wb") as image_file:
 | 
	
		
			
				|  |  | +            image_file.write(base64.b64decode(encoded_string))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return f"file://{file_path}"
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def _convert_tools(self, tools: list[PromptMessageTool]) -> list[dict]:
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        Convert tools
 | 
	
		
			
				|  |  | +        """
 | 
	
		
			
				|  |  | +        tool_definitions = []
 | 
	
		
			
				|  |  | +        for tool in tools:
 | 
	
		
			
				|  |  | +            properties = tool.parameters['properties']
 | 
	
		
			
				|  |  | +            required_properties = tool.parameters['required']
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            properties_definitions = {}
 | 
	
		
			
				|  |  | +            for p_key, p_val in properties.items():
 | 
	
		
			
				|  |  | +                desc = p_val['description']
 | 
	
		
			
				|  |  | +                if 'enum' in p_val:
 | 
	
		
			
				|  |  | +                    desc += (f"; Only accepts one of the following predefined options: "
 | 
	
		
			
				|  |  | +                             f"[{', '.join(p_val['enum'])}]")
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                properties_definitions[p_key] = {
 | 
	
		
			
				|  |  | +                    'description': desc,
 | 
	
		
			
				|  |  | +                    'type': p_val['type'],
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            tool_definition = {
 | 
	
		
			
				|  |  | +                "type": "function",
 | 
	
		
			
				|  |  | +                "function": {
 | 
	
		
			
				|  |  | +                    "name": tool.name,
 | 
	
		
			
				|  |  | +                    "description": tool.description,
 | 
	
		
			
				|  |  | +                    "parameters": properties_definitions,
 | 
	
		
			
				|  |  | +                    "required": required_properties
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            tool_definitions.append(tool_definition)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        return tool_definitions
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      @property
 | 
	
		
			
				|  |  |      def _invoke_error_mapping(self) -> dict[type[InvokeError], list[type[Exception]]]:
 | 
	
		
			
				|  |  |          """
 |