Coverage for src/appl/core/types/content.py: 84%
56 statements
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-22 15:39 -0800
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-22 15:39 -0800
1import base64
2from io import BytesIO
3from os import PathLike
4from typing import Optional
6import PIL.Image
7from PIL.ImageFile import ImageFile
8from pydantic import BaseModel, Field
10from .basic import *
11from .futures import String, is_string
14class Image(BaseModel):
15 """Represent an image in the message."""
17 url: str = Field(
18 ..., description="Either a URL of the image or the base64 encoded image data."
19 )
20 detail: Optional[str] = Field(
21 None, description="Specifies the detail level of the image."
22 )
24 def __init__(self, url: str, detail: Optional[str] = None) -> None:
25 """Initialize the image with the URL and detail level.
27 See [the guide](https://platform.openai.com/docs/guides/vision/low-or-high-fidelity-image-understanding)
28 for more information about the detail level.
29 """
30 super().__init__(url=url, detail=detail)
32 @classmethod
33 def from_image(cls, image: ImageFile, detail: Optional[str] = None) -> "Image":
34 """Construct an image prompt from a PIL ImageFile."""
35 buffered = BytesIO()
36 # Save the image to the buffer in PNG format
37 image.save(buffered, format="PNG")
38 # Get the byte data from the buffer
39 img_byte = buffered.getvalue()
40 img_base64 = base64.b64encode(img_byte).decode("utf-8")
41 return cls(url=f"data:image/png;base64,{img_base64}", detail=detail)
43 @classmethod
44 def from_file(cls, file: PathLike, detail: Optional[str] = None) -> "Image":
45 """Construct an image prompt from an image file."""
46 image = PIL.Image.open(file)
47 return cls.from_image(image, detail)
49 def __repr__(self) -> str:
50 return f"Image(url={self.url})"
52 def __str__(self) -> str:
53 return f"[Image]{self.url}"
56StrOrImg = Union[String, Image]
57"""A type that can be either a string or an image."""
60class ContentList(BaseModel):
61 """Represent a list of contents containing text and images."""
63 contents: List[StrOrImg] = Field(..., description="The content of the message")
65 def __iadd__(
66 self, other: Union[StrOrImg, List[StrOrImg], "ContentList"]
67 ) -> "ContentList":
68 if isinstance(other, ContentList):
69 self.extend(other.contents)
70 elif isinstance(other, list):
71 self.extend(other)
72 else:
73 self.append(other)
74 return self
76 def append(self, content: StrOrImg) -> None:
77 """Append a content to the list.
79 If the last content is a string, it will be concatenated with the new content.
80 """
81 if is_string(content) and len(self.contents) and is_string(self.contents[-1]):
82 self.contents[-1] += content # type: ignore
83 else:
84 self.contents.append(content)
86 def extend(self, contents: list[StrOrImg]) -> None:
87 """Extend the list with multiple contents."""
88 for content in contents:
89 self.append(content)
91 def get_contents(self) -> List[Dict[str, Any]]:
92 """Return the contents as a list of dictionaries."""
94 def get_dict(content):
95 if isinstance(content, Image):
96 image_args = {"url": content.url}
97 if content.detail:
98 image_args["detail"] = content.detail
99 return {"type": "image_url", "image_url": image_args}
100 return {"type": "text", "text": str(content)}
102 return [get_dict(c) for c in self.contents]
104 def __str__(self) -> str:
105 return "\n".join([str(c) for c in self.contents])