mirascope 1.16.9__py3-none-any.whl → 1.17.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mirascope/__init__.py +20 -1
- mirascope/core/anthropic/_utils/_convert_message_params.py +13 -0
- mirascope/core/azure/_utils/_convert_message_params.py +10 -0
- mirascope/core/azure/_utils/_message_param_converter.py +46 -12
- mirascope/core/base/__init__.py +4 -0
- mirascope/core/base/_utils/_convert_messages_to_message_params.py +36 -3
- mirascope/core/base/_utils/_parse_content_template.py +35 -9
- mirascope/core/base/message_param.py +30 -2
- mirascope/core/base/messages.py +10 -0
- mirascope/core/bedrock/_utils/_convert_message_params.py +18 -1
- mirascope/core/gemini/_utils/_convert_message_params.py +48 -5
- mirascope/core/gemini/_utils/_message_param_converter.py +51 -5
- mirascope/core/gemini/_utils/_setup_call.py +12 -2
- mirascope/core/groq/_utils/_convert_message_params.py +9 -0
- mirascope/core/groq/_utils/_message_param_converter.py +9 -2
- mirascope/core/mistral/_utils/_convert_message_params.py +7 -0
- mirascope/core/mistral/_utils/_message_param_converter.py +41 -35
- mirascope/core/openai/_utils/_convert_message_params.py +38 -1
- mirascope/core/openai/_utils/_message_param_converter.py +28 -4
- mirascope/core/vertex/_utils/_convert_message_params.py +56 -6
- mirascope/core/vertex/_utils/_message_param_converter.py +13 -5
- mirascope/core/vertex/_utils/_setup_call.py +10 -1
- mirascope/llm/call_response.py +5 -1
- mirascope/retries/__init__.py +5 -0
- mirascope/retries/fallback.py +128 -0
- {mirascope-1.16.9.dist-info → mirascope-1.17.0.dist-info}/METADATA +1 -1
- {mirascope-1.16.9.dist-info → mirascope-1.17.0.dist-info}/RECORD +29 -28
- {mirascope-1.16.9.dist-info → mirascope-1.17.0.dist-info}/WHEEL +0 -0
- {mirascope-1.16.9.dist-info → mirascope-1.17.0.dist-info}/licenses/LICENSE +0 -0
mirascope/__init__.py
CHANGED
|
@@ -6,6 +6,15 @@ from contextlib import suppress
|
|
|
6
6
|
with suppress(ImportError):
|
|
7
7
|
from . import core as core
|
|
8
8
|
|
|
9
|
+
from .core import (
|
|
10
|
+
BaseDynamicConfig,
|
|
11
|
+
BaseMessageParam,
|
|
12
|
+
BaseTool,
|
|
13
|
+
BaseToolKit,
|
|
14
|
+
Messages,
|
|
15
|
+
prompt_template,
|
|
16
|
+
)
|
|
17
|
+
|
|
9
18
|
with suppress(ImportError):
|
|
10
19
|
from . import integrations as integrations
|
|
11
20
|
|
|
@@ -14,4 +23,14 @@ with suppress(ImportError):
|
|
|
14
23
|
|
|
15
24
|
__version__ = importlib.metadata.version("mirascope")
|
|
16
25
|
|
|
17
|
-
__all__ = [
|
|
26
|
+
__all__ = [
|
|
27
|
+
"BaseDynamicConfig",
|
|
28
|
+
"BaseMessageParam",
|
|
29
|
+
"BaseTool",
|
|
30
|
+
"BaseToolKit",
|
|
31
|
+
"core",
|
|
32
|
+
"integrations",
|
|
33
|
+
"prompt_template",
|
|
34
|
+
"retries",
|
|
35
|
+
"__version__",
|
|
36
|
+
]
|
|
@@ -5,6 +5,7 @@ import base64
|
|
|
5
5
|
from anthropic.types import MessageParam
|
|
6
6
|
|
|
7
7
|
from ...base import BaseMessageParam
|
|
8
|
+
from ...base._utils._parse_content_template import _load_media, get_image_type
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def convert_message_params(
|
|
@@ -45,6 +46,18 @@ def convert_message_params(
|
|
|
45
46
|
},
|
|
46
47
|
}
|
|
47
48
|
)
|
|
49
|
+
elif part.type == "image_url":
|
|
50
|
+
image = _load_media(part.url)
|
|
51
|
+
converted_content.append(
|
|
52
|
+
{
|
|
53
|
+
"type": "image",
|
|
54
|
+
"source": {
|
|
55
|
+
"data": base64.b64encode(image).decode("utf-8"),
|
|
56
|
+
"media_type": f"image/{get_image_type(image)}",
|
|
57
|
+
"type": "base64",
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
)
|
|
48
61
|
elif part.type == "document":
|
|
49
62
|
if part.media_type != "application/pdf":
|
|
50
63
|
raise ValueError(
|
|
@@ -51,6 +51,16 @@ def convert_message_params(
|
|
|
51
51
|
},
|
|
52
52
|
}
|
|
53
53
|
)
|
|
54
|
+
elif part.type == "image_url":
|
|
55
|
+
converted_content.append(
|
|
56
|
+
{
|
|
57
|
+
"type": "image_url",
|
|
58
|
+
"image_url": {
|
|
59
|
+
"url": part.url,
|
|
60
|
+
"detail": part.detail if part.detail else "auto",
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
)
|
|
54
64
|
elif part.type == "tool_call":
|
|
55
65
|
converted_message_param = AssistantMessage(
|
|
56
66
|
tool_calls=[
|
|
@@ -4,18 +4,43 @@ from typing import cast
|
|
|
4
4
|
from azure.ai.inference.models import (
|
|
5
5
|
AssistantMessage,
|
|
6
6
|
ChatRequestMessage,
|
|
7
|
+
ContentItem,
|
|
8
|
+
ImageContentItem,
|
|
7
9
|
TextContentItem,
|
|
8
10
|
ToolMessage,
|
|
9
11
|
UserMessage,
|
|
10
12
|
)
|
|
11
13
|
|
|
12
14
|
from mirascope.core.azure._utils import convert_message_params
|
|
13
|
-
from mirascope.core.base import
|
|
15
|
+
from mirascope.core.base import (
|
|
16
|
+
BaseMessageParam,
|
|
17
|
+
ImageURLPart,
|
|
18
|
+
TextPart,
|
|
19
|
+
ToolCallPart,
|
|
20
|
+
ToolResultPart,
|
|
21
|
+
)
|
|
14
22
|
from mirascope.core.base._utils._base_message_param_converter import (
|
|
15
23
|
BaseMessageParamConverter,
|
|
16
24
|
)
|
|
17
25
|
|
|
18
26
|
|
|
27
|
+
def _parse_content(content: list[ContentItem]) -> list[TextPart | ImageURLPart]:
|
|
28
|
+
converted_parts = []
|
|
29
|
+
for part in content:
|
|
30
|
+
if isinstance(part, TextContentItem):
|
|
31
|
+
converted_parts.append(TextPart(type="text", text=part.text))
|
|
32
|
+
elif isinstance(part, ImageContentItem):
|
|
33
|
+
converted_parts.append(
|
|
34
|
+
ImageURLPart(
|
|
35
|
+
type="image_url",
|
|
36
|
+
url=part.image_url.url,
|
|
37
|
+
detail=part.image_url.detail,
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return converted_parts
|
|
42
|
+
|
|
43
|
+
|
|
19
44
|
class AzureMessageParamConverter(BaseMessageParamConverter):
|
|
20
45
|
"""Converts between Azure `ChatRequestMessage` / `AssistantMessage` and Mirascope `BaseMessageParam`."""
|
|
21
46
|
|
|
@@ -41,19 +66,14 @@ class AzureMessageParamConverter(BaseMessageParamConverter):
|
|
|
41
66
|
BaseMessageParam(role="user", content=message_param.content)
|
|
42
67
|
)
|
|
43
68
|
elif isinstance(message_param.content, list):
|
|
44
|
-
converted_parts =
|
|
45
|
-
for part in message_param.content:
|
|
46
|
-
if isinstance(part, TextContentItem):
|
|
47
|
-
converted_parts.append(
|
|
48
|
-
TextPart(type="text", text=part.text)
|
|
49
|
-
)
|
|
50
|
-
# TODO: add support for image and audio parts here
|
|
69
|
+
converted_parts = _parse_content(message_param.content)
|
|
51
70
|
converted.append(
|
|
52
71
|
BaseMessageParam(role="user", content=converted_parts)
|
|
53
72
|
)
|
|
54
73
|
elif isinstance(message_param, AssistantMessage):
|
|
74
|
+
converted_parts = []
|
|
55
75
|
if tool_calls := message_param.tool_calls:
|
|
56
|
-
|
|
76
|
+
converted_parts.extend(
|
|
57
77
|
ToolCallPart(
|
|
58
78
|
type="tool_call",
|
|
59
79
|
name=tool_call.function.name,
|
|
@@ -61,10 +81,24 @@ class AzureMessageParamConverter(BaseMessageParamConverter):
|
|
|
61
81
|
args=json.loads(tool_call.function.arguments),
|
|
62
82
|
)
|
|
63
83
|
for tool_call in tool_calls
|
|
64
|
-
|
|
84
|
+
)
|
|
85
|
+
if isinstance(message_param.content, str):
|
|
86
|
+
converted_parts.append(
|
|
87
|
+
TextPart(type="text", text=message_param.content)
|
|
88
|
+
)
|
|
89
|
+
elif isinstance(message_param.content, list):
|
|
90
|
+
converted_parts = _parse_content(message_param.content)
|
|
65
91
|
else:
|
|
66
|
-
|
|
67
|
-
converted.append(
|
|
92
|
+
converted_parts.append(TextPart(type="text", text=""))
|
|
93
|
+
converted.append(
|
|
94
|
+
BaseMessageParam(
|
|
95
|
+
role="assistant",
|
|
96
|
+
content=converted_parts[0].text
|
|
97
|
+
if len(converted_parts) == 1
|
|
98
|
+
and isinstance(converted_parts[0], TextPart)
|
|
99
|
+
else converted_parts,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
68
102
|
elif isinstance(message_param, ToolMessage):
|
|
69
103
|
converted.append(
|
|
70
104
|
BaseMessageParam(
|
mirascope/core/base/__init__.py
CHANGED
|
@@ -12,10 +12,12 @@ from .from_call_args import FromCallArgs
|
|
|
12
12
|
from .merge_decorators import merge_decorators
|
|
13
13
|
from .message_param import (
|
|
14
14
|
AudioPart,
|
|
15
|
+
AudioURLPart,
|
|
15
16
|
BaseMessageParam,
|
|
16
17
|
CacheControlPart,
|
|
17
18
|
DocumentPart,
|
|
18
19
|
ImagePart,
|
|
20
|
+
ImageURLPart,
|
|
19
21
|
TextPart,
|
|
20
22
|
ToolCallPart,
|
|
21
23
|
ToolResultPart,
|
|
@@ -32,6 +34,7 @@ from .types import AudioSegment
|
|
|
32
34
|
|
|
33
35
|
__all__ = [
|
|
34
36
|
"AudioPart",
|
|
37
|
+
"AudioURLPart",
|
|
35
38
|
"AudioSegment",
|
|
36
39
|
"BaseCallKwargs",
|
|
37
40
|
"BaseCallParams",
|
|
@@ -52,6 +55,7 @@ __all__ = [
|
|
|
52
55
|
"FromCallArgs",
|
|
53
56
|
"GenerateJsonSchemaNoTitles",
|
|
54
57
|
"ImagePart",
|
|
58
|
+
"ImageURLPart",
|
|
55
59
|
"merge_decorators",
|
|
56
60
|
"metadata",
|
|
57
61
|
"Messages",
|
|
@@ -10,10 +10,12 @@ from typing_extensions import TypeIs
|
|
|
10
10
|
|
|
11
11
|
from ..message_param import (
|
|
12
12
|
AudioPart,
|
|
13
|
+
AudioURLPart,
|
|
13
14
|
BaseMessageParam,
|
|
14
15
|
CacheControlPart,
|
|
15
16
|
DocumentPart,
|
|
16
17
|
ImagePart,
|
|
18
|
+
ImageURLPart,
|
|
17
19
|
TextPart,
|
|
18
20
|
)
|
|
19
21
|
from ..types import AudioSegment, Image, has_pil_module, has_pydub_module
|
|
@@ -29,17 +31,33 @@ def _convert_message_sequence_part_to_content_part(
|
|
|
29
31
|
| TextPart
|
|
30
32
|
| CacheControlPart
|
|
31
33
|
| ImagePart
|
|
34
|
+
| ImageURLPart
|
|
32
35
|
| Image.Image
|
|
33
36
|
| AudioPart
|
|
37
|
+
| AudioURLPart
|
|
34
38
|
| AudioSegment
|
|
35
39
|
| Wave_read
|
|
36
40
|
| DocumentPart,
|
|
37
|
-
) ->
|
|
41
|
+
) -> (
|
|
42
|
+
TextPart
|
|
43
|
+
| ImagePart
|
|
44
|
+
| ImageURLPart
|
|
45
|
+
| AudioPart
|
|
46
|
+
| AudioURLPart
|
|
47
|
+
| CacheControlPart
|
|
48
|
+
| DocumentPart
|
|
49
|
+
):
|
|
38
50
|
if isinstance(message_sequence_part, str):
|
|
39
51
|
return TextPart(text=message_sequence_part, type="text")
|
|
40
52
|
elif isinstance(
|
|
41
53
|
message_sequence_part,
|
|
42
|
-
TextPart
|
|
54
|
+
TextPart
|
|
55
|
+
| ImagePart
|
|
56
|
+
| ImageURLPart
|
|
57
|
+
| AudioPart
|
|
58
|
+
| AudioURLPart
|
|
59
|
+
| CacheControlPart
|
|
60
|
+
| DocumentPart,
|
|
43
61
|
):
|
|
44
62
|
return message_sequence_part
|
|
45
63
|
elif has_pil_module and isinstance(message_sequence_part, Image.Image):
|
|
@@ -82,13 +100,26 @@ def convert_message_content_to_message_param_content(
|
|
|
82
100
|
| TextPart
|
|
83
101
|
| CacheControlPart
|
|
84
102
|
| ImagePart
|
|
103
|
+
| ImageURLPart
|
|
85
104
|
| Image.Image
|
|
86
105
|
| AudioPart
|
|
106
|
+
| AudioURLPart
|
|
87
107
|
| AudioSegment
|
|
88
108
|
| Wave_read
|
|
89
109
|
| DocumentPart
|
|
90
110
|
],
|
|
91
|
-
) ->
|
|
111
|
+
) -> (
|
|
112
|
+
list[
|
|
113
|
+
TextPart
|
|
114
|
+
| ImagePart
|
|
115
|
+
| ImageURLPart
|
|
116
|
+
| AudioPart
|
|
117
|
+
| AudioURLPart
|
|
118
|
+
| CacheControlPart
|
|
119
|
+
| DocumentPart
|
|
120
|
+
]
|
|
121
|
+
| str
|
|
122
|
+
):
|
|
92
123
|
if isinstance(message_sequence, str):
|
|
93
124
|
return message_sequence
|
|
94
125
|
return [
|
|
@@ -113,8 +144,10 @@ def convert_messages_to_message_params(
|
|
|
113
144
|
| TextPart
|
|
114
145
|
| CacheControlPart
|
|
115
146
|
| ImagePart
|
|
147
|
+
| ImageURLPart
|
|
116
148
|
| Image.Image
|
|
117
149
|
| AudioPart
|
|
150
|
+
| AudioURLPart
|
|
118
151
|
| AudioSegment
|
|
119
152
|
| Wave_read
|
|
120
153
|
| DocumentPart
|
|
@@ -9,10 +9,12 @@ from typing_extensions import TypedDict
|
|
|
9
9
|
|
|
10
10
|
from ..message_param import (
|
|
11
11
|
AudioPart,
|
|
12
|
+
AudioURLPart,
|
|
12
13
|
BaseMessageParam,
|
|
13
14
|
CacheControlPart,
|
|
14
15
|
DocumentPart,
|
|
15
16
|
ImagePart,
|
|
17
|
+
ImageURLPart,
|
|
16
18
|
TextPart,
|
|
17
19
|
)
|
|
18
20
|
from ..types import Image, has_pil_module
|
|
@@ -138,7 +140,12 @@ def _load_media(source: str | bytes) -> bytes:
|
|
|
138
140
|
|
|
139
141
|
def _construct_image_part(
|
|
140
142
|
source: str | bytes | Image.Image, options: dict[str, str] | None
|
|
141
|
-
) -> ImagePart:
|
|
143
|
+
) -> ImagePart | ImageURLPart:
|
|
144
|
+
detail = None
|
|
145
|
+
if options:
|
|
146
|
+
detail = options.get("detail", None)
|
|
147
|
+
if isinstance(source, str) and source.startswith(("http://", "https://", "gs://")):
|
|
148
|
+
return ImageURLPart(type="image_url", url=source, detail=detail)
|
|
142
149
|
if isinstance(source, Image.Image):
|
|
143
150
|
image = pil_image_to_bytes(source)
|
|
144
151
|
media_type = (
|
|
@@ -149,9 +156,6 @@ def _construct_image_part(
|
|
|
149
156
|
else:
|
|
150
157
|
image = _load_media(source)
|
|
151
158
|
media_type = f"image/{get_image_type(image)}"
|
|
152
|
-
detail = None
|
|
153
|
-
if options:
|
|
154
|
-
detail = options.get("detail", None)
|
|
155
159
|
return ImagePart(
|
|
156
160
|
type="image",
|
|
157
161
|
media_type=media_type,
|
|
@@ -160,8 +164,10 @@ def _construct_image_part(
|
|
|
160
164
|
)
|
|
161
165
|
|
|
162
166
|
|
|
163
|
-
def _construct_audio_part(source: str | bytes) -> AudioPart:
|
|
167
|
+
def _construct_audio_part(source: str | bytes) -> AudioPart | AudioURLPart:
|
|
164
168
|
# Note: audio does not currently support additional options, at least for now.
|
|
169
|
+
if isinstance(source, str) and source.startswith(("http://", "https://", "gs://")):
|
|
170
|
+
return AudioURLPart(type="audio_url", url=source)
|
|
165
171
|
audio = _load_media(source)
|
|
166
172
|
return AudioPart(
|
|
167
173
|
type="audio", media_type=f"audio/{get_audio_type(audio)}", audio=audio
|
|
@@ -179,8 +185,16 @@ def _construct_document_part(source: str | bytes) -> DocumentPart:
|
|
|
179
185
|
|
|
180
186
|
def _construct_parts(
|
|
181
187
|
part: _Part, attrs: dict[str, Any]
|
|
182
|
-
) -> list[
|
|
183
|
-
|
|
188
|
+
) -> list[
|
|
189
|
+
TextPart
|
|
190
|
+
| ImagePart
|
|
191
|
+
| ImageURLPart
|
|
192
|
+
| AudioPart
|
|
193
|
+
| AudioURLPart
|
|
194
|
+
| CacheControlPart
|
|
195
|
+
| DocumentPart
|
|
196
|
+
]:
|
|
197
|
+
if part["type"] in "image":
|
|
184
198
|
source = attrs[part["template"]]
|
|
185
199
|
return [_construct_image_part(source, part["options"])] if source else []
|
|
186
200
|
elif part["type"] == "images":
|
|
@@ -229,7 +243,13 @@ def _construct_parts(
|
|
|
229
243
|
source = attrs[part["template"]]
|
|
230
244
|
if not isinstance(
|
|
231
245
|
source,
|
|
232
|
-
TextPart
|
|
246
|
+
TextPart
|
|
247
|
+
| ImagePart
|
|
248
|
+
| ImageURLPart
|
|
249
|
+
| AudioPart
|
|
250
|
+
| AudioURLPart
|
|
251
|
+
| CacheControlPart
|
|
252
|
+
| DocumentPart,
|
|
233
253
|
):
|
|
234
254
|
raise ValueError(
|
|
235
255
|
f"When using 'part' template, '{part['template']}' must be a valid content part."
|
|
@@ -246,7 +266,13 @@ def _construct_parts(
|
|
|
246
266
|
for source in sources:
|
|
247
267
|
if not isinstance(
|
|
248
268
|
source,
|
|
249
|
-
TextPart
|
|
269
|
+
TextPart
|
|
270
|
+
| ImagePart
|
|
271
|
+
| ImageURLPart
|
|
272
|
+
| AudioPart
|
|
273
|
+
| AudioURLPart
|
|
274
|
+
| CacheControlPart
|
|
275
|
+
| DocumentPart,
|
|
250
276
|
):
|
|
251
277
|
raise ValueError(
|
|
252
278
|
f"When using 'parts' template, '{part['template']}' must be a list of valid content parts."
|
|
@@ -49,18 +49,44 @@ class ImagePart(BaseModel):
|
|
|
49
49
|
detail: str | None
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
class ImageURLPart(BaseModel):
|
|
53
|
+
"""A content part for images with a URL or base64 encoded image data.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
type: Always "image_url"
|
|
57
|
+
url: The URL to the image
|
|
58
|
+
detail: (Optional) The detail to use for the image (supported by OpenAI)
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
type: Literal["image_url"]
|
|
62
|
+
url: str
|
|
63
|
+
detail: str | None
|
|
64
|
+
|
|
65
|
+
|
|
52
66
|
class AudioPart(BaseModel):
|
|
53
67
|
"""A content part for audio.
|
|
54
68
|
|
|
55
69
|
Attributes:
|
|
56
70
|
type: Always "audio"
|
|
57
71
|
media_type: The media type (e.g. audio/wav)
|
|
58
|
-
audio: The raw audio bytes
|
|
72
|
+
audio: The raw audio bytes or base64 encoded audio data
|
|
59
73
|
"""
|
|
60
74
|
|
|
61
75
|
type: Literal["audio"]
|
|
62
76
|
media_type: str
|
|
63
|
-
audio: bytes
|
|
77
|
+
audio: bytes | str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AudioURLPart(BaseModel):
|
|
81
|
+
"""A content part for audio with a URL or base64 encoded audio data.
|
|
82
|
+
|
|
83
|
+
Attributes:
|
|
84
|
+
type: Always "audio_url"
|
|
85
|
+
url: The URL to the audio
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
type: Literal["audio_url"]
|
|
89
|
+
url: str
|
|
64
90
|
|
|
65
91
|
|
|
66
92
|
class DocumentPart(BaseModel):
|
|
@@ -125,7 +151,9 @@ class BaseMessageParam(BaseModel):
|
|
|
125
151
|
| Sequence[
|
|
126
152
|
TextPart
|
|
127
153
|
| ImagePart
|
|
154
|
+
| ImageURLPart
|
|
128
155
|
| AudioPart
|
|
156
|
+
| AudioURLPart
|
|
129
157
|
| CacheControlPart
|
|
130
158
|
| DocumentPart
|
|
131
159
|
| ToolCallPart
|
mirascope/core/base/messages.py
CHANGED
|
@@ -10,10 +10,12 @@ from ._utils._convert_messages_to_message_params import (
|
|
|
10
10
|
)
|
|
11
11
|
from .message_param import (
|
|
12
12
|
AudioPart,
|
|
13
|
+
AudioURLPart,
|
|
13
14
|
BaseMessageParam,
|
|
14
15
|
CacheControlPart,
|
|
15
16
|
DocumentPart,
|
|
16
17
|
ImagePart,
|
|
18
|
+
ImageURLPart,
|
|
17
19
|
TextPart,
|
|
18
20
|
)
|
|
19
21
|
from .types import AudioSegment
|
|
@@ -27,8 +29,10 @@ class Messages:
|
|
|
27
29
|
| TextPart
|
|
28
30
|
| CacheControlPart
|
|
29
31
|
| ImagePart
|
|
32
|
+
| ImageURLPart
|
|
30
33
|
| Image.Image
|
|
31
34
|
| AudioPart
|
|
35
|
+
| AudioURLPart
|
|
32
36
|
| AudioSegment
|
|
33
37
|
| Wave_read
|
|
34
38
|
| DocumentPart
|
|
@@ -46,8 +50,10 @@ class Messages:
|
|
|
46
50
|
| TextPart
|
|
47
51
|
| CacheControlPart
|
|
48
52
|
| ImagePart
|
|
53
|
+
| ImageURLPart
|
|
49
54
|
| Image.Image
|
|
50
55
|
| AudioPart
|
|
56
|
+
| AudioURLPart
|
|
51
57
|
| AudioSegment
|
|
52
58
|
| Wave_read
|
|
53
59
|
| DocumentPart
|
|
@@ -67,8 +73,10 @@ class Messages:
|
|
|
67
73
|
| TextPart
|
|
68
74
|
| CacheControlPart
|
|
69
75
|
| ImagePart
|
|
76
|
+
| ImageURLPart
|
|
70
77
|
| Image.Image
|
|
71
78
|
| AudioPart
|
|
79
|
+
| AudioURLPart
|
|
72
80
|
| AudioSegment
|
|
73
81
|
| Wave_read
|
|
74
82
|
| DocumentPart
|
|
@@ -88,8 +96,10 @@ class Messages:
|
|
|
88
96
|
| TextPart
|
|
89
97
|
| CacheControlPart
|
|
90
98
|
| ImagePart
|
|
99
|
+
| ImageURLPart
|
|
91
100
|
| Image.Image
|
|
92
101
|
| AudioPart
|
|
102
|
+
| AudioURLPart
|
|
93
103
|
| AudioSegment
|
|
94
104
|
| Wave_read
|
|
95
105
|
| DocumentPart
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
from typing import cast
|
|
4
4
|
|
|
5
5
|
from ...base import BaseMessageParam
|
|
6
|
+
from ...base._utils import get_image_type
|
|
7
|
+
from ...base._utils._parse_content_template import _load_media
|
|
6
8
|
from .._types import ConversationRoleType, InternalBedrockMessageParam
|
|
7
9
|
|
|
8
10
|
|
|
@@ -37,7 +39,22 @@ def convert_message_params(
|
|
|
37
39
|
" currently only supports JPEG, PNG, GIF, and WebP images."
|
|
38
40
|
)
|
|
39
41
|
converted_content.append(
|
|
40
|
-
{
|
|
42
|
+
{
|
|
43
|
+
"image": {
|
|
44
|
+
"format": part.media_type.split("/")[-1],
|
|
45
|
+
"source": {"bytes": part.image},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
elif part.type == "image_url":
|
|
50
|
+
image = _load_media(part.url)
|
|
51
|
+
converted_content.append(
|
|
52
|
+
{
|
|
53
|
+
"image": {
|
|
54
|
+
"format": get_image_type(image),
|
|
55
|
+
"source": {"bytes": image},
|
|
56
|
+
},
|
|
57
|
+
}
|
|
41
58
|
)
|
|
42
59
|
elif part.type == "tool_result":
|
|
43
60
|
if converted_content:
|
|
@@ -7,6 +7,8 @@ from google.generativeai import protos
|
|
|
7
7
|
from google.generativeai.types import ContentDict
|
|
8
8
|
|
|
9
9
|
from ...base import BaseMessageParam
|
|
10
|
+
from ...base._utils import get_audio_type
|
|
11
|
+
from ...base._utils._parse_content_template import _load_media
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
def convert_message_params(
|
|
@@ -23,13 +25,9 @@ def convert_message_params(
|
|
|
23
25
|
) # pragma: no cover
|
|
24
26
|
converted_message_params += [
|
|
25
27
|
{
|
|
26
|
-
"role": "
|
|
28
|
+
"role": "system",
|
|
27
29
|
"parts": [message_param.content],
|
|
28
30
|
},
|
|
29
|
-
{
|
|
30
|
-
"role": "model",
|
|
31
|
-
"parts": ["Ok! I will adhere to this system message."],
|
|
32
|
-
},
|
|
33
31
|
]
|
|
34
32
|
elif isinstance((content := message_param.content), str):
|
|
35
33
|
converted_message_params.append(
|
|
@@ -55,6 +53,29 @@ def convert_message_params(
|
|
|
55
53
|
)
|
|
56
54
|
image = PIL.Image.open(io.BytesIO(part.image))
|
|
57
55
|
converted_content.append(image)
|
|
56
|
+
elif part.type == "image_url":
|
|
57
|
+
if part.url.startswith(("https://", "http://")):
|
|
58
|
+
image = PIL.Image.open(io.BytesIO(_load_media(part.url)))
|
|
59
|
+
media_type = (
|
|
60
|
+
PIL.Image.MIME[image.format]
|
|
61
|
+
if image.format
|
|
62
|
+
else "image/unknown"
|
|
63
|
+
)
|
|
64
|
+
if media_type not in [
|
|
65
|
+
"image/jpeg",
|
|
66
|
+
"image/png",
|
|
67
|
+
"image/webp",
|
|
68
|
+
"image/heic",
|
|
69
|
+
"image/heif",
|
|
70
|
+
]:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Unsupported image media type: {media_type}. "
|
|
73
|
+
"Gemini currently only supports JPEG, PNG, WebP, HEIC, "
|
|
74
|
+
"and HEIF images."
|
|
75
|
+
)
|
|
76
|
+
converted_content.append(image)
|
|
77
|
+
else:
|
|
78
|
+
converted_content.append(protos.FileData(file_uri=part.url))
|
|
58
79
|
elif part.type == "audio":
|
|
59
80
|
if part.media_type not in [
|
|
60
81
|
"audio/wav",
|
|
@@ -72,6 +93,28 @@ def convert_message_params(
|
|
|
72
93
|
converted_content.append(
|
|
73
94
|
{"mime_type": part.media_type, "data": part.audio}
|
|
74
95
|
)
|
|
96
|
+
elif part.type == "audio_url":
|
|
97
|
+
if part.url.startswith(("https://", "http://")):
|
|
98
|
+
audio = _load_media(part.url)
|
|
99
|
+
audio_type = get_audio_type(audio)
|
|
100
|
+
if audio_type not in [
|
|
101
|
+
"audio/wav",
|
|
102
|
+
"audio/mp3",
|
|
103
|
+
"audio/aiff",
|
|
104
|
+
"audio/aac",
|
|
105
|
+
"audio/ogg",
|
|
106
|
+
"audio/flac",
|
|
107
|
+
]:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"Unsupported audio media type: {audio_type}. "
|
|
110
|
+
"Gemini currently only supports WAV, MP3, AIFF, AAC, OGG, "
|
|
111
|
+
"and FLAC audio file types."
|
|
112
|
+
)
|
|
113
|
+
converted_content.append(
|
|
114
|
+
{"mime_type": audio_type, "data": audio}
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
converted_content.append(protos.FileData(file_uri=part.url))
|
|
75
118
|
elif part.type == "tool_call":
|
|
76
119
|
converted_content.append(
|
|
77
120
|
protos.FunctionCall(
|