Files
wehub-resource-sync 97e91a83f3
Ruff / Ruff (push) Has been cancelled
Test / Core Tests (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.10) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.11) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.12) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.13) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.9) (push) Has been cancelled
Test / Full Coverage (Python 3.11) (push) Has been cancelled
Test / Core Provider Tests (OpenAI) (push) Has been cancelled
Test / Core Provider Tests (Anthropic) (push) Has been cancelled
Test / Core Provider Tests (Google) (push) Has been cancelled
Test / Core Provider Tests (Other) (push) Has been cancelled
Test / Anthropic Tests (push) Has been cancelled
Test / Gemini Tests (push) Has been cancelled
Test / Google GenAI Tests (push) Has been cancelled
Test / Vertex AI Tests (push) Has been cancelled
Test / OpenAI Tests (push) Has been cancelled
Test / Writer Tests (push) Has been cancelled
Test / Auto Client Tests (push) Has been cancelled
ty / type-check (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:38 +08:00

869 lines
31 KiB
Python

from __future__ import annotations
import base64
import re
from collections.abc import Mapping, Hashable
from functools import lru_cache
from typing import (
Any,
Callable,
Literal,
Optional,
Union,
TypedDict,
TypeVar,
cast,
)
from pathlib import Path
from urllib.parse import urlparse
import mimetypes
import requests
from pydantic import BaseModel, Field
from instructor.v2.core.errors import MultimodalError
from instructor.v2.core.mode import Mode
mimetypes.add_type("image/webp", ".webp")
F = TypeVar("F", bound=Callable[..., Any])
K = TypeVar("K", bound=Hashable)
V = TypeVar("V")
# OpenAI source: https://platform.openai.com/docs/guides/vision/what-type-of-files-can-i-upload
# Anthropic source: https://docs.anthropic.com/en/docs/build-with-claude/vision#ensuring-image-quality
VALID_MIME_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"]
VALID_AUDIO_MIME_TYPES = [
"audio/aac",
"audio/flac",
"audio/mp3",
"audio/m4a",
"audio/mpeg",
"audio/mpga",
"audio/mp4",
"audio/opus",
"audio/pcm",
"audio/wav",
"audio/webm",
]
VALID_PDF_MIME_TYPES = ["application/pdf"]
CacheControlType = Mapping[str, str]
OptionalCacheControlType = Optional[CacheControlType]
class ImageParamsBase(TypedDict):
type: Literal["image"]
source: str
class ImageParams(ImageParamsBase, total=False):
cache_control: CacheControlType
class Image(BaseModel):
"""Image content loaded from a URL, path, base64 data, or bytes."""
source: Union[str, Path, bytes] = Field( # noqa: UP007
description="URL, file path, base64 data, or raw bytes of the image"
)
media_type: str = Field(description="MIME type of the image")
data: Union[str, None] = Field( # noqa: UP007
None, description="Base64 encoded image data", repr=False
)
@classmethod
def autodetect(cls, source: str | Path | bytes) -> Image:
"""Attempt to autodetect an image from a source string, Path, or bytes."""
if isinstance(source, str):
if cls.is_base64(source):
return cls.from_base64(source)
if source.startswith(("http://", "https://")):
return cls.from_url(source)
if source.startswith("gs://"):
return cls.from_gs_url(source)
# Since detecting the max length of a file universally cross-platform is difficult,
# we'll just try/catch the Path conversion and file check
try:
path = Path(source)
if path.is_file():
return cls.from_path(path)
except OSError:
pass # Fall through to raw base64 attempt
return cls.from_raw_base64(source)
if isinstance(source, Path):
return cls.from_path(source)
if isinstance(source, bytes):
encoded = base64.b64encode(source).decode("utf-8")
return cls.from_raw_base64(encoded)
raise ValueError(f"Unsupported image source type: {type(source).__name__}")
@classmethod
def autodetect_safely(cls, source: Union[str, Path, bytes]) -> Union[Image, str]: # noqa: UP007
"""Safely attempt to autodetect an image from a source string or path.
Args:
source (Union[str,path]): The source string or path.
Returns:
An Image if the source is detected to be a valid image, otherwise
the source itself as a string.
"""
try:
return cls.autodetect(source)
except ValueError:
return str(source)
@classmethod
def is_base64(cls, s: str) -> bool:
return bool(re.match(r"^data:image/[a-zA-Z]+;base64,", s))
@classmethod # Caching likely unnecessary
def from_base64(cls, data_uri: str) -> Image:
header, encoded = data_uri.split(",", 1)
media_type = header.split(":")[1].split(";")[0]
if media_type not in VALID_MIME_TYPES:
raise MultimodalError(
f"Unsupported image format: {media_type}. Supported formats: {', '.join(VALID_MIME_TYPES)}",
content_type="image",
)
return cls(
source=data_uri,
media_type=media_type,
data=encoded,
)
@classmethod
def from_gs_url(cls, data_uri: str, timeout: int = 30) -> Image:
"""
Create an Image instance from a Google Cloud Storage URL.
Args:
data_uri: GCS URL starting with gs://
timeout: Request timeout in seconds (default: 30)
"""
if not data_uri.startswith("gs://"):
raise ValueError("URL must start with gs://")
public_url = f"https://storage.googleapis.com/{data_uri[5:]}"
try:
response = requests.get(public_url, timeout=timeout)
response.raise_for_status()
media_type = response.headers.get("Content-Type")
if media_type not in VALID_MIME_TYPES:
raise ValueError(f"Unsupported image format: {media_type}")
data = base64.b64encode(response.content).decode("utf-8")
return cls(source=data_uri, media_type=media_type, data=data)
except requests.RequestException as e:
raise ValueError(
"Failed to access GCS image (must be publicly readable)"
) from e
@classmethod # Caching likely unnecessary
def from_raw_base64(cls, data: str) -> Image:
try:
decoded = base64.b64decode(data)
# Detect image type from file signature (magic bytes)
# This replaces imghdr which was removed in Python 3.13
img_type = None
if decoded.startswith(b"\xff\xd8\xff"):
img_type = "jpeg"
elif decoded.startswith(b"\x89PNG\r\n\x1a\n"):
img_type = "png"
elif decoded.startswith(b"GIF87a") or decoded.startswith(b"GIF89a"):
img_type = "gif"
elif decoded.startswith(b"RIFF") and decoded[8:12] == b"WEBP":
img_type = "webp"
if img_type:
return cls(
source=data,
media_type=f"image/{img_type}",
data=data,
)
raise ValueError(f"Unsupported image type: {img_type}")
except Exception as e:
raise ValueError(f"Invalid or unsupported base64 image data") from e
@classmethod
@lru_cache
def from_url(cls, url: str) -> Image:
if url.startswith("gs://"):
return cls.from_gs_url(url)
if cls.is_base64(url):
return cls.from_base64(url)
parsed_url = urlparse(url)
media_type, _ = mimetypes.guess_type(parsed_url.path)
if not media_type:
try:
response = requests.head(url, allow_redirects=True)
media_type = response.headers.get("Content-Type")
except requests.RequestException as e:
raise ValueError(f"Failed to fetch image from URL") from e
if media_type not in VALID_MIME_TYPES:
raise ValueError(f"Unsupported image format: {media_type}")
return cls(source=url, media_type=media_type, data=None)
@classmethod
@lru_cache
def from_path(cls, path: Union[str, Path]) -> Image: # noqa: UP007
path = Path(path)
if not path.is_file():
raise FileNotFoundError(f"Image file not found: {path}")
if path.stat().st_size == 0:
raise ValueError("Image file is empty")
media_type, _ = mimetypes.guess_type(str(path))
if media_type not in VALID_MIME_TYPES:
raise ValueError(f"Unsupported image format: {media_type}")
data = base64.b64encode(path.read_bytes()).decode("utf-8")
return cls(source=path, media_type=media_type, data=data)
@staticmethod
@lru_cache
def url_to_base64(url: str) -> str:
"""Cachable helper method for getting image url and encoding to base64."""
response = requests.get(url)
response.raise_for_status()
return base64.b64encode(response.content).decode("utf-8")
def to_anthropic(self) -> dict[str, Any]:
from instructor.v2.providers.anthropic.multimodal import image_to_anthropic
return image_to_anthropic(self)
def to_openai(self, mode: Mode) -> dict[str, Any]:
from instructor.v2.providers.openai.multimodal import image_to_openai
return image_to_openai(self, mode)
def to_genai(self):
from instructor.v2.providers.genai.multimodal import image_to_genai
return image_to_genai(self)
class Audio(BaseModel):
"""Represents an audio that can be loaded from a URL or file path."""
source: Union[str, Path] = Field(description="URL or file path of the audio") # noqa: UP007
data: Union[str, None] = Field( # noqa: UP007
None, description="Base64 encoded audio data", repr=False
)
media_type: str = Field(description="MIME type of the audio")
@classmethod
def autodetect(cls, source: str | Path) -> Audio:
"""Attempt to autodetect an audio from a source string or Path."""
if isinstance(source, str):
if cls.is_base64(source):
return cls.from_base64(source)
if source.startswith(("http://", "https://")):
return cls.from_url(source)
if source.startswith("gs://"):
return cls.from_gs_url(source)
# Since detecting the max length of a file universally cross-platform is difficult,
# we'll just try/catch the Path conversion and file check
try:
path = Path(source)
if path.is_file():
return cls.from_path(path)
except OSError:
pass # Fall through to error
raise ValueError("Unable to determine audio source")
if isinstance(source, Path):
return cls.from_path(source)
raise ValueError(f"Unsupported audio source type: {type(source).__name__}")
@classmethod
def autodetect_safely(cls, source: Union[str, Path]) -> Union[Audio, str]: # noqa: UP007
"""Safely attempt to autodetect an audio from a source string or path.
Args:
source (Union[str,path]): The source string or path.
Returns:
An Audio if the source is detected to be a valid audio, otherwise
the source itself as a string.
"""
try:
return cls.autodetect(source)
except ValueError:
return str(source)
@classmethod
def is_base64(cls, s: str) -> bool:
return bool(re.match(r"^data:audio/[a-zA-Z0-9+-]+;base64,", s))
@classmethod
def from_base64(cls, data_uri: str) -> Audio:
header, encoded = data_uri.split(",", 1)
media_type = header.split(":")[1].split(";")[0]
if media_type not in VALID_AUDIO_MIME_TYPES:
raise ValueError(f"Unsupported audio format: {media_type}")
return cls(
source=data_uri,
media_type=media_type,
data=encoded,
)
@classmethod
def from_url(cls, url: str) -> Audio:
"""Create an Audio instance from a URL."""
if url.startswith("gs://"):
return cls.from_gs_url(url)
response = requests.get(url)
content_type = response.headers.get("content-type")
assert content_type in VALID_AUDIO_MIME_TYPES, (
f"Invalid audio format. Must be one of: {', '.join(VALID_AUDIO_MIME_TYPES)}"
)
data = base64.b64encode(response.content).decode("utf-8")
return cls(source=url, data=data, media_type=content_type)
@classmethod
def from_path(cls, path: Union[str, Path]) -> Audio: # noqa: UP007
"""Create an Audio instance from a file path."""
path = Path(path)
assert path.is_file(), f"Audio file not found: {path}"
mime_type = mimetypes.guess_type(str(path))[0]
if mime_type == "audio/x-wav":
mime_type = "audio/wav"
if (
mime_type == "audio/vnd.dlna.adts"
): # <--- this is the case for aac audio files in Windows
mime_type = "audio/aac"
assert mime_type in VALID_AUDIO_MIME_TYPES, (
f"Invalid audio format. Must be one of: {', '.join(VALID_AUDIO_MIME_TYPES)}"
)
data = base64.b64encode(path.read_bytes()).decode("utf-8")
return cls(source=str(path), data=data, media_type=mime_type)
@classmethod
def from_gs_url(cls, data_uri: str, timeout: int = 30) -> Audio:
"""
Create an Audio instance from a Google Cloud Storage URL.
Args:
data_uri: GCS URL starting with gs://
timeout: Request timeout in seconds (default: 30)
"""
if not data_uri.startswith("gs://"):
raise ValueError("URL must start with gs://")
public_url = f"https://storage.googleapis.com/{data_uri[5:]}"
try:
response = requests.get(public_url, timeout=timeout)
response.raise_for_status()
media_type = response.headers.get("Content-Type")
if media_type not in VALID_AUDIO_MIME_TYPES:
raise ValueError(f"Unsupported audio format: {media_type}")
data = base64.b64encode(response.content).decode("utf-8")
return cls(source=data_uri, media_type=media_type, data=data)
except requests.RequestException as e:
raise ValueError(
"Failed to access GCS audio (must be publicly readable)"
) from e
def to_openai(self, mode: Mode) -> dict[str, Any]:
from instructor.v2.providers.openai.multimodal import audio_to_openai
return audio_to_openai(self, mode)
def to_anthropic(self) -> dict[str, Any]:
from instructor.v2.providers.anthropic.multimodal import audio_to_anthropic
return audio_to_anthropic(self)
def to_genai(self):
from instructor.v2.providers.genai.multimodal import audio_to_genai
return audio_to_genai(self)
class ImageWithCacheControl(Image):
"""Image with Anthropic prompt caching support."""
cache_control: OptionalCacheControlType = Field(
None, description="Optional Anthropic cache control image"
)
@classmethod
def from_image_params(cls, image_params: ImageParams) -> Image:
source = image_params["source"]
cache_control = image_params.get("cache_control")
base_image = Image.autodetect(source)
return cls(
source=base_image.source,
media_type=base_image.media_type,
data=base_image.data,
cache_control=cache_control,
)
def to_anthropic(self) -> dict[str, Any]:
from instructor.v2.providers.anthropic.multimodal import (
image_with_cache_control_to_anthropic,
)
return image_with_cache_control_to_anthropic(self)
class PDF(BaseModel):
source: str | Path = Field(description="URL, file path, or base64 data of the PDF")
media_type: str = Field(
description="MIME type of the PDF", default="application/pdf"
)
data: str | None = Field(None, description="Base64 encoded PDF data", repr=False)
@classmethod
def autodetect(cls, source: str | Path) -> PDF:
"""Attempt to autodetect a PDF from a source string or Path.
Args:
source (Union[str,path]): The source string or path.
Returns:
A PDF if the source is detected to be a valid PDF.
Raises:
ValueError: If the source is not detected to be a valid PDF.
"""
if isinstance(source, str):
if cls.is_base64(source):
return cls.from_base64(source)
if source.startswith(("http://", "https://")):
return cls.from_url(source)
if source.startswith("gs://"):
return cls.from_gs_url(source)
try:
if Path(source).is_file():
return cls.from_path(source)
except FileNotFoundError as err:
raise MultimodalError(
"PDF file not found",
content_type="pdf",
file_path=str(source),
) from err
except OSError as e:
if e.errno == 63: # File name too long
raise MultimodalError(
"PDF file name too long",
content_type="pdf",
file_path=str(source),
) from e
raise MultimodalError(
"Unable to read PDF file",
content_type="pdf",
file_path=str(source),
) from e
return cls.from_raw_base64(source)
if isinstance(source, Path):
return cls.from_path(source)
raise ValueError(f"Unsupported PDF source type: {type(source).__name__}")
@classmethod
def autodetect_safely(cls, source: Union[str, Path]) -> Union[PDF, str]: # noqa: UP007
"""Safely attempt to autodetect a PDF from a source string or path.
Args:
source (Union[str,path]): The source string or path.
Returns:
A PDF if the source is detected to be a valid PDF, otherwise
the source itself as a string.
"""
try:
return cls.autodetect(source)
except ValueError:
return str(source)
@classmethod
def is_base64(cls, s: str) -> bool:
return bool(re.match(r"^data:application/pdf;base64,", s))
@classmethod
def from_base64(cls, data_uri: str) -> PDF:
header, encoded = data_uri.split(",", 1)
media_type = header.split(":")[1].split(";")[0]
if media_type not in VALID_PDF_MIME_TYPES:
raise ValueError(f"Unsupported PDF format: {media_type}")
return cls(
source=data_uri,
media_type=media_type,
data=encoded,
)
@classmethod
@lru_cache
def from_path(cls, path: str | Path) -> PDF:
path = Path(path)
if not path.is_file():
raise FileNotFoundError(f"PDF file not found: {path}")
if path.stat().st_size == 0:
raise ValueError("PDF file is empty")
media_type, _ = mimetypes.guess_type(str(path))
if media_type not in VALID_PDF_MIME_TYPES:
raise ValueError(f"Unsupported PDF format: {media_type}")
data = base64.b64encode(path.read_bytes()).decode("utf-8")
return cls(source=path, media_type=media_type, data=data)
@classmethod
def from_raw_base64(cls, data: str) -> PDF:
try:
decoded = base64.b64decode(data)
# Check if it's a valid PDF by looking for the PDF header
if decoded.startswith(b"%PDF-"):
return cls(
source=data,
media_type="application/pdf",
data=data,
)
raise ValueError("Invalid PDF format")
except Exception as e:
raise ValueError("Invalid or unsupported base64 PDF data") from e
@classmethod
def from_gs_url(cls, data_uri: str, timeout: int = 30) -> PDF:
"""
Create a PDF instance from a Google Cloud Storage URL.
Args:
data_uri: GCS URL starting with gs://
timeout: Request timeout in seconds (default: 30)
"""
if not data_uri.startswith("gs://"):
raise ValueError("URL must start with gs://")
public_url = f"https://storage.googleapis.com/{data_uri[5:]}"
try:
response = requests.get(public_url, timeout=timeout)
response.raise_for_status()
media_type = response.headers.get("Content-Type", "application/pdf")
if media_type not in VALID_PDF_MIME_TYPES:
raise ValueError(f"Unsupported PDF format: {media_type}")
data = base64.b64encode(response.content).decode("utf-8")
return cls(source=data_uri, media_type=media_type, data=data)
except requests.RequestException as e:
raise ValueError(
"Failed to access GCS PDF (must be publicly readable)"
) from e
@classmethod
@lru_cache
def from_url(cls, url: str) -> PDF:
if url.startswith("gs://"):
return cls.from_gs_url(url)
parsed_url = urlparse(url)
media_type, _ = mimetypes.guess_type(parsed_url.path)
if not media_type:
try:
response = requests.head(url, allow_redirects=True)
media_type = response.headers.get("Content-Type")
except requests.RequestException as e:
raise ValueError("Failed to fetch PDF from URL") from e
if media_type not in VALID_PDF_MIME_TYPES:
raise ValueError(f"Unsupported PDF format: {media_type}")
return cls(source=url, media_type=media_type, data=None)
def to_mistral(self) -> dict[str, Any]:
from instructor.v2.providers.mistral.multimodal import pdf_to_mistral
return pdf_to_mistral(self)
def to_openai(self, mode: Mode) -> dict[str, Any]:
from instructor.v2.providers.openai.multimodal import pdf_to_openai
return pdf_to_openai(self, mode)
def to_anthropic(self) -> dict[str, Any]:
from instructor.v2.providers.anthropic.multimodal import pdf_to_anthropic
return pdf_to_anthropic(self)
def to_genai(self):
from instructor.v2.providers.genai.multimodal import pdf_to_genai
return pdf_to_genai(self)
def to_bedrock(self, name: str | None = None) -> dict[str, Any]:
"""Convert to Bedrock's document format."""
# Determine the document name
if name is None:
if isinstance(self.source, Path):
name = self.source.name
elif isinstance(self.source, str):
# Try to extract filename from path or URL
if self.source.startswith(("http://", "https://", "gs://")):
name = Path(urlparse(self.source).path).name or "document"
else:
name = (
Path(self.source).name
if Path(self.source).exists()
else "document"
)
else:
name = "document"
# Sanitize name according to Bedrock requirements
# Only allow alphanumeric, whitespace (max one in row), hyphens, parentheses, square brackets
name = re.sub(r"[^\w\s\-\(\)\[\]]", "", name)
name = re.sub(r"\s+", " ", name) # Consolidate whitespace
name = name.strip()
# Handle S3 URIs
if isinstance(self.source, str) and self.source.startswith("s3://"):
if not re.match(r"s3://[^/]+/.*", self.source):
raise ValueError(f"Invalid S3 URI format: {self.source}")
# Note: bucketOwner is optional but recommended for cross-account access
return {
"document": {
"format": "pdf",
"name": name,
"source": {
"s3Location": {
"uri": self.source
# "bucketOwner": "account-id" # Optional, can be added by user
}
},
}
}
# Handle bytes-based sources (base64 only)
if not self.data:
raise ValueError(
"PDF data is missing. Provide base64-encoded data or use an s3:// source."
)
# Decode base64 data to bytes
pdf_bytes = base64.b64decode(self.data)
return {
"document": {"format": "pdf", "name": name, "source": {"bytes": pdf_bytes}}
}
class PDFWithCacheControl(PDF):
"""PDF with Anthropic prompt caching support."""
def to_anthropic(self) -> dict[str, Any]:
from instructor.v2.providers.anthropic.multimodal import (
pdf_with_cache_control_to_anthropic,
)
return pdf_with_cache_control_to_anthropic(self)
class PDFWithGenaiFile(PDF):
@classmethod
def from_new_genai_file(
cls, file_path: str, retry_delay: int = 10, max_retries: int = 20
) -> PDFWithGenaiFile:
from instructor.v2.providers.genai.multimodal import upload_new_pdf_file
return upload_new_pdf_file(cls, file_path, retry_delay, max_retries)
@classmethod
def from_existing_genai_file(cls, file_name: str) -> PDFWithGenaiFile:
from instructor.v2.providers.genai.multimodal import load_existing_pdf_file
return load_existing_pdf_file(cls, file_name)
def to_genai(self):
from instructor.v2.providers.genai.multimodal import uploaded_pdf_to_genai
return uploaded_pdf_to_genai(self)
def convert_contents(
contents: Union[ # noqa: UP007
str,
dict[str, Any],
Image,
Audio,
PDF,
list[Union[str, dict[str, Any], Image, Audio, PDF]], # noqa: UP007
],
mode: Mode,
) -> Union[str, list[dict[str, Any]]]: # noqa: UP007
"""Convert content items to the appropriate format based on the specified mode."""
if isinstance(contents, str):
return contents
if isinstance(contents, (Image, Audio, PDF)) or isinstance(contents, dict):
contents = [contents]
converted_contents: list[dict[str, Union[str, Image]]] = [] # noqa: UP007
text_file_type = (
"input_text"
if mode in {Mode.RESPONSES_TOOLS, Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS}
else "text"
)
for content in contents:
if isinstance(content, str):
converted_contents.append({"type": text_file_type, "text": content})
elif isinstance(content, dict):
converted_contents.append(content)
elif isinstance(content, (Image, Audio, PDF)):
if mode in {
Mode.ANTHROPIC_JSON,
Mode.ANTHROPIC_TOOLS,
Mode.ANTHROPIC_REASONING_TOOLS,
}:
converted_contents.append(content.to_anthropic())
elif mode in {Mode.GEMINI_JSON, Mode.GEMINI_TOOLS}:
raise NotImplementedError("Gemini is not supported yet")
elif mode in {
Mode.MISTRAL_STRUCTURED_OUTPUTS,
Mode.MISTRAL_TOOLS,
} and isinstance(content, (PDF)):
converted_contents.append(content.to_mistral())
else:
converted_contents.append(content.to_openai(mode))
else:
raise ValueError(f"Unsupported content type: {type(content)}")
return converted_contents
def autodetect_media(
source: str | Path | Image | Audio | PDF,
) -> Image | Audio | PDF | str:
"""Autodetect images, audio, or PDFs from a given source.
Args:
source: URL, file path, Path, or data URI to inspect.
Returns:
The detected :class:`Image`, :class:`Audio`, or :class:`PDF` instance.
If detection fails, the original source is returned.
"""
if isinstance(source, (Image, Audio, PDF)):
return source
# Normalize once for cheap checks and mimetype guess
source = str(source)
if source.startswith("data:image/"):
return Image.autodetect_safely(source)
if source.startswith("data:audio/"):
return Audio.autodetect_safely(source)
if source.startswith("data:application/pdf"):
return PDF.autodetect_safely(source)
media_type, _ = mimetypes.guess_type(source)
if media_type in VALID_MIME_TYPES:
return Image.autodetect_safely(source)
if media_type in VALID_AUDIO_MIME_TYPES:
return Audio.autodetect_safely(source)
if media_type in VALID_PDF_MIME_TYPES:
return PDF.autodetect_safely(source)
for cls in (Image, Audio, PDF):
item = cls.autodetect_safely(source) # type: ignore[arg-type]
if not isinstance(item, str):
return item
return source
def convert_messages(
messages: list[
dict[
str,
Union[ # noqa: UP007
str,
dict[str, Any],
Image,
Audio,
PDF,
list[Union[str, dict[str, Any], Image, Audio, PDF]], # noqa: UP007
],
]
],
mode: Mode,
autodetect_images: bool = False,
) -> list[dict[str, Any]]:
"""Convert messages to the appropriate format based on the specified mode."""
converted_messages: list[dict[str, Any]] = []
def is_image_params(x: Any) -> bool:
return isinstance(x, dict) and x.get("type") == "image" and "source" in x
for message in messages:
if "type" in message:
if message["type"] in {"audio", "image"}:
converted_messages.append(message)
continue
raise ValueError(f"Unsupported message type: {message['type']}")
role = message["role"]
content = message["content"] or []
other_kwargs = {
k: v for k, v in message.items() if k not in ["role", "content", "type"]
}
if autodetect_images:
if isinstance(content, list):
new_content: list[str | dict[str, Any] | Image | Audio | PDF] = [] # noqa: UP007
for item in content:
if isinstance(item, str):
new_content.append(autodetect_media(item))
elif is_image_params(item):
new_content.append(
ImageWithCacheControl.from_image_params(
cast(ImageParams, item)
)
)
else:
new_content.append(item)
content = new_content
elif isinstance(content, str):
content = autodetect_media(content)
elif is_image_params(content):
content = ImageWithCacheControl.from_image_params(
cast(ImageParams, content)
)
if isinstance(content, str):
converted_messages.append(
{"role": role, "content": content, **other_kwargs}
)
else:
# At this point content is narrowed to non-str types accepted by convert_contents
converted_content = convert_contents(content, mode)
converted_messages.append(
{"role": role, "content": converted_content, **other_kwargs}
)
return converted_messages
def extract_genai_multimodal_content(
contents: list[Any],
autodetect_images: bool = True,
):
"""Compatibility wrapper for the GenAI-owned multimodal converter."""
from instructor.v2.providers.genai.multimodal import extract_multimodal_content
return extract_multimodal_content(contents, autodetect_images)