【Dify(v1.x) 核心源码深入解析】File 模块

重磅推荐专栏:
《大模型AIGC》
《课程大纲》
《知识星球》

本专栏致力于探索和讨论当今最前沿的技术趋势和应用领域,包括但不限于ChatGPT和Stable Diffusion等。我们将深入研究大型模型的开发和应用,以及与之相关的人工智能生成内容(AIGC)技术。通过深入的技术解析和实践经验分享,旨在帮助读者更好地理解和应用这些领域的最新进展

一、引言

在复杂的软件系统中,文件处理模块往往扮演着至关重要的角色。Dify 的 file 模块精心设计,以应对多样化场景下的文件管理需求。它不仅支持多种文件类型与传输方式,还融入了安全验证机制与灵活的文件操作功能,为整个系统提供坚实的文件处理基础。

二、模块概览

Dify 的 file 模块由多个子模块协同工作,包括文件模型定义、文件解析、签名验证、文件生成与工具文件解析等。这些子模块相互配合,共同完成文件的处理流程。

(一) 核心组件介绍

  1. 文件模型(File Model)

    • 定义了文件的核心属性,如文件类型、传输方法、相关标识符等,是整个文件处理流程的数据基础。
  2. 文件解析器(UploadFileParser)

    • 负责解析上传的文件,根据配置决定以何种方式(URL 或 Base64)提供图像数据。
  3. 工具文件解析器(ToolFileParser)

    • 专注于工具文件的管理,提供工具文件管理器的获取接口。
  4. 签名验证与生成(helpers.py)

    • 包含文件 URL 签名的生成与验证逻辑,确保文件访问的安全性。
  5. 文件管理器(file_manager.py)

    • 提供文件的高级操作,如将文件转换为提示消息内容、下载文件等。

三、文件模型(models.py)

(一) 文件类型与传输方法枚举

  • FileType 枚举 :定义了支持的文件类型,如图像(IMAGE)、文档(DOCUMENT)、音频(AUDIO)、视频(VIDEO)和自定义(CUSTOM)。
  • FileTransferMethod 枚举 :规定了文件传输方式,包括远程 URL(REMOTE_URL)、本地文件(LOCAL_FILE)和工具文件(TOOL_FILE)。

(二) 文件配置类

  • ImageConfig :用于存储图像上传相关的配置,如数量限制、传输方法和详细信息。
  • FileUploadConfig :包含了文件上传的全面配置,涵盖图像配置、允许的文件类型、扩展名、上传方法和数量限制。

(三) 文件模型类(File)

  • 核心属性 :包括文件标识符(id)、所属租户(tenant_id)、文件类型(type)、传输方法(transfer_method)、远程 URL(remote_url)、相关 ID(related_id)、文件名(filename)、扩展名(extension)、MIME 类型(mime_type)、大小(size)和存储键(_storage_key)。
  • 方法 :提供了将文件转换为字典、生成 Markdown 表示、生成文件 URL 以及转换为插件参数等方法。
class File(BaseModel):
    dify_model_identity: str = FILE_MODEL_IDENTITY

    id: Optional[str] = None  # message file id
    tenant_id: str
    type: FileType
    transfer_method: FileTransferMethod
    remote_url: Optional[str] = None  # remote url
    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"![{self.filename or ''}]({url})"
        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 ToolFileParser.get_tool_file_manager().sign_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

(四) 类图

type
1
1
transfer_method
1
1
File
+id str
+tenant_id str
+type FileType
+transfer_method FileTransferMethod
+remote_url str
+related_id str
+filename str
+extension str
+mime_type str
+size int
+_storage_key str
+markdown str
+to_dict() : dict
+generate_url() : str
+to_plugin_parameter() : dict
FileType
+IMAGE str
+DOCUMENT str
+AUDIO str
+VIDEO str
+CUSTOM str
FileTransferMethod
+REMOTE_URL str
+LOCAL_FILE str
+TOOL_FILE str

四、文件解析器(upload_file_parser.py)

(一) 功能概述

