mirascope 1.16.9__py3-none-any.whl → 1.18.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.
Files changed (54) hide show
  1. mirascope/__init__.py +20 -1
  2. mirascope/core/__init__.py +4 -0
  3. mirascope/core/anthropic/_utils/_convert_message_params.py +13 -0
  4. mirascope/core/azure/_utils/_convert_message_params.py +10 -0
  5. mirascope/core/azure/_utils/_message_param_converter.py +46 -12
  6. mirascope/core/base/__init__.py +4 -0
  7. mirascope/core/base/_utils/_convert_messages_to_message_params.py +36 -3
  8. mirascope/core/base/_utils/_parse_content_template.py +35 -9
  9. mirascope/core/base/message_param.py +30 -2
  10. mirascope/core/base/messages.py +10 -0
  11. mirascope/core/base/stream_config.py +1 -1
  12. mirascope/core/bedrock/_utils/_convert_message_params.py +18 -1
  13. mirascope/core/gemini/__init__.py +10 -0
  14. mirascope/core/gemini/_utils/_convert_message_params.py +48 -5
  15. mirascope/core/gemini/_utils/_message_param_converter.py +51 -5
  16. mirascope/core/gemini/_utils/_setup_call.py +12 -2
  17. mirascope/core/google/__init__.py +29 -0
  18. mirascope/core/google/_call.py +67 -0
  19. mirascope/core/google/_call_kwargs.py +13 -0
  20. mirascope/core/google/_utils/__init__.py +16 -0
  21. mirascope/core/google/_utils/_calculate_cost.py +88 -0
  22. mirascope/core/google/_utils/_convert_common_call_params.py +39 -0
  23. mirascope/core/google/_utils/_convert_finish_reason_to_common_finish_reasons.py +27 -0
  24. mirascope/core/google/_utils/_convert_message_params.py +177 -0
  25. mirascope/core/google/_utils/_get_json_output.py +37 -0
  26. mirascope/core/google/_utils/_handle_stream.py +35 -0
  27. mirascope/core/google/_utils/_message_param_converter.py +153 -0
  28. mirascope/core/google/_utils/_setup_call.py +180 -0
  29. mirascope/core/google/call_params.py +22 -0
  30. mirascope/core/google/call_response.py +202 -0
  31. mirascope/core/google/call_response_chunk.py +97 -0
  32. mirascope/core/google/dynamic_config.py +26 -0
  33. mirascope/core/google/stream.py +128 -0
  34. mirascope/core/google/tool.py +104 -0
  35. mirascope/core/groq/_utils/_convert_message_params.py +9 -0
  36. mirascope/core/groq/_utils/_message_param_converter.py +9 -2
  37. mirascope/core/mistral/_utils/_convert_message_params.py +7 -0
  38. mirascope/core/mistral/_utils/_message_param_converter.py +41 -35
  39. mirascope/core/openai/_utils/_convert_message_params.py +38 -1
  40. mirascope/core/openai/_utils/_message_param_converter.py +28 -4
  41. mirascope/core/vertex/__init__.py +15 -0
  42. mirascope/core/vertex/_utils/_convert_message_params.py +56 -6
  43. mirascope/core/vertex/_utils/_message_param_converter.py +13 -5
  44. mirascope/core/vertex/_utils/_setup_call.py +10 -1
  45. mirascope/llm/_protocols.py +1 -0
  46. mirascope/llm/call_response.py +5 -1
  47. mirascope/llm/llm_call.py +4 -0
  48. mirascope/llm/llm_override.py +16 -3
  49. mirascope/retries/__init__.py +5 -0
  50. mirascope/retries/fallback.py +128 -0
  51. {mirascope-1.16.9.dist-info → mirascope-1.18.0.dist-info}/METADATA +4 -1
  52. {mirascope-1.16.9.dist-info → mirascope-1.18.0.dist-info}/RECORD +54 -35
  53. {mirascope-1.16.9.dist-info → mirascope-1.18.0.dist-info}/WHEEL +0 -0
  54. {mirascope-1.16.9.dist-info → mirascope-1.18.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__ = ["core", "integrations", "retries", "__version__"]
26
+ __all__ = [
27
+ "BaseDynamicConfig",
28
+ "BaseMessageParam",
29
+ "BaseTool",
30
+ "BaseToolKit",
31
+ "core",
32
+ "integrations",
33
+ "prompt_template",
34
+ "retries",
35
+ "__version__",
36
+ ]
@@ -24,6 +24,9 @@ with suppress(ImportError):
24
24
  with suppress(ImportError):
25
25
  from . import cohere as cohere
26
26
 
27
+ with suppress(ImportError):
28
+ from . import google as google
29
+
27
30
  with suppress(ImportError):
28
31
  from . import gemini as gemini
29
32
 
@@ -57,6 +60,7 @@ __all__ = [
57
60
  "cohere",
58
61
  "FromCallArgs",
59
62
  "gemini",
63
+ "google",
60
64
  "groq",
61
65
  "litellm",
62
66
  "merge_decorators",
@@ -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 BaseMessageParam, TextPart, ToolCallPart, ToolResultPart
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
- content = [
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
- content = message_param.content or ""
67
- converted.append(BaseMessageParam(role="assistant", content=content))
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(
@@ -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
- ) -> TextPart | ImagePart | AudioPart | CacheControlPart | DocumentPart:
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 | ImagePart | AudioPart | CacheControlPart | DocumentPart,
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
- ) -> list[TextPart | ImagePart | AudioPart | CacheControlPart | DocumentPart] | str:
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[TextPart | ImagePart | AudioPart | CacheControlPart | DocumentPart]:
183
- if part["type"] == "image":
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 | ImagePart | AudioPart | CacheControlPart | DocumentPart,
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 | ImagePart | AudioPart | CacheControlPart | DocumentPart,
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
@@ -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
@@ -1,4 +1,4 @@
1
- from typing import TypedDict
1
+ from typing_extensions import TypedDict
2
2
 
3
3
 
4
4
  class StreamConfig(TypedDict):
@@ -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
- {"format": part.media_type, "bytes": part.image}
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:
@@ -1,5 +1,7 @@
1
1
  """The Mirascope Gemini Module."""
2
2
 
3
+ import inspect
4
+ import warnings
3
5
  from typing import TypeAlias
4
6
 
5
7
  from google.generativeai.protos import FunctionResponse
@@ -17,6 +19,14 @@ from .tool import GeminiTool
17
19
 
18
20
  GeminiMessageParam: TypeAlias = ContentDict | FunctionResponse | BaseMessageParam
19
21
 
22
+ warnings.warn(
23
+ inspect.cleandoc("""
24
+ The `mirascope.core.gemini` module is deprecated and will be removed in a future release.
25
+ Please use the `mirascope.core.google` module instead.
26
+ """),
27
+ category=DeprecationWarning,
28
+ )
29
+
20
30
  __all__ = [
21
31
  "call",
22
32
  "GeminiDynamicConfig",
@@ -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": "user",
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(