mirror of
				https://github.com/langgenius/dify.git
				synced 2025-10-31 02:42:59 +00:00 
			
		
		
		
	 349c3cf7b8
			
		
	
	
		349c3cf7b8
		
			
		
	
	
	
	
		
			
			Enhance `LLMNode` with multimodal capability, introducing support for image outputs. This implementation extracts base64-encoded images from LLM responses, saves them to the storage service, and records the file metadata in the `ToolFile` table. In conversations, these images are rendered as markdown-based inline images. Additionally, the images are included in the LLMNode's output as file variables, enabling subsequent nodes in the workflow to utilize them. To integrate file outputs into workflows, adjustments to the frontend code are necessary. For multimodal output functionality, updates to related model configurations are required. Currently, this capability has been applied exclusively to Google's Gemini models. Close #15814. Signed-off-by: -LAN- <laipz8200@outlook.com> Co-authored-by: -LAN- <laipz8200@outlook.com>
		
			
				
	
	
		
			149 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			149 lines
		
	
	
		
			5.2 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from collections.abc import Mapping, Sequence
 | |
| from typing import Any, Optional
 | |
| 
 | |
| from pydantic import BaseModel, Field, model_validator
 | |
| 
 | |
| from core.model_runtime.entities.message_entities import ImagePromptMessageContent
 | |
| from core.tools.signature import sign_tool_file
 | |
| 
 | |
| from . import helpers
 | |
| from .constants import FILE_MODEL_IDENTITY
 | |
| from .enums import FileTransferMethod, FileType
 | |
| 
 | |
| 
 | |
| class ImageConfig(BaseModel):
 | |
|     """
 | |
|     NOTE: This part of validation is deprecated, but still used in app features "Image Upload".
 | |
|     """
 | |
| 
 | |
|     number_limits: int = 0
 | |
|     transfer_methods: Sequence[FileTransferMethod] = Field(default_factory=list)
 | |
|     detail: ImagePromptMessageContent.DETAIL | None = None
 | |
| 
 | |
| 
 | |
| class FileUploadConfig(BaseModel):
 | |
|     """
 | |
|     File Upload Entity.
 | |
|     """
 | |
| 
 | |
|     image_config: Optional[ImageConfig] = None
 | |
|     allowed_file_types: Sequence[FileType] = Field(default_factory=list)
 | |
|     allowed_file_extensions: Sequence[str] = Field(default_factory=list)
 | |
|     allowed_file_upload_methods: Sequence[FileTransferMethod] = Field(default_factory=list)
 | |
|     number_limits: int = 0
 | |
| 
 | |
| 
 | |
| class File(BaseModel):
 | |
|     # NOTE: dify_model_identity is a special identifier used to distinguish between
 | |
|     # new and old data formats during serialization and deserialization.
 | |
|     dify_model_identity: str = FILE_MODEL_IDENTITY
 | |
| 
 | |
|     id: Optional[str] = None  # message file id
 | |
|     tenant_id: str
 | |
|     type: FileType
 | |
|     transfer_method: FileTransferMethod
 | |
|     # If `transfer_method` is `FileTransferMethod.remote_url`, the
 | |
|     # `remote_url` attribute must not be `None`.
 | |
|     remote_url: Optional[str] = None  # remote url
 | |
|     # If `transfer_method` is `FileTransferMethod.local_file` or
 | |
|     # `FileTransferMethod.tool_file`, the `related_id` attribute must not be `None`.
 | |
|     #
 | |
|     # It should be set to `ToolFile.id` when `transfer_method` is `tool_file`.
 | |
|     related_id: Optional[str] = None
 | |
|     filename: Optional[str] = None
 | |
|     extension: Optional[str] = Field(default=None, description="File extension, should contains dot")
 | |
|     mime_type: Optional[str] = None
 | |
|     size: int = -1
 | |
| 
 | |
|     # Those properties are private, should not be exposed to the outside.
 | |
|     _storage_key: str
 | |