UploadFileParser 类专注于解析上传的文件,特别是图像文件。它根据系统配置决定以 URL 或 Base64 的形式提供图像数据,并且能够生成带有签名的临时图像 URL。

(二) 关键方法详解

  1. get_image_data 方法

    • 用于获取图像数据。首先检查文件是否存在且为支持的图像格式。
    • 根据配置(dify_config.MULTIMODAL_SEND_FORMAT)或强制参数(force_url)决定返回 URL 还是 Base64 编码的数据。
    • 如果选择 URL 方式,调用 get_signed_temp_image_url 方法获取签名 URL;如果选择 Base64 方式,从存储中加载文件数据并进行 Base64 编码。
@classmethod
def get_image_data(cls, upload_file, force_url: bool = False) -> Optional[str]:
    if not upload_file:
        return None

    if upload_file.extension not in IMAGE_EXTENSIONS:
        return None

    if dify_config.MULTIMODAL_SEND_FORMAT == "url" or force_url:
        return cls.get_signed_temp_image_url(upload_file.id)
    else:
        # get image file base64
        try:
            data = storage.load(upload_file.key)
        except FileNotFoundError:
            logging.exception(f"File not found: {upload_file.key}")
            return None

        encoded_string = base64.b64encode(data).decode("utf-8")
        return f"data:{upload_file.mime_type};base64,{encoded_string}"
  1. get_signed_temp_image_url 方法

    • 根据文件 ID 生成带有签名的临时图像 URL。利用 UrlSigner 类的 get_signed_url 方法完成签名过程。
@classmethod
def get_signed_temp_image_url(cls, upload_file_id) -> str:
    base_url = dify_config.FILES_URL
    image_preview_url = f"{base_url}/files/{upload_file_id}/image-preview"

    return UrlSigner.get_signed_url(url=image_preview_url, sign_key=upload_file_id, prefix="image-preview")
  1. verify_image_file_signature 方法

    • 验证图像文件签名的有效性。首先调用 UrlSigner 类的 verify 方法进行签名验证,然后检查时间戳是否在允许的时间范围内。
@classmethod
def verify_image_file_signature(cls, upload_file_id: str, timestamp: str, nonce: str, sign: str) -> bool:
    result = UrlSigner.verify(
        sign_key=upload_file_id, timestamp=timestamp, nonce=nonce, sign=sign, prefix="image-preview"
    )

    # verify signature
    if not result:
        return False

    current_time = int(time.time())
    return current_time - int(timestamp) <= dify_config.FILES_ACCESS_TIMEOUT

五、工具文件解析器(tool_file_parser.py)

(一) 功能描述

ToolFileParser 类主要用于管理工具文件,提供获取工具文件管理器的接口。

(二) 关键代码解读

class ToolFileParser:
    @staticmethod
    def get_tool_file_manager() -> "ToolFileManager":
        return cast("ToolFileManager", tool_file_manager["manager"])
  • get_tool_file_manager 方法 :静态方法,从 tool_file_manager 字典中获取工具文件管理器实例,并进行类型转换,以便后续使用工具文件管理器的功能。

六、签名生成与验证(helpers.py)

(一) 签名生成函数

  1. get_signed_file_url 函数

    • 生成带有签名的文件 URL。构造 URL 路径,生成时间戳和随机数(nonce),利用 HMAC-SHA256 算法对特定字符串进行签名,最后将签名编码并附加到 URL 上。
def get_signed_file_url(upload_file_id: str) -> str:
    url = f"{dify_config.FILES_URL}/files/{upload_file_id}/file-preview"

    timestamp = str(int(time.time()))
    nonce = os.urandom(16).hex()
    key = dify_config.SECRET_KEY.encode()
    msg = f"file-preview|{upload_file_id}|{timestamp}|{nonce}"
    sign = hmac.new(key, msg.encode(), hashlib.sha256).digest()
    encoded_sign = base64.urlsafe_b64encode(sign).decode()

    return f"{url}?timestamp={timestamp}&nonce={nonce}&sign={encoded_sign}"
  1. get_signed_file_url_for_plugin 函数

    • 为插件生成带有签名的文件上传 URL。与 get_signed_file_url 函数类似,但构造的 URL 路径不同,并且在签名字符串中包含了更多的参数(如文件名、MIME 类型、租户 ID 和用户 ID)。
