mirascope 1.16.8__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.
Files changed (43) hide show
  1. mirascope/__init__.py +20 -1
  2. mirascope/core/anthropic/_utils/_convert_message_params.py +13 -0
  3. mirascope/core/anthropic/call_response.py +10 -2
  4. mirascope/core/azure/_utils/_convert_message_params.py +10 -0
  5. mirascope/core/azure/_utils/_message_param_converter.py +80 -19
  6. mirascope/core/azure/call_response.py +8 -2
  7. mirascope/core/base/__init__.py +4 -0
  8. mirascope/core/base/_create.py +1 -1
  9. mirascope/core/base/_utils/_convert_function_to_base_tool.py +2 -2
  10. mirascope/core/base/_utils/_convert_messages_to_message_params.py +36 -3
  11. mirascope/core/base/_utils/_parse_content_template.py +35 -9
  12. mirascope/core/base/call_response.py +7 -1
  13. mirascope/core/base/message_param.py +30 -2
  14. mirascope/core/base/messages.py +10 -0
  15. mirascope/core/bedrock/_utils/_convert_message_params.py +18 -1
  16. mirascope/core/bedrock/call_response.py +8 -2
  17. mirascope/core/cohere/_utils/_message_param_converter.py +3 -2
  18. mirascope/core/cohere/call_response.py +8 -2
  19. mirascope/core/gemini/_utils/_convert_message_params.py +48 -5
  20. mirascope/core/gemini/_utils/_message_param_converter.py +56 -6
  21. mirascope/core/gemini/_utils/_setup_call.py +12 -2
  22. mirascope/core/gemini/call_response.py +8 -2
  23. mirascope/core/groq/_utils/_convert_message_params.py +9 -0
  24. mirascope/core/groq/_utils/_message_param_converter.py +44 -15
  25. mirascope/core/groq/call_response.py +8 -2
  26. mirascope/core/mistral/_utils/_convert_message_params.py +7 -0
  27. mirascope/core/mistral/_utils/_message_param_converter.py +41 -35
  28. mirascope/core/mistral/call_response.py +8 -2
  29. mirascope/core/openai/_utils/_convert_message_params.py +39 -1
  30. mirascope/core/openai/_utils/_message_param_converter.py +36 -10
  31. mirascope/core/openai/call_response.py +8 -2
  32. mirascope/core/vertex/_utils/_convert_message_params.py +56 -6
  33. mirascope/core/vertex/_utils/_message_param_converter.py +17 -7
  34. mirascope/core/vertex/_utils/_setup_call.py +10 -1
  35. mirascope/core/vertex/call_response.py +8 -2
  36. mirascope/llm/call_response.py +11 -3
  37. mirascope/llm/stream.py +3 -3
  38. mirascope/retries/__init__.py +5 -0
  39. mirascope/retries/fallback.py +128 -0
  40. {mirascope-1.16.8.dist-info → mirascope-1.17.0.dist-info}/METADATA +1 -1
  41. {mirascope-1.16.8.dist-info → mirascope-1.17.0.dist-info}/RECORD +43 -42
  42. {mirascope-1.16.8.dist-info → mirascope-1.17.0.dist-info}/WHEEL +0 -0
  43. {mirascope-1.16.8.dist-info → mirascope-1.17.0.dist-info}/licenses/LICENSE +0 -0
