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.
- mirascope/__init__.py +20 -1
- mirascope/core/__init__.py +4 -0
- mirascope/core/anthropic/_utils/_convert_message_params.py +13 -0
- mirascope/core/azure/_utils/_convert_message_params.py +10 -0
- mirascope/core/azure/_utils/_message_param_converter.py +46 -12
- mirascope/core/base/__init__.py +4 -0
- mirascope/core/base/_utils/_convert_messages_to_message_params.py +36 -3
- mirascope/core/base/_utils/_parse_content_template.py +35 -9
- mirascope/core/base/message_param.py +30 -2
- mirascope/core/base/messages.py +10 -0
- mirascope/core/base/stream_config.py +1 -1
- mirascope/core/bedrock/_utils/_convert_message_params.py +18 -1
- mirascope/core/gemini/__init__.py +10 -0
- mirascope/core/gemini/_utils/_convert_message_params.py +48 -5
- mirascope/core/gemini/_utils/_message_param_converter.py +51 -5
- mirascope/core/gemini/_utils/_setup_call.py +12 -2
- mirascope/core/google/__init__.py +29 -0
- mirascope/core/google/_call.py +67 -0
- mirascope/core/google/_call_kwargs.py +13 -0
- mirascope/core/google/_utils/__init__.py +16 -0
- mirascope/core/google/_utils/_calculate_cost.py +88 -0
- mirascope/core/google/_utils/_convert_common_call_params.py +39 -0
- mirascope/core/google/_utils/_convert_finish_reason_to_common_finish_reasons.py +27 -0
- mirascope/core/google/_utils/_convert_message_params.py +177 -0
- mirascope/core/google/_utils/_get_json_output.py +37 -0
- mirascope/core/google/_utils/_handle_stream.py +35 -0
- mirascope/core/google/_utils/_message_param_converter.py +153 -0
- mirascope/core/google/_utils/_setup_call.py +180 -0
- mirascope/core/google/call_params.py +22 -0
- mirascope/core/google/call_response.py +202 -0
- mirascope/core/google/call_response_chunk.py +97 -0
- mirascope/core/google/dynamic_config.py +26 -0
- mirascope/core/google/stream.py +128 -0
- mirascope/core/google/tool.py +104 -0
- mirascope/core/groq/_utils/_convert_message_params.py +9 -0
- mirascope/core/groq/_utils/_message_param_converter.py +9 -2
- mirascope/core/mistral/_utils/_convert_message_params.py +7 -0
- mirascope/core/mistral/_utils/_message_param_converter.py +41 -35
- mirascope/core/openai/_utils/_convert_message_params.py +38 -1
- mirascope/core/openai/_utils/_message_param_converter.py +28 -4
- mirascope/core/vertex/__init__.py +15 -0
- mirascope/core/vertex/_utils/_convert_message_params.py +56 -6
- mirascope/core/vertex/_utils/_message_param_converter.py +13 -5
- mirascope/core/vertex/_utils/_setup_call.py +10 -1
- mirascope/llm/_protocols.py +1 -0
- mirascope/llm/call_response.py +5 -1
- mirascope/llm/llm_call.py +4 -0
- mirascope/llm/llm_override.py +16 -3
- mirascope/retries/__init__.py +5 -0
- mirascope/retries/fallback.py +128 -0
- {mirascope-1.16.9.dist-info → mirascope-1.18.0.dist-info}/METADATA +4 -1
- {mirascope-1.16.9.dist-info → mirascope-1.18.0.dist-info}/RECORD +54 -35
- {mirascope-1.16.9.dist-info → mirascope-1.18.0.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
-
|
|
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."
|
|
@@ -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
|
}
|
|
@@ -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
|
|
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,
|
|
67
|
+
elif isinstance(content, Iterable):
|
|
61
68
|
for part in content:
|
|
62
|
-
if "
|
|
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="
|
|
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(
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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(
|