def get_signed_file_url_for_plugin(filename: str, mimetype: str, tenant_id: str, user_id: str) -> str:
    url = f"{dify_config.FILES_URL}/files/upload/for-plugin"

    if user_id is None:
        user_id = "DEFAULT-USER"

    timestamp = str(int(time.time()))
    nonce = os.urandom(16).hex()
    key = dify_config.SECRET_KEY.encode()
    msg = f"upload|{filename}|{mimetype}|{tenant_id}|{user_id}|{timestamp}|{nonce}"
    sign = hmac.new(key, msg.encode(), hashlib.sha256).digest()
    encoded_sign = base64.urlsafe_b64encode(sign).decode()

    return f"{url}?timestamp={timestamp}&nonce={nonce}&sign={encoded_sign}&user_id={user_id}&tenant_id={tenant_id}"

(二) 签名验证函数

  1. verify_plugin_file_signature 函数

    • 验证插件文件签名的有效性。重新计算签名并将其与传入的签名进行比较,同时检查时间戳是否在允许的时间范围内。
def verify_plugin_file_signature(
    *, filename: str, mimetype: str, tenant_id: str, user_id: str | None, timestamp: str, nonce: str, sign: str
) -> bool:
    if user_id is None:
        user_id = "DEFAULT-USER"

    data_to_sign = f"upload|{filename}|{mimetype}|{tenant_id}|{user_id}|{timestamp}|{nonce}"
    secret_key = dify_config.SECRET_KEY.encode()
    recalculated_sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest()
    recalculated_encoded_sign = base64.urlsafe_b64encode(recalculated_sign).decode()

    # verify signature
    if sign != recalculated_encoded_sign:
        return False

    current_time = int(time.time())
    return current_time - int(timestamp) <= dify_config.FILES_ACCESS_TIMEOUT
  1. verify_image_signature 函数

    • 验证图像文件签名的有效性。与 verify_plugin_file_signature 函数类似,但签名字符串的构造方式略有不同。
def verify_image_signature(*, upload_file_id: str, timestamp: str, nonce: str, sign: str) -> bool:
    data_to_sign = f"image-preview|{upload_file_id}|{timestamp}|{nonce}"
    secret_key = dify_config.SECRET_KEY.encode()
    recalculated_sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest()
    recalculated_encoded_sign = base64.urlsafe_b64encode(recalculated_sign).decode()

    # verify signature
    if sign != recalculated_encoded_sign:
        return False

    current_time = int(time.time())
    return current_time - int(timestamp) <= dify_config.FILES_ACCESS_TIMEOUT
  1. verify_file_signature 函数

    • 验证文件签名的有效性。与 verify_image_signature 函数类似,适用于通用文件的签名验证。
def verify_file_signature(*, upload_file_id: str, timestamp: str, nonce: str, sign: str) -> bool:
    data_to_sign = f"file-preview|{upload_file_id}|{timestamp}|{nonce}"
    secret_key = dify_config.SECRET_KEY.encode()
    recalculated_sign = hmac.new(secret_key, data_to_sign.encode(), hashlib.sha256).digest()
    recalculated_encoded_sign = base64.urlsafe_b64encode(recalculated_sign).decode()

    # verify signature
    if sign != recalculated_encoded_sign:
        return False

    current_time = int(time.time())
    return current_time - int(timestamp) <= dify_config.FILES_ACCESS_TIMEOUT

(三) 流程图

image-preview
upload
file-preview
生成文件 URL 签名
选择签名类型
构造 image-preview 签名字符串
构造 upload 签名字符串
构造 file-preview 签名字符串
计算 HMAC-SHA256 签名
将签名编码并附加到 URL
返回带有签名的 URL