@@ -188,5 +188,11 @@ class CohereCallResponse(
188
188
  return _convert_finish_reasons_to_common_finish_reasons(self.finish_reasons)
189
189
 
190
190
  @property
191
- def common_message_param(self) -> list[BaseMessageParam]:
192
- return CohereMessageParamConverter.from_provider([self.message_param])
191
+ def common_message_param(self) -> BaseMessageParam:
192
+ return CohereMessageParamConverter.from_provider([self.message_param])[0]
193
+
194
+ @property
195
+ def common_user_message_param(self) -> BaseMessageParam | None:
196
+ if not self.user_message_param:
197
+ return None
198
+ return CohereMessageParamConverter.from_provider([self.user_message_param])[0]
@@ -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(
@@ -8,7 +8,14 @@ from google.generativeai.types import (
8
8
  )
9
9
 
10
10
  from mirascope.core import BaseMessageParam
11
- from mirascope.core.base import DocumentPart, ImagePart, TextPart
11
+ from mirascope.core.base import (
12
+ AudioPart,
13
+ AudioURLPart,
14
+ DocumentPart,
15
+ ImagePart,
16
+ ImageURLPart,
17
+ TextPart,
18
+ )
12
19
  from mirascope.core.base._utils._base_message_param_converter import (
13
20
  BaseMessageParamConverter,
14
21
  )
@@ -29,6 +36,28 @@ def _to_image_part(mime_type: str, data: bytes) -> ImagePart:
29
36
  return ImagePart(type="image", media_type=mime_type, image=data, detail=None)
30
37
 
31
38
 
39
+ def _is_audio_mime(mime_type: str) -> bool:
40
+ return mime_type in [
41
+ "audio/wav",
42
+ "audio/mp3",
43
+ "audio/wav",
44
+ "audio/mp3",
45
+ "audio/aiff",
46
+ "audio/aac",
47
+ "audio/ogg",
48
+ "audio/flac",
49
+ ]
50
+
51
+
52
+ def _to_audio_part(mime_type: str, data: bytes) -> AudioPart:
53
+ if not _is_audio_mime(mime_type):
54
+ raise ValueError(
55
+ f"Unsupported audio media type: {mime_type}. "
56
+ "Expected one of: audio/wav, audio/mp3, audio/aiff, audio/aac, audio/ogg, audio/flac."
57
+ )
58
+ return AudioPart(type="audio", media_type=mime_type, audio=data)
59
+
60
+
32
61
  def _to_document_part(mime_type: str, data: bytes) -> DocumentPart:
33
62
  if mime_type != "application/pdf":
34
63
  raise ValueError(
@@ -57,7 +86,11 @@ class GeminiMessageParamConverter(BaseMessageParamConverter):
57
86
  """
58
87
  converted: list[BaseMessageParam] = []
59
88
  for message_param in message_params:
60
- role: str = "assistant"
89
+ role: str = (
90
+ "assistant"
91
+ if message_param["role"] == "model"
92
+ else message_param["role"]
93
+ )
61
94
  content_list = []
62
95
  for part in cast(
63
96
  list[protos.Part | protos.FunctionCall | protos.FunctionResponse],
@@ -73,6 +106,8 @@ class GeminiMessageParamConverter(BaseMessageParamConverter):
73
106
  data = blob.data
74
107
  if _is_image_mime(mime):
75
108
  content_list.append(_to_image_part(mime, data))
109
+ elif _is_audio_mime(mime):
110
+ content_list.append(_to_audio_part(mime, data))
76
111
  elif mime == "application/pdf":
77
112
  content_list.append(_to_document_part(mime, data))
78
113
  else:
@@ -81,10 +116,25 @@ class GeminiMessageParamConverter(BaseMessageParamConverter):
81
116
  )
82
117
 
83
118
  elif part.file_data:
84
- # part.file_data.file_uri has Google storage URI like "gs://bucket_name/file_name"
85
- raise ValueError(
86
- f"FileData.file_uri is not support: {part.file_data}. Cannot convert to BaseMessageParam."
87
- )
119
+ if _is_image_mime(part.file_data.mime_type):
120
+ content_list.append(
121
+ ImageURLPart(
122
+ type="image_url",
123
+ url=part.file_data.file_uri,
124
+ detail=None,
125
+ )
126
+ )
127
+ elif _is_audio_mime(part.file_data.mime_type):
128
+ content_list.append(
129
+ AudioURLPart(
130
+ type="audio_url",
131
+ url=part.file_data.file_uri,
132
+ )
133
+ )
134
+ else:
135
+ raise ValueError(
136
+ f"Unsupported file_data mime type: {part.file_data.mime_type}. Cannot convert to BaseMessageParam."
137
+ )
88
138
  elif part.function_call:
89
139
  converted.append(
90
140
  BaseMessageParam(
@@ -11,7 +11,7 @@ from google.generativeai.types import (
11
11
  GenerateContentResponse,
12
12
  GenerationConfigDict,
13
13
  )
14
- from google.generativeai.types.content_types import ToolConfigDict
14
+ from google.generativeai.types.content_types import ToolConfigDict, to_content
15
15
  from pydantic import BaseModel
16
16
 
17
17
  from ...base import BaseMessageParam, BaseTool, _utils
@@ -108,6 +108,7 @@ def setup_call(
108
108
  call_kwargs = cast(GeminiCallKwargs, base_call_kwargs)
109
109
  messages = cast(list[BaseMessageParam | ContentDict], messages)
110
110
  messages = convert_message_params(messages)
111
+
111
112
  if json_mode:
112
113
  generation_config = call_kwargs.get("generation_config", {})
113
114
  if is_dataclass(generation_config):
@@ -125,11 +126,20 @@ def setup_call(
125
126
  "allowed_function_names": [tool_types[0]._name()],
126
127
  }
127
128
  call_kwargs["tool_config"] = tool_config
128
- call_kwargs |= {"contents": messages}
129
129
 
130
130
  if client is None:
131
131
  client = GenerativeModel(model_name=model)
132
132
 
133
+ if messages and messages[0]["role"] == "system":
134
+ system_instruction = client._system_instruction
135
+ system_instruction = (
136
+ list(system_instruction.parts) if system_instruction else []
137
+ )
138
+ system_instruction.extend(messages.pop(0)["parts"]) # pyright: ignore [reportArgumentType]
139
+ client._system_instruction = to_content(system_instruction) # pyright: ignore [reportArgumentType]
140
+
141
+ call_kwargs |= {"contents": messages}
142
+
133
143
  create = (
134
144
  get_async_create_fn(client.generate_content_async)
135
145
  if fn_is_async(fn)
@@ -199,5 +199,11 @@ class GeminiCallResponse(
199
199
  return _convert_finish_reasons_to_common_finish_reasons(self.finish_reasons)
200
200
 
201
201
  @property
202
- def common_message_param(self) -> list[BaseMessageParam]:
203
- return GeminiMessageParamConverter.from_provider([self.message_param])
202
+ def common_message_param(self) -> BaseMessageParam:
203
+ return GeminiMessageParamConverter.from_provider([self.message_param])[0]
204
+
205
+ @property
206
+ def common_user_message_param(self) -> BaseMessageParam | None:
207
+ if not self.user_message_param:
208
+ return None
209
+ return GeminiMessageParamConverter.from_provider([self.user_message_param])[0]
@@ -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",
@@ -1,17 +1,14 @@
1
1
  import json
2
2
  from typing import cast
3
3
 
4
- from groq.types.chat import (
5
- ChatCompletionAssistantMessageParam,
6
- ChatCompletionMessageParam,
7
- )
4
+ from groq.types.chat import ChatCompletionMessageParam
8
5
 
9
6
  from mirascope.core import BaseMessageParam
10
- from mirascope.core.base import TextPart
7
+ from mirascope.core.base import TextPart, ToolResultPart
11
8
  from mirascope.core.base._utils._base_message_param_converter import (
12
9
  BaseMessageParamConverter,
13
10
  )
14
- from mirascope.core.base.message_param import ToolCallPart
11
+ from mirascope.core.base.message_param import ImageURLPart, ToolCallPart
15
12
  from mirascope.core.groq._utils import convert_message_params
16
13
 
17
14
 
@@ -31,16 +28,29 @@ class GroqMessageParamConverter(BaseMessageParamConverter):
31
28
 
32
29
  @staticmethod
33
30
  def from_provider(
34
- message_params: list[ChatCompletionAssistantMessageParam],
31
+ message_params: list[ChatCompletionMessageParam],
35
32
  ) -> list[BaseMessageParam]:
36
- """
37
- Convert from Groq's `ChatCompletionAssistantMessageParam` to Mirascope `BaseMessageParam`.
38
- """
33
+ """Convert from Groq's `ChatCompletionAssistantMessageParam` to Mirascope `BaseMessageParam`."""
39
34
  converted = []
40
35
  for message_param in message_params:
41
36
  contents = []
42
- if (content := message_param.get("content")) and content is not None:
43
- contents.append(TextPart(text=content, type="text"))
37
+ content = message_param.get("content")
38
+ if message_param["role"] == "tool":
39
+ converted.append(
40
+ BaseMessageParam(
41
+ role="tool",
42
+ content=[
43
+ ToolResultPart(
44
+ type="tool_result",
45
+ name=getattr(message_param, "name", ""),
46
+ content=message_param["content"],
47
+ id=message_param["tool_call_id"],
48
+ is_error=False,
49
+ )
50
+ ],
51
+ )
52
+ )
53
+ continue
44
54
  if tool_calls := message_param.get("tool_calls"):
45
55
  for tool_call in tool_calls:
46
56
  contents.append(
@@ -51,10 +61,29 @@ class GroqMessageParamConverter(BaseMessageParamConverter):
51
61
  args=json.loads(tool_call["function"]["arguments"]),
52
62
  )
53
63
  )
54
- if len(contents) == 1 and isinstance(contents[0], TextPart):
64
+ elif isinstance(content, str):
55
65
  converted.append(
56
- BaseMessageParam(role="assistant", content=contents[0].text)
66
+ BaseMessageParam(role=message_param["role"], content=content)
67
+ )
68
+ continue
69
+ elif isinstance(content, list):
70
+ for part in content:
71
+ if "text" in part:
72
+ contents.append(TextPart(type="text", text=part["text"]))
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
+ )
81
+ if contents:
82
+ converted.append(
83
+ BaseMessageParam(role=message_param["role"], content=contents)
57
84
  )
58
85
  else:
59
- converted.append(BaseMessageParam(role="tool", content=contents))
86
+ converted.append(
87
+ BaseMessageParam(role=message_param["role"], content="")
88
+ )
60
89
  return converted
@@ -181,5 +181,11 @@ class GroqCallResponse(
181
181
  return cast(list[FinishReason], self.finish_reasons)
182
182
 
183
183
  @property
184
- def common_message_param(self) -> list[BaseMessageParam]:
185
- return GroqMessageParamConverter.from_provider([self.message_param])
184
+ def common_message_param(self) -> BaseMessageParam:
185
+ return GroqMessageParamConverter.from_provider([self.message_param])[0]
186
+
187
+ @property
188
+ def common_user_message_param(self) -> BaseMessageParam | None:
189
+ if not self.user_message_param:
190
+ return None
191
+ return GroqMessageParamConverter.from_provider([self.user_message_param])[0]
@@ -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."
@@ -186,5 +186,11 @@ class MistralCallResponse(
186
186
  return _convert_finish_reasons_to_common_finish_reasons(self.finish_reasons)
187
187
 
188
188
  @property
189
- def common_message_param(self) -> list[BaseMessageParam]:
190
- return MistralMessageParamConverter.from_provider([self.message_param])
189
+ def common_message_param(self) -> BaseMessageParam:
190
+ return MistralMessageParamConverter.from_provider([self.message_param])[0]
191
+
192
+ @property
193
+ def common_user_message_param(self) -> BaseMessageParam | None:
194
+ if not self.user_message_param:
195
+ return None
196
+ return MistralMessageParamConverter.from_provider([self.user_message_param])[0]
@@ -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
  }
@@ -105,4 +142,5 @@ def convert_message_params(
105
142
  converted_message_params.append(
106
143
  {"role": message_param.role, "content": converted_content}
107
144
  )
145
+ # print(converted_message_params)
108
146
  return converted_message_params
@@ -1,15 +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
- from openai.types.chat import (
7
- ChatCompletionAssistantMessageParam,
8
- ChatCompletionMessageParam,
9
- )
7
+ from openai.types.chat import ChatCompletionMessageParam
10
8
 
11
9
  from mirascope.core import BaseMessageParam
12
- 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
+ )
13
17
  from mirascope.core.base._utils._base_message_param_converter import (
14
18
  BaseMessageParamConverter,
15
19
  )
@@ -28,14 +32,18 @@ class OpenAIMessageParamConverter(BaseMessageParamConverter):
28
32
 
29
33
  @staticmethod
30
34
  def from_provider(
31
- message_params: list[ChatCompletionAssistantMessageParam],
35
+ message_params: list[ChatCompletionMessageParam],
32
36
  ) -> list[BaseMessageParam]:
33
37
  """Converts OpenAI message params to base message params."""
34
38
  converted = []
35
39
  for message_param in message_params:
36
40
  contents = []
37
41
  content = message_param.get("content")
38
- if message_param["role"] == "tool":
42
+ if (
43
+ message_param["role"] == "tool"
44
+ and "name" in message_param
45
+ and "content" in message_param
46
+ ):
39
47
  converted.append(
40
48
  BaseMessageParam(
41
49
  role="tool",
@@ -56,12 +64,30 @@ class OpenAIMessageParamConverter(BaseMessageParamConverter):
56
64
  BaseMessageParam(role=message_param["role"], content=content)
57
65
  )
58
66
  continue
59
- elif isinstance(content, list):
67
+ elif isinstance(content, Iterable):
60
68
  for part in content:
61
- if "text" in part:
69
+ if part["type"] == "text":
62
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
+ )
63
89
  else:
64
- raise ValueError(part["refusal"])
90
+ raise ValueError(part["refusal"]) # pyright: ignore [reportGeneralTypeIssues]
65
91
  if tool_calls := message_param.get("tool_calls"):
66
92
  for tool_call in tool_calls:
67
93
  contents.append(
@@ -229,5 +229,11 @@ class OpenAICallResponse(
229
229
  return cast(list[FinishReason], self.finish_reasons)
230
230
 
231
231
  @property
232
- def common_message_param(self) -> list[BaseMessageParam]:
233
- return OpenAIMessageParamConverter.from_provider([self.message_param])
232
+ def common_message_param(self) -> BaseMessageParam:
233
+ return OpenAIMessageParamConverter.from_provider([self.message_param])[0]
234
+
235
+ @property
236
+ def common_user_message_param(self) -> BaseMessageParam | None:
237
+ if not self.user_message_param:
238
+ return None
239
+ return OpenAIMessageParamConverter.from_provider([self.user_message_param])[0]