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.
- mirascope/__init__.py +20 -1
- mirascope/core/anthropic/_utils/_convert_message_params.py +13 -0
- mirascope/core/anthropic/call_response.py +10 -2
- mirascope/core/azure/_utils/_convert_message_params.py +10 -0
- mirascope/core/azure/_utils/_message_param_converter.py +80 -19
- mirascope/core/azure/call_response.py +8 -2
- mirascope/core/base/__init__.py +4 -0
- mirascope/core/base/_create.py +1 -1
- mirascope/core/base/_utils/_convert_function_to_base_tool.py +2 -2
- 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/call_response.py +7 -1
- 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/bedrock/call_response.py +8 -2
- mirascope/core/cohere/_utils/_message_param_converter.py +3 -2
- mirascope/core/cohere/call_response.py +8 -2
- mirascope/core/gemini/_utils/_convert_message_params.py +48 -5
- mirascope/core/gemini/_utils/_message_param_converter.py +56 -6
- mirascope/core/gemini/_utils/_setup_call.py +12 -2
- mirascope/core/gemini/call_response.py +8 -2
- mirascope/core/groq/_utils/_convert_message_params.py +9 -0
- mirascope/core/groq/_utils/_message_param_converter.py +44 -15
- mirascope/core/groq/call_response.py +8 -2
- mirascope/core/mistral/_utils/_convert_message_params.py +7 -0
- mirascope/core/mistral/_utils/_message_param_converter.py +41 -35
- mirascope/core/mistral/call_response.py +8 -2
- mirascope/core/openai/_utils/_convert_message_params.py +39 -1
- mirascope/core/openai/_utils/_message_param_converter.py +36 -10
- mirascope/core/openai/call_response.py +8 -2
- mirascope/core/vertex/_utils/_convert_message_params.py +56 -6
- mirascope/core/vertex/_utils/_message_param_converter.py +17 -7
- mirascope/core/vertex/_utils/_setup_call.py +10 -1
- mirascope/core/vertex/call_response.py +8 -2
- mirascope/llm/call_response.py +11 -3
- mirascope/llm/stream.py +3 -3
- mirascope/retries/__init__.py +5 -0
- mirascope/retries/fallback.py +128 -0
- {mirascope-1.16.8.dist-info → mirascope-1.17.0.dist-info}/METADATA +1 -1
- {mirascope-1.16.8.dist-info → mirascope-1.17.0.dist-info}/RECORD +43 -42
- {mirascope-1.16.8.dist-info → mirascope-1.17.0.dist-info}/WHEEL +0 -0
- {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) ->
|
|
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": "
|
|
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
|
|
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 =
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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) ->
|
|
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[
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
64
|
+
elif isinstance(content, str):
|
|
55
65
|
converted.append(
|
|
56
|
-
BaseMessageParam(role="
|
|
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(
|
|
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) ->
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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) ->
|
|
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":
|
|
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
|
|
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[
|
|
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
|
|
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,
|
|
67
|
+
elif isinstance(content, Iterable):
|
|
60
68
|
for part in content:
|
|
61
|
-
if "
|
|
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) ->
|
|
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]
|