| 
 | |
|     def __init__(
 | |
|         self,
 | |
|         *,
 | |
|         id: Optional[str] = None,
 | |
|         tenant_id: str,
 | |
|         type: FileType,
 | |
|         transfer_method: FileTransferMethod,
 | |
|         remote_url: Optional[str] = None,
 | |
|         related_id: Optional[str] = None,
 | |
|         filename: Optional[str] = None,
 | |
|         extension: Optional[str] = None,
 | |
|         mime_type: Optional[str] = None,
 | |
|         size: int = -1,
 | |
|         storage_key: Optional[str] = None,
 | |
|         dify_model_identity: Optional[str] = FILE_MODEL_IDENTITY,
 | |
|         url: Optional[str] = None,
 | |
|     ):
 | |
|         super().__init__(
 | |
|             id=id,
 | |
|             tenant_id=tenant_id,
 | |
|             type=type,
 | |
|             transfer_method=transfer_method,
 | |
|             remote_url=remote_url,
 | |
|             related_id=related_id,
 | |
|             filename=filename,
 | |
|             extension=extension,
 | |
|             mime_type=mime_type,
 | |
|             size=size,
 | |
|             dify_model_identity=dify_model_identity,
 | |
|             url=url,
 | |
|         )
 | |
|         self._storage_key = str(storage_key)
 | |
| 
 | |
|     def to_dict(self) -> Mapping[str, str | int | None]:
 | |
|         data = self.model_dump(mode="json")
 | |
|         return {
 | |
|             **data,
 | |
|             "url": self.generate_url(),
 | |
|         }
 | |
| 
 | |
|     @property
 | |
|     def markdown(self) -> str:
 | |
|         url = self.generate_url()
 | |
|         if self.type == FileType.IMAGE:
 | |
|             text = f""
 | |
|         else:
 | |
|             text = f"[{self.filename or url}]({url})"
 | |
| 
 | |
|         return text
 | |
| 
 | |
|     def generate_url(self) -> Optional[str]:
 | |
|         if self.transfer_method == FileTransferMethod.REMOTE_URL:
 | |
|             return self.remote_url
 | |
|         elif self.transfer_method == FileTransferMethod.LOCAL_FILE:
 | |
|             if self.related_id is None:
 | |
|                 raise ValueError("Missing file related_id")
 | |
|             return helpers.get_signed_file_url(upload_file_id=self.related_id)
 | |
|         elif self.transfer_method == FileTransferMethod.TOOL_FILE:
 | |
|             assert self.related_id is not None
 | |
|             assert self.extension is not None
 | |
|             return sign_tool_file(tool_file_id=self.related_id, extension=self.extension)
 | |
| 
 | |
|     def to_plugin_parameter(self) -> dict[str, Any]:
 | |
|         return {
 | |
|             "dify_model_identity": FILE_MODEL_IDENTITY,
 | |
|             "mime_type": self.mime_type,
 | |
|             "filename": self.filename,
 | |
|             "extension": self.extension,
 | |
|             "size": self.size,
 | |
|             "type": self.type,
 | |
|             "url": self.generate_url(),
 | |
|         }
 | |
| 
 | |
|     @model_validator(mode="after")
 | |
|     def validate_after(self):
 | |
|         match self.transfer_method:
 | |
|             case FileTransferMethod.REMOTE_URL:
 | |
|                 if not self.remote_url:
 | |
|                     raise ValueError("Missing file url")
 | |
|                 if not isinstance(self.remote_url, str) or not self.remote_url.startswith("http"):
 | |
|                     raise ValueError("Invalid file url")
 | |
|             case FileTransferMethod.LOCAL_FILE:
 | |
|                 if not self.related_id:
 | |
|                     raise ValueError("Missing file related_id")
 | |
|             case FileTransferMethod.TOOL_FILE:
 | |
|                 if not self.related_id:
 | |
|                     raise ValueError("Missing file related_id")
 | |
|         return self
 |