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
@@ -0,0 +1,128 @@
1
+ """The `GoogleStream` class for convenience around streaming LLM calls.
2
+
3
+ usage docs: learn/streams.md
4
+ """
5
+
6
+ from typing import cast
7
+
8
+ from google.genai.types import (
9
+ Candidate,
10
+ Content,
11
+ ContentDict,
12
+ ContentListUnion,
13
+ ContentListUnionDict,
14
+ FinishReason,
15
+ FunctionCall,
16
+ GenerateContentResponse,
17
+ PartDict,
18
+ Tool,
19
+ )
20
+
21
+ from ..base.stream import BaseStream
22
+ from ._utils import calculate_cost
23
+ from .call_params import GoogleCallParams
24
+ from .call_response import GoogleCallResponse
25
+ from .call_response_chunk import GoogleCallResponseChunk
26
+ from .dynamic_config import GoogleDynamicConfig
27
+ from .tool import GoogleTool
28
+
29
+
30
+ class GoogleStream(
31
+ BaseStream[
32
+ GoogleCallResponse,
33
+ GoogleCallResponseChunk,
34
+ ContentDict,
35
+ ContentDict,
36
+ ContentDict,
37
+ ContentListUnion | ContentListUnionDict,
38
+ GoogleTool,
39
+ Tool,
40
+ GoogleDynamicConfig,
41
+ GoogleCallParams,
42
+ FinishReason,
43
+ ]
44
+ ):
45
+ """A class for convenience around streaming Google LLM calls.
46
+
47
+ Example:
48
+
49
+ ```python
50
+ from mirascope.core import prompt_template
51
+ from mirascope.core.google import google_call
52
+
53
+
54
+ @google_call("google-1.5-flash", stream=True)
55
+ def recommend_book(genre: str) -> str:
56
+ return f"Recommend a {genre} book"
57
+
58
+ stream = recommend_book("fantasy") # returns `GoogleStream` instance
59
+ for chunk, _ in stream:
60
+ print(chunk.content, end="", flush=True)
61
+ ```
62
+ """
63
+
64
+ _provider = "google"
65
+
66
+ @property
67
+ def cost(self) -> float | None:
68
+ """Returns the cost of the call."""
69
+ return calculate_cost(self.input_tokens, self.output_tokens, self.model)
70
+
71
+ def _construct_message_param(
72
+ self, tool_calls: list[FunctionCall] | None = None, content: str | None = None
73
+ ) -> ContentDict:
74
+ """Constructs the message parameter for the assistant."""
75
+ return {
76
+ "role": "model",
77
+ "parts": cast(
78
+ list[PartDict],
79
+ [{"text": content}]
80
+ + (
81
+ [
82
+ {"function_call": tool_call}
83
+ for tool_call in (tool_calls or [])
84
+ if tool_calls
85
+ ]
86
+ or []
87
+ ),
88
+ ),
89
+ }
90
+
91
+ def construct_call_response(self) -> GoogleCallResponse:
92
+ """Constructs the call response from a consumed GoogleStream.
93
+
94
+ Raises:
95
+ ValueError: if the stream has not yet been consumed.
96
+ """
97
+ if not hasattr(self, "message_param"):
98
+ raise ValueError(
99
+ "No stream response, check if the stream has been consumed."
100
+ )
101
+ response = GenerateContentResponse(
102
+ candidates=[
103
+ Candidate(
104
+ finish_reason=self.finish_reasons[0]
105
+ if self.finish_reasons
106
+ else FinishReason.STOP,
107
+ content=Content(
108
+ role=self.message_param["role"], # pyright: ignore [reportTypedDictNotRequiredAccess]
109
+ parts=self.message_param["parts"], # pyright: ignore [reportTypedDictNotRequiredAccess, reportArgumentType]
110
+ ),
111
+ )
112
+ ]
113
+ )
114
+
115
+ return GoogleCallResponse(
116
+ metadata=self.metadata,
117
+ response=response,
118
+ tool_types=self.tool_types,
119
+ prompt_template=self.prompt_template,
120
+ fn_args=self.fn_args if self.fn_args else {},
121
+ dynamic_config=self.dynamic_config,
122
+ messages=self.messages,
123
+ call_params=self.call_params,
124
+ call_kwargs=self.call_kwargs,
125
+ user_message_param=self.user_message_param,
126
+ start_time=self.start_time,
127
+ end_time=self.end_time,
128
+ )
@@ -0,0 +1,104 @@
1
+ """The `GoogleTool` class for easy tool usage with Google's Google LLM calls.
2
+
3
+ usage docs: learn/tools.md
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from google.genai.types import (
11
+ FunctionCall,
12
+ FunctionDeclaration,
13
+ Tool,
14
+ )
15
+ from pydantic.json_schema import SkipJsonSchema
16
+
17
+ from ..base import BaseTool
18
+
19
+
20
+ class GoogleTool(BaseTool):
21
+ """A class for defining tools for Google LLM calls.
22
+
23
+ Example:
24
+
25
+ ```python
26
+ from mirascope.core import prompt_template
27
+ from mirascope.core.google import google_call
28
+
29
+
30
+ def format_book(title: str, author: str) -> str:
31
+ return f"{title} by {author}"
32
+
33
+
34
+ @google_call("google-1.5-flash", tools=[format_book])
35
+ def recommend_book(genre: str) -> str:
36
+ return f"Recommend a {genre} book"
37
+
38
+
39
+ response = recommend_book("fantasy")
40
+ if tool := response.tool: # returns an `GoogleTool` instance
41
+ print(tool.call())
42
+ ```
43
+ """
44
+
45
+ __provider__ = "google"
46
+
47
+ tool_call: SkipJsonSchema[FunctionCall]
48
+
49
+ @classmethod
50
+ def tool_schema(cls) -> Tool:
51
+ """Constructs a JSON Schema tool schema from the `BaseModel` schema defined.
52
+
53
+ Example:
54
+ ```python
55
+ from mirascope.core.google import GoogleTool
56
+
57
+
58
+ def format_book(title: str, author: str) -> str:
59
+ return f"{title} by {author}"
60
+
61
+
62
+ tool_type = GoogleTool.type_from_fn(format_book)
63
+ print(tool_type.tool_schema()) # prints the Google-specific tool schema
64
+ ```
65
+ """
66
+ model_schema = cls.model_json_schema()
67
+ fn: dict[str, Any] = {"name": cls._name(), "description": cls._description()}
68
+
69
+ if model_schema["properties"]:
70
+ fn["parameters"] = model_schema
71
+
72
+ if "parameters" in fn:
73
+ if "$defs" in fn["parameters"]:
74
+ raise ValueError(
75
+ "Unfortunately Google's Google API cannot handle nested structures "
76
+ "with $defs."
77
+ )
78
+
79
+ def handle_enum_schema(prop_schema: dict[str, Any]) -> dict[str, Any]:
80
+ if "enum" in prop_schema:
81
+ prop_schema["format"] = "enum"
82
+ return prop_schema
83
+
84
+ fn["parameters"]["properties"] = {
85
+ prop: {
86
+ key: value
87
+ for key, value in handle_enum_schema(prop_schema).items()
88
+ if key != "default"
89
+ }
90
+ for prop, prop_schema in fn["parameters"]["properties"].items()
91
+ }
92
+ return Tool(function_declarations=[FunctionDeclaration(**fn)])
93
+
94
+ @classmethod
95
+ def from_tool_call(cls, tool_call: FunctionCall) -> GoogleTool:
96
+ """Constructs an `GoogleTool` instance from a `tool_call`.
97
+
98
+ Args:
99
+ tool_call: The Google tool call from which to construct this tool instance.
100
+ """
101
+ model_json = {"tool_call": tool_call}
102
+ if tool_call.args:
103
+ model_json |= dict(tool_call.args.items())
104
+ return cls.model_validate(model_json)
@@ -45,6 +45,15 @@ def convert_message_params(
45
45
  },
46
46
  }
47
47
  )
48
+ elif part.type == "image_url":
49
+ converted_content.append(
50
+ {
51
+ "type": "image_url",
52
+ "image_url": {
53
+ "url": part.url,
54
+ },
55
+ }
56
+ )
48
57
  elif part.type == "tool_call":
49
58
  converted_message_param = {
50
59
  "role": "assistant",
@@ -8,7 +8,7 @@ from mirascope.core.base import TextPart, ToolResultPart
8
8
  from mirascope.core.base._utils._base_message_param_converter import (
9
9
  BaseMessageParamConverter,
10
10
  )
11
- from mirascope.core.base.message_param import ToolCallPart
11
+ from mirascope.core.base.message_param import ImageURLPart, ToolCallPart
12
12
  from mirascope.core.groq._utils import convert_message_params
13
13
 
14
14
 
@@ -70,7 +70,14 @@ class GroqMessageParamConverter(BaseMessageParamConverter):
70
70
  for part in content:
71
71
  if "text" in part:
72
72
  contents.append(TextPart(type="text", text=part["text"]))
73
- # TODO: add support for image parts here
73
+ elif "image_url" in part:
74
+ contents.append(
75
+ ImageURLPart(
76
+ type="image_url",
77
+ url=part["image_url"]["url"],
78
+ detail=part["image_url"].get("detail"),
79
+ )
80
+ )
74
81
  if contents:
75
82
  converted.append(
76
83
  BaseMessageParam(role=message_param["role"], content=contents)
@@ -69,6 +69,13 @@ def convert_message_params(
69
69
  )
70
70
  )
71
71
  )
72
+ elif part.type == "image_url":
73
+ converted_content.append(
74
+ {
75
+ "type": "image_url",
76
+ "image_url": part.url,
77
+ }
78
+ )
72
79
  elif part.type == "tool_call":
73
80
  converted_message_params.append(
74
81
  AssistantMessage(
@@ -19,7 +19,22 @@ from mirascope.core.base._utils._base_message_param_converter import (
19
19
  from mirascope.core.mistral._utils import convert_message_params
20
20
 
21
21
  from ...base import BaseMessageParam, ImagePart, TextPart, ToolResultPart
22
- from ...base.message_param import ToolCallPart
22
+ from ...base.message_param import ImageURLPart, ToolCallPart
23
+
24
+
25
+ def _create_image_part_from_data_url(image_url: str) -> ImagePart | None:
26
+ match = re.match(r"data:(image/\w+);base64,(.+)", image_url)
27
+ if not match:
28
+ return None
29
+ mime_type = match.group(1)
30
+ image_base64 = match.group(2)
31
+ image_data = base64.b64decode(image_base64)
32
+ return ImagePart(
33
+ type="image",
34
+ media_type=mime_type,
35
+ image=image_data,
36
+ detail=None,
37
+ )
23
38
 
24
39
 
25
40
  class MistralMessageParamConverter(BaseMessageParamConverter):
@@ -102,44 +117,35 @@ class MistralMessageParamConverter(BaseMessageParamConverter):
102
117
  elif isinstance(chunk, ImageURLChunk):
103
118
  image_url = chunk.image_url
104
119
  if isinstance(image_url, str):
105
- # Extract data from the data URL
106
- match = re.match(r"data:(image/\w+);base64,(.+)", image_url)
107
- if not match:
108
- raise ValueError(
109
- "ImageURLChunk image_url is not in a supported data URL format."
110
- )
111
- mime_type = match.group(1)
112
- image_base64 = match.group(2)
113
- image_data = base64.b64decode(image_base64)
114
- converted_parts.append(
115
- ImagePart(
116
- type="image",
117
- media_type=mime_type,
118
- image=image_data,
119
- detail=None,
120
+ if image_part := _create_image_part_from_data_url(
121
+ image_url
122
+ ):
123
+ converted_parts.append(image_part)
124
+ else:
125
+ converted_parts.append(
126
+ ImageURLPart(
127
+ type="image_url",
128
+ url=image_url,
129
+ detail=None,
130
+ )
120
131
  )
121
- )
132
+
122
133
  else:
123
134
  img_url_str = image_url.url # type: ignore
124
- match = re.match(
125
- r"data:(image/\w+);base64,(.+)", img_url_str
126
- )
127
- if not match:
128
- raise ValueError(
129
- "ImageURLChunk image_url is not in a supported data URL format."
130
- )
131
- mime_type = match.group(1)
132
- image_base64 = match.group(2)
133
- image_data = base64.b64decode(image_base64)
134
- converted_parts.append(
135
- ImagePart(
136
- type="image",
137
- media_type=mime_type,
138
- image=image_data,
139
- detail=None,
135
+ if image_part := _create_image_part_from_data_url(
136
+ img_url_str
137
+ ):
138
+ converted_parts.append(image_part)
139
+ else:
140
+ converted_parts.append(
141
+ ImageURLPart(
142
+ type="image_url",
143
+ url=img_url_str,
144
+ detail=image_url.detail
145
+ if isinstance(image_url.detail, str)
146
+ else None,
147
+ )
140
148
  )
141
- )
142
-
143
149
  elif isinstance(chunk, ReferenceChunk):
144
150
  raise ValueError(
145
151
  "ReferenceChunk is not supported for conversion to BaseMessageParam."
@@ -6,6 +6,8 @@ import json
6
6
  from openai.types.chat import ChatCompletionMessageParam
7
7
 
8
8
  from ...base import BaseMessageParam
9
+ from ...base._utils import get_audio_type
10
+ from ...base._utils._parse_content_template import _load_media
9
11
 
10
12
 
11
13
  def convert_message_params(
@@ -44,6 +46,16 @@ def convert_message_params(
44
46
  },
45
47
  }
46
48
  )
49
+ elif part.type == "image_url":
50
+ converted_content.append(
51
+ {
52
+ "type": "image_url",
53
+ "image_url": {
54
+ "url": part.url,
55
+ "detail": part.detail if part.detail else "auto",
56
+ },
57
+ }
58
+ )
47
59
  elif part.type == "audio":
48
60
  if part.media_type not in [
49
61
  "audio/wav",
@@ -53,11 +65,36 @@ def convert_message_params(
53
65
  f"Unsupported audio media type: {part.media_type}. "
54
66
  "OpenAI currently only supports WAV and MP3 audio file types."
55
67
  )
68
+ data = (
69
+ part.audio
70
+ if isinstance(part.audio, str)
71
+ else base64.b64encode(part.audio).decode("utf-8")
72
+ )
56
73
  converted_content.append(
57
74
  {
58
75
  "input_audio": {
59
76
  "format": part.media_type.split("/")[-1],
60
- "data": base64.b64encode(part.audio).decode("utf-8"),
77
+ "data": data,
78
+ },
79
+ "type": "input_audio",
80
+ }
81
+ )
82
+ elif part.type == "audio_url":
83
+ audio = _load_media(part.url)
84
+ audio_type = get_audio_type(audio)
85
+ if audio_type not in [
86
+ "audio/wav",
87
+ "audio/mp3",
88
+ ]:
89
+ raise ValueError(
90
+ f"Unsupported audio media type: {audio_type}. "
91
+ "OpenAI currently only supports WAV and MP3 audio file types."
92
+ )
93
+ converted_content.append(
94
+ {
95
+ "input_audio": {
96
+ "format": audio_type.split("/")[-1],
97
+ "data": base64.b64encode(audio).decode("utf-8"),
61
98
  },
62
99
  "type": "input_audio",
63
100
  }
@@ -1,12 +1,19 @@
1
1
  """This module contains the OpenAIMessageParamConverter class."""
2
2
 
3
3
  import json
4
+ from collections.abc import Iterable
4
5
  from typing import cast
5
6
 
6
7
  from openai.types.chat import ChatCompletionMessageParam
7
8
 
8
9
  from mirascope.core import BaseMessageParam
9
- from mirascope.core.base import TextPart, ToolCallPart, ToolResultPart
10
+ from mirascope.core.base import (
11
+ AudioPart,
12
+ ImageURLPart,
13
+ TextPart,
14
+ ToolCallPart,
15
+ ToolResultPart,
16
+ )
10
17
  from mirascope.core.base._utils._base_message_param_converter import (
11
18
  BaseMessageParamConverter,
12
19
  )
@@ -57,12 +64,29 @@ class OpenAIMessageParamConverter(BaseMessageParamConverter):
57
64
  BaseMessageParam(role=message_param["role"], content=content)
58
65
  )
59
66
  continue
60
- elif isinstance(content, list):
67
+ elif isinstance(content, Iterable):
61
68
  for part in content:
62
- if "text" in part:
69
+ if part["type"] == "text":
63
70
  contents.append(TextPart(type="text", text=part["text"]))
71
+ elif part["type"] == "image_url":
72
+ image_url = part["image_url"]
73
+ contents.append(
74
+ ImageURLPart(
75
+ type="image_url",
76
+ url=image_url["url"],
77
+ detail=image_url.get("detail", None),
78
+ )
79
+ )
80
+ elif part["type"] == "input_audio":
81
+ input_audio = part["input_audio"]
82
+ contents.append(
83
+ AudioPart(
84
+ type="audio",
85
+ media_type=f"audio/{input_audio['format']}",
86
+ audio=input_audio["data"],
87
+ )
88
+ )
64
89
  else:
65
- # TODO: add support for image and audio parts here
66
90
  raise ValueError(part["refusal"]) # pyright: ignore [reportGeneralTypeIssues]
67
91
  if tool_calls := message_param.get("tool_calls"):
68
92
  for tool_call in tool_calls:
@@ -1,5 +1,7 @@
1
1
  """The Mirascope Vertex Module."""
2
2
 
3
+ import inspect
4
+ import warnings
3
5
  from typing import TypeAlias
4
6
 
5
7
  from google.cloud.aiplatform_v1beta1.types import FunctionResponse
@@ -17,6 +19,19 @@ from .tool import VertexTool
17
19
 
18
20
  VertexMessageParam: TypeAlias = Content | 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
+ You can use Vertex AI by setting a custom `client` or environment variables.
28
+ See these docs for reference:
29
+ - Google AI SDK Custom Client: https://googleapis.github.io/python-genai/#create-a-client
30
+ - Mirascope Google Custom Client: https://mirascope.com/learn/calls/#__tabbed_39_5
31
+ """),
32
+ category=DeprecationWarning,
33
+ )
34
+
20
35
  __all__ = [
21
36
  "call",
22
37
  "VertexCallParams",
@@ -1,10 +1,16 @@
1
1
  """Utility for converting `BaseMessageParam` to `Content`"""
2
2
 
3
+ import base64
4
+ import io
5
+
6
+ import PIL.Image
3
7
  from google.cloud.aiplatform_v1beta1.types import content as gapic_content_types
4
8
  from google.cloud.aiplatform_v1beta1.types import tool as gapic_tool_types
5
9
  from vertexai.generative_models import Content, Image, Part
6
10
 
7
11
  from ...base import BaseMessageParam
12
+ from ...base._utils import get_audio_type
13
+ from ...base._utils._parse_content_template import _load_media
8
14
 
9
15
 
10
16
  def convert_message_params(
@@ -22,15 +28,11 @@ def convert_message_params(
22
28
  ) # pragma: no cover
23
29
  converted_message_params += [
24
30
  Content(
25
- role="user",
31
+ role="system",
26
32
  parts=[
27
33
  Part.from_text(content) if isinstance(content, str) else content
28
34
  ],
29
35
  ),
30
- Content(
31
- role="model",
32
- parts=[Part.from_text("Ok! I will adhere to this system message.")],
33
- ),
34
36
  ]
35
37
  elif isinstance((content := message_param.content), str):
36
38
  converted_message_params.append(
@@ -56,6 +58,29 @@ def convert_message_params(
56
58
  )
57
59
  image = Image.from_bytes(part.image)
58
60
  converted_content.append(Part.from_image(image))
61
+ elif part.type == "image_url":
62
+ # Should download the image to determine the media type
63
+ image = PIL.Image.open(io.BytesIO(_load_media(part.url)))
64
+ media_type = (
65
+ PIL.Image.MIME[image.format]
66
+ if image.format
67
+ else "image/unknown"
68
+ )
69
+ if media_type not in [
70
+ "image/jpeg",
71
+ "image/png",
72
+ "image/webp",
73
+ "image/heic",
74
+ "image/heif",
75
+ ]:
76
+ raise ValueError(
77
+ f"Unsupported image media type: {media_type}. "
78
+ "Gemini currently only supports JPEG, PNG, WebP, HEIC, "
79
+ "and HEIF images."
80
+ )
81
+ converted_content.append(
82
+ Part.from_uri(part.url, mime_type=media_type)
83
+ )
59
84
  elif part.type == "audio":
60
85
  if part.media_type not in [
61
86
  "audio/wav",
@@ -71,7 +96,32 @@ def convert_message_params(
71
96
  "and FLAC audio file types."
72
97
  )
73
98
  converted_content.append(
74
- Part.from_data(mime_type=part.media_type, data=part.audio)
99
+ Part.from_data(
100
+ mime_type=part.media_type,
101
+ data=part.audio
102
+ if isinstance(part.audio, bytes)
103
+ else base64.b64decode(part.audio),
104
+ )
105
+ )
106
+ elif part.type == "audio_url":
107
+ # Should download the audio to determine the media type
108
+ audio = _load_media(part.url)
109
+ audio_type = get_audio_type(audio)
110
+ if audio_type not in [
111
+ "audio/wav",
112
+ "audio/mp3",
113
+ "audio/aiff",
114
+ "audio/aac",
115
+ "audio/ogg",
116
+ "audio/flac",
117
+ ]:
118
+ raise ValueError(
119
+ f"Unsupported audio media type: {audio_type}. "
120
+ "Gemini currently only supports WAV, MP3, AIFF, AAC, OGG, "
121
+ "and FLAC audio file types."
122
+ )
123
+ converted_content.append(
124
+ Part.from_uri(part.url, mime_type=audio_type)
75
125
  )
76
126
  elif part.type == "tool_call":
77
127
  if converted_content:
@@ -7,7 +7,7 @@ from mirascope.core.base import DocumentPart, ImagePart, TextPart
7
7
  from mirascope.core.base._utils._base_message_param_converter import (
8
8
  BaseMessageParamConverter,
9
9
  )
10
- from mirascope.core.base.message_param import ToolCallPart, ToolResultPart
10
+ from mirascope.core.base.message_param import ImageURLPart, ToolCallPart, ToolResultPart
11
11
  from mirascope.core.vertex._utils import convert_message_params
12
12
 
13
13
 
@@ -87,10 +87,18 @@ class VertexMessageParamConverter(BaseMessageParamConverter):
87
87
  )
88
88
 
89
89
  elif part.file_data:
90
- # part.file_data.file_uri has Google storage URI like "gs://bucket_name/file_name"
91
- raise ValueError(
92
- f"FileData.file_uri is not support: {part.file_data}. Cannot convert to BaseMessageParam."
93
- )
90
+ if _is_image_mime(part.file_data.mime_type):
91
+ contents.append(
92
+ ImageURLPart(
93
+ type="image_url",
94
+ url=part.file_data.file_uri,
95
+ detail=None,
96
+ )
97
+ )
98
+ else:
99
+ raise ValueError(
100
+ f"FileData.file_uri is not support: {part.file_data}. Cannot convert to BaseMessageParam."
101
+ )
94
102
  elif part.function_call:
95
103
  converted.append(
96
104
  BaseMessageParam(