Files
wehub-resource-sync c56bef871b
Sync docs with Docusaurus / sync (push) Waiting to run
Tests / Check if changed (push) Waiting to run
Tests / format (push) Blocked by required conditions
Tests / check-imports (push) Blocked by required conditions
Tests / Unit / macos-latest (push) Blocked by required conditions
Tests / Unit / ubuntu-latest (push) Blocked by required conditions
Tests / Unit / windows-latest (push) Blocked by required conditions
Tests / mypy (push) Blocked by required conditions
Tests / Integration / ubuntu-latest (push) Blocked by required conditions
Tests / Integration / macos-latest (push) Blocked by required conditions
Tests / Integration / windows-latest (push) Blocked by required conditions
Tests / notify-slack-on-failure (push) Blocked by required conditions
Tests / Mark tests as completed (push) Blocked by required conditions
Docker image release / Build base image (push) Waiting to run
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:28 +08:00

190 lines
6.6 KiB
Python

# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import base64
import mimetypes
import os
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any
from urllib.parse import unquote, urlparse
import filetype
from haystack import logging
from haystack.utils.dataclasses import _warn_on_inplace_mutation
logger = logging.getLogger(__name__)
@_warn_on_inplace_mutation
@dataclass
class FileContent:
"""
The file content of a chat message.
:param base64_data: A base64 string representing the file.
:param mime_type: The MIME type of the file (e.g. "application/pdf").
Providing this value is recommended, as most LLM providers require it.
If not provided, the MIME type is guessed from the base64 string, which can be slow and not always reliable.
:param filename: Optional filename of the file. Some LLM providers use this information.
:param extra: Dictionary of extra information about the file. Can be used to store provider-specific information.
To avoid serialization issues, values should be JSON serializable.
:param validation: If True (default), a validation process is performed:
- Check whether the base64 string is valid;
- Guess the MIME type if not provided.
Set to False to skip validation and speed up initialization.
"""
base64_data: str
mime_type: str | None = None
filename: str | None = None
extra: dict[str, Any] = field(default_factory=dict)
validation: bool = True
def __post_init__(self) -> None:
if not self.validation:
return
try:
decoded_data = base64.b64decode(self.base64_data, validate=True)
except Exception as e:
raise ValueError("The base64 string is not valid") from e
# mime_type is an important information, so we try to guess it if not provided
if not self.mime_type:
guess = filetype.guess(decoded_data)
if guess:
self.mime_type = guess.mime
else:
msg = (
"Failed to guess the MIME type of the file. Omitting the MIME type may result in "
"processing errors or incorrect handling of the file by LLM providers."
)
logger.warning(msg)
def __repr__(self) -> str:
"""
Return a string representation of the FileContent, truncating the base64_data to 100 bytes.
"""
fields = []
truncated_data = self.base64_data[:100] + "..." if len(self.base64_data) > 100 else self.base64_data
fields.append(f"base64_data={truncated_data!r}")
fields.append(f"mime_type={self.mime_type!r}")
fields.append(f"filename={self.filename!r}")
fields.append(f"extra={self.extra!r}")
fields_str = ", ".join(fields)
return f"{self.__class__.__name__}({fields_str})"
def to_dict(self) -> dict[str, Any]:
"""
Convert FileContent into a dictionary.
"""
return asdict(self)
def _to_trace_dict(self) -> dict[str, Any]:
"""
Convert the FileContent to a dictionary representation for tracing.
The base64_data is replaced with a placeholder string to avoid sending large payloads to the tracing backend.
:returns:
Serialized version of the object only for tracing purposes.
"""
data = self.to_dict()
data["base64_data"] = f"Base64 string ({len(self.base64_data)} characters)"
return data
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "FileContent":
"""
Create an FileContent from a dictionary.
"""
return FileContent(**data)
@classmethod
def from_file_path(
cls, file_path: str | Path, *, filename: str | None = None, extra: dict[str, Any] | None = None
) -> "FileContent":
"""
Create an FileContent object from a file path.
:param file_path:
The path to the file.
:param filename:
Optional file name. Some LLM providers use this information. If not provided, the filename is extracted
from the file path.
:param extra:
Dictionary of extra information about the file. Can be used to store provider-specific information.
To avoid serialization issues, values should be JSON serializable.
:returns:
An FileContent object.
"""
if isinstance(file_path, str):
file_path = Path(file_path)
mime_type = mimetypes.guess_type(file_path.as_posix())[0]
filename = filename or file_path.name
with open(file_path, "rb") as f:
data = f.read()
return cls(
base64_data=base64.b64encode(data).decode("utf-8"),
mime_type=mime_type,
filename=filename,
extra=extra or {},
validation=False,
)
@classmethod
def from_url(
cls,
url: str,
*,
retry_attempts: int = 2,
timeout: int = 10,
filename: str | None = None,
extra: dict[str, Any] | None = None,
) -> "FileContent":
"""
Create an FileContent object from a URL. The file is downloaded and converted to a base64 string.
:param url:
The URL of the file.
:param retry_attempts:
The number of times to retry to fetch the URL's content.
:param timeout:
Timeout in seconds for the request.
:param filename:
Optional filename of the file. Some LLM providers use this information. If not provided, the filename is
extracted from the URL.
:param extra:
Dictionary of extra information about the file. Can be used to store provider-specific information.
To avoid serialization issues, values should be JSON serializable.
:returns:
An FileContent object.
"""
from haystack.components.fetchers.link_content import LinkContentFetcher
fetcher = LinkContentFetcher(raise_on_failure=True, retry_attempts=retry_attempts, timeout=timeout)
bytestream = fetcher.run(urls=[url])["streams"][0]
mime_type = bytestream.mime_type
data = bytestream.data
if not filename:
filename = os.path.basename(unquote(urlparse(url).path))
return cls(
base64_data=base64.b64encode(data).decode("utf-8"),
mime_type=mime_type,
filename=filename,
extra=extra or {},
validation=False,
)