七、文件管理器(file_manager.py)

(一) 功能概述

File Manager 模块提供了对文件的高级操作,如获取文件属性、将文件转换为提示消息内容、下载文件等。

(二) 关键方法详解

  1. get_attr 函数

    • 根据文件和指定的属性,获取文件的相应属性值。支持获取文件类型、大小、名称、MIME 类型、传输方法、URL、扩展名和相关 ID。
def get_attr(*, file: File, attr: FileAttribute):
    match attr:
        case FileAttribute.TYPE:
            return file.type.value
        case FileAttribute.SIZE:
            return file.size
        case FileAttribute.NAME:
            return file.filename
        case FileAttribute.MIME_TYPE:
            return file.mime_type
        case FileAttribute.TRANSFER_METHOD:
            return file.transfer_method.value
        case FileAttribute.URL:
            return file.remote_url
        case FileAttribute.EXTENSION:
            return file.extension
        case FileAttribute.RELATED_ID:
            return file.related_id
  1. to_prompt_message_content 函数

    • 将文件转换为提示消息内容。根据文件类型选择相应的提示消息内容类(如图像、音频、视频或文档),并将文件的相关信息(如 Base64 数据、URL、格式和 MIME 类型)传递给该类以生成提示消息内容。
def to_prompt_message_content(
    f: File,
    /,
    *,
    image_detail_config: ImagePromptMessageContent.DETAIL | None = None,
) -> MultiModalPromptMessageContent:
    if f.extension is None:
        raise ValueError("Missing file extension")
    if f.mime_type is None:
        raise ValueError("Missing file mime_type")

    params = {
        "base64_data": _get_encoded_string(f) if dify_config.MULTIMODAL_SEND_FORMAT == "base64" else "",
        "url": _to_url(f) if dify_config.MULTIMODAL_SEND_FORMAT == "url" else "",
        "format": f.extension.removeprefix("."),
        "mime_type": f.mime_type,
    }
    if f.type == FileType.IMAGE:
        params["detail"] = image_detail_config or ImagePromptMessageContent.DETAIL.LOW

    prompt_class_map: Mapping[FileType, type[MultiModalPromptMessageContent]] = {
        FileType.IMAGE: ImagePromptMessageContent,
        FileType.AUDIO: AudioPromptMessageContent,
        FileType.VIDEO: VideoPromptMessageContent,
        FileType.DOCUMENT: DocumentPromptMessageContent,
    }

    try:
        return prompt_class_map[f.type].model_validate(params)
    except KeyError:
        raise ValueError(f"file type {f.type} is not supported")
  1. download 函数

    • 下载文件。根据文件的传输方法,分别处理远程 URL、本地文件和工具文件的下载。
def download(f: File, /):
    if f.transfer_method in (FileTransferMethod.TOOL_FILE, FileTransferMethod.LOCAL_FILE):
        return _download_file_content(f._storage_key)
    elif f.transfer_method == FileTransferMethod.REMOTE_URL:
        response = ssrf_proxy.get(f.remote_url, follow_redirects=True)
        response.raise_for_status()
        return response.content
    raise ValueError(f"unsupported transfer method: {f.transfer_method}")
  1. _get_encoded_string 函数

    • 获取文件的 Base64 编码字符串。根据文件的传输方法,从相应的位置获取文件数据(如远程 URL 或存储),然后进行 Base64 编码。
def _get_encoded_string(f: File, /):
    match f.transfer_method:
        case FileTransferMethod.REMOTE_URL:
            response = ssrf_proxy.get(f.remote_url, follow_redirects=True)
            response.raise_for_status()
            data = response.content
        case FileTransferMethod.LOCAL_FILE:
            data = _download_file_content(f._storage_key)
        case FileTransferMethod.TOOL_FILE:
            data = _download_file_content(f._storage_key)

    encoded_string = base64.b64encode(data).decode("utf-8")
    return encoded_string
  1. _to_url 函数

    • 将文件转换为 URL。根据文件的传输方法,生成相应的 URL,包括远程 URL、本地文件的签名 URL 和工具文件的签名 URL。
def _to_url(f: File, /):
    if f.transfer_method == FileTransferMethod.REMOTE_URL:
        if f.remote_url is None:
            raise ValueError("Missing file remote_url")
        return f.remote_url
    elif f.transfer_method == FileTransferMethod.LOCAL_FILE:
        if f.related_id is None:
            raise ValueError("Missing file related_id")
        return f.remote_url or helpers.get_signed_file_url(upload_file_id=f.related_id)
    elif f.transfer_method == FileTransferMethod.TOOL_FILE:
        # add sign url
        if f.related_id is None or f.extension is None:
            raise ValueError("Missing file related_id or extension")
        return ToolFileParser.get_tool_file_manager().sign_file(tool_file_id=f.related_id, extension=f.extension)
    else:
        raise ValueError(f"Unsupported transfer method: {f.transfer_method}")

(三) 时序图

调用者 文件管理器 存储 网络代理 调用 download 方法 判断传输方法 通过 ssrf_proxy 获取远程 URL 内容 返回响应内容 调用 _download_file_content 方法获取文件内容 返回文件数据 alt [远程 URL] [本地文件或工具文件] 返回文件内容 调用者 文件管理器 存储 网络代理

八、枚举与常量(enums.py 和 constants.py)

(一) 枚举类

  1. FileType 枚举 :定义了支持的文件类型。
  2. FileTransferMethod 枚举 :规定了文件传输方式。
  3. FileBelongsTo 枚举 :表示文件所属(用户或助手)。
  4. FileAttribute 枚举 :列举了文件的属性。
  5. ArrayFileAttribute 枚举 :定义了数组文件属性。

(二) 常量

  • FILE_MODEL_IDENTITY 常量 :标识文件模型。
from enum import StrEnum


class FileType(StrEnum):
    IMAGE = "image"
    DOCUMENT = "document"
    AUDIO = "audio"
    VIDEO = "video"
    CUSTOM = "custom"

    @staticmethod
    def value_of(value):
        for member in FileType:
            if member.value == value:
                return member
        raise ValueError(f"No matching enum found for value '{value}'")


class FileTransferMethod(StrEnum):
    REMOTE_URL = "remote_url"
    LOCAL_FILE = "local_file"
    TOOL_FILE = "tool_file"

    @staticmethod
    def value_of(value):
        for member in FileTransferMethod:
            if member.value == value:
                return member
        raise ValueError(f"No matching enum found for value '{value}'")


class FileBelongsTo(StrEnum):
    USER = "user"
    ASSISTANT = "assistant"

    @staticmethod
    def value_of(value):
        for member in FileBelongsTo:
            if member.value == value:
                return member
        raise ValueError(f"No matching enum found for value '{value}'")


class FileAttribute(StrEnum):
    TYPE = "type"
    SIZE = "size"
    NAME = "name"
    MIME_TYPE = "mime_type"
    TRANSFER_METHOD = "transfer_method"
    URL = "url"
    EXTENSION = "extension"
    RELATED_ID = "related_id"


class ArrayFileAttribute(StrEnum):
    LENGTH = "length"
FILE_MODEL_IDENTITY = "__dify__file__"

九、总结与展望

Dify 的 file 模块通过精心设计的架构和丰富的功能,为系统的文件处理提供了强大的支持。文件模型定义了文件的核心属性,文件解析器和工具文件解析器分别处理不同类型文件的解析需求,签名验证与生成确保了文件访问的安全性,文件管理器提供了文件的高级操作功能。枚举与常量的定义则为模块的使用提供了清晰的规范。

在未来的发展中,Dify 的 file 模块可能会进一步优化性能,扩展对更多文件类型的支持,并加强对文件安全性和完整性的保护。同时,随着系统功能的不断丰富,file 模块也可能会与更多的模块进行深度集成,以满足更加复杂的业务需求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小爷毛毛(卓寿杰)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值