mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-05-13 18:08:16 +08:00

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
|