mirascope 1.17.0__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/core/__init__.py +4 -0
- mirascope/core/base/stream_config.py +1 -1
- mirascope/core/gemini/__init__.py +10 -0
- 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/vertex/__init__.py +15 -0
- mirascope/llm/_protocols.py +1 -0
- mirascope/llm/llm_call.py +4 -0
- mirascope/llm/llm_override.py +16 -3
- {mirascope-1.17.0.dist-info → mirascope-1.18.0.dist-info}/METADATA +4 -1
- {mirascope-1.17.0.dist-info → mirascope-1.18.0.dist-info}/RECORD +29 -11
- {mirascope-1.17.0.dist-info → mirascope-1.18.0.dist-info}/WHEEL +0 -0
- {mirascope-1.17.0.dist-info → mirascope-1.18.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
3
|
+
from google.genai import Client
|
|
4
|
+
from google.genai.types import (
|
|
5
|
+
Content,
|
|
6
|
+
ContentDict,
|
|
7
|
+
ContentOrDict,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from mirascope.core import BaseMessageParam
|
|
11
|
+
from mirascope.core.base import DocumentPart, ImagePart, TextPart
|
|
12
|
+
from mirascope.core.base._utils._base_message_param_converter import (
|
|
13
|
+
BaseMessageParamConverter,
|
|
14
|
+
)
|
|
15
|
+
from mirascope.core.base.message_param import (
|
|
16
|
+
AudioURLPart,
|
|
17
|
+
ImageURLPart,
|
|
18
|
+
ToolCallPart,
|
|
19
|
+
ToolResultPart,
|
|
20
|
+
)
|
|
21
|
+
from mirascope.core.gemini._utils._message_param_converter import _is_audio_mime
|
|
22
|
+
from mirascope.core.google._utils import convert_message_params
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_image_mime(mime_type: str) -> bool:
|
|
26
|
+
return mime_type in ["image/jpeg", "image/png", "image/gif", "image/webp"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _to_image_part(mime_type: str, data: bytes) -> ImagePart:
|
|
30
|
+
if not _is_image_mime(mime_type):
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"Unsupported image media type: {mime_type}. "
|
|
33
|
+
"Expected one of: image/jpeg, image/png, image/gif, image/webp."
|
|
34
|
+
)
|
|
35
|
+
return ImagePart(type="image", media_type=mime_type, image=data, detail=None)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _to_document_part(mime_type: str, data: bytes) -> DocumentPart:
|
|
39
|
+
if mime_type != "application/pdf":
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"Unsupported document media type: {mime_type}. "
|
|
42
|
+
"Only application/pdf is supported."
|
|
43
|
+
)
|
|
44
|
+
return DocumentPart(type="document", media_type=mime_type, document=data)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GoogleMessageParamConverter(BaseMessageParamConverter):
|
|
48
|
+
"""Converts between Google `ContentDict` and Mirascope `BaseMessageParam`."""
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def to_provider(
|
|
52
|
+
message_params: list[BaseMessageParam], client: Client | None = None
|
|
53
|
+
) -> list[ContentDict]:
|
|
54
|
+
"""
|
|
55
|
+
Convert from Mirascope `BaseMessageParam` to Google `ContentDict`.
|
|
56
|
+
"""
|
|
57
|
+
return convert_message_params(
|
|
58
|
+
cast(list[BaseMessageParam | ContentDict], message_params),
|
|
59
|
+
client or Client(),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def from_provider(message_params: list[ContentOrDict]) -> list[BaseMessageParam]:
|
|
64
|
+
"""
|
|
65
|
+
Convert from Google's `ContentDict` to Mirascope `BaseMessageParam`.
|
|
66
|
+
"""
|
|
67
|
+
converted: list[BaseMessageParam] = []
|
|
68
|
+
for message_param in message_params:
|
|
69
|
+
if isinstance(message_param, dict):
|
|
70
|
+
message_param = Content.model_validate(message_param)
|
|
71
|
+
role: str = "assistant"
|
|
72
|
+
content_list = []
|
|
73
|
+
for part in message_param.parts or []:
|
|
74
|
+
if part.text:
|
|
75
|
+
content_list.append(TextPart(type="text", text=part.text))
|
|
76
|
+
|
|
77
|
+
elif part.inline_data:
|
|
78
|
+
blob = part.inline_data
|
|
79
|
+
mime = blob.mime_type or ""
|
|
80
|
+
data = blob.data or b""
|
|
81
|
+
if _is_image_mime(mime or ""):
|
|
82
|
+
content_list.append(_to_image_part(mime, data))
|
|
83
|
+
elif mime == "application/pdf":
|
|
84
|
+
content_list.append(_to_document_part(mime, data))
|
|
85
|
+
else:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Unsupported inline_data mime type: {mime}. Cannot convert to BaseMessageParam."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
elif part.file_data:
|
|
91
|
+
if _is_image_mime(cast(str, part.file_data.mime_type)):
|
|
92
|
+
content_list.append(
|
|
93
|
+
ImageURLPart(
|
|
94
|
+
type="image_url",
|
|
95
|
+
url=cast(str, part.file_data.file_uri),
|
|
96
|
+
detail=None,
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
elif _is_audio_mime(cast(str, part.file_data.mime_type)):
|
|
100
|
+
content_list.append(
|
|
101
|
+
AudioURLPart(
|
|
102
|
+
type="audio_url",
|
|
103
|
+
url=cast(str, part.file_data.file_uri),
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
else:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"Unsupported file_data mime type: {part.file_data.mime_type}. Cannot convert to BaseMessageParam."
|
|
109
|
+
)
|
|
110
|
+
elif part.function_call:
|
|
111
|
+
converted.append(
|
|
112
|
+
BaseMessageParam(
|
|
113
|
+
role=role,
|
|
114
|
+
content=[
|
|
115
|
+
ToolCallPart(
|
|
116
|
+
type="tool_call",
|
|
117
|
+
name=part.function_call.name, # pyright: ignore [reportArgumentType]
|
|
118
|
+
args=part.function_call.args,
|
|
119
|
+
)
|
|
120
|
+
],
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
elif part.function_response:
|
|
124
|
+
converted.append(
|
|
125
|
+
BaseMessageParam(
|
|
126
|
+
role=role,
|
|
127
|
+
content=[
|
|
128
|
+
ToolResultPart(
|
|
129
|
+
type="tool_result",
|
|
130
|
+
name=part.function_response.name, # pyright: ignore [reportArgumentType]
|
|
131
|
+
content=part.function_response.response[ # pyright: ignore [reportArgumentType, reportOptionalSubscript]
|
|
132
|
+
"result"
|
|
133
|
+
],
|
|
134
|
+
id=None,
|
|
135
|
+
is_error=False,
|
|
136
|
+
)
|
|
137
|
+
],
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
"Part does not contain any supported content (text, image, or document)."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if len(content_list) == 1 and isinstance(content_list[0], TextPart):
|
|
146
|
+
converted.append(
|
|
147
|
+
BaseMessageParam(role=role, content=content_list[0].text)
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
if content_list:
|
|
151
|
+
converted.append(BaseMessageParam(role=role, content=content_list))
|
|
152
|
+
|
|
153
|
+
return converted
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""This module contains the setup_call function, which is used to set up the"""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
from collections.abc import Awaitable, Callable, Generator
|
|
5
|
+
from typing import Any, cast, overload
|
|
6
|
+
|
|
7
|
+
from google.genai import Client
|
|
8
|
+
from google.genai.types import (
|
|
9
|
+
ContentDict,
|
|
10
|
+
FunctionCallingConfig,
|
|
11
|
+
FunctionCallingConfigMode,
|
|
12
|
+
GenerateContentConfig,
|
|
13
|
+
GenerateContentConfigDict,
|
|
14
|
+
GenerateContentResponse,
|
|
15
|
+
Part,
|
|
16
|
+
PartDict,
|
|
17
|
+
ToolConfig,
|
|
18
|
+
ToolListUnion,
|
|
19
|
+
)
|
|
20
|
+
from pydantic import BaseModel
|
|
21
|
+
|
|
22
|
+
from ...base import BaseMessageParam, BaseTool, _utils
|
|
23
|
+
from ...base._utils import (
|
|
24
|
+
AsyncCreateFn,
|
|
25
|
+
CreateFn,
|
|
26
|
+
fn_is_async,
|
|
27
|
+
get_async_create_fn,
|
|
28
|
+
get_create_fn,
|
|
29
|
+
)
|
|
30
|
+
from ...base.call_params import CommonCallParams
|
|
31
|
+
from ...base.stream_config import StreamConfig
|
|
32
|
+
from .._call_kwargs import GoogleCallKwargs
|
|
33
|
+
from ..call_params import GoogleCallParams
|
|
34
|
+
from ..dynamic_config import GoogleDynamicConfig
|
|
35
|
+
from ..tool import GoogleTool
|
|
36
|
+
from ._convert_common_call_params import convert_common_call_params
|
|
37
|
+
from ._convert_message_params import convert_message_params
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _get_generate_content_config(
|
|
41
|
+
config: GenerateContentConfig | GenerateContentConfigDict,
|
|
42
|
+
) -> GenerateContentConfig:
|
|
43
|
+
if isinstance(config, dict):
|
|
44
|
+
return GenerateContentConfig.model_validate(config)
|
|
45
|
+
return config
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@contextlib.contextmanager
|
|
49
|
+
def _generate_content_config_context(
|
|
50
|
+
call_kwargs: GoogleCallKwargs,
|
|
51
|
+
) -> Generator[GenerateContentConfig, None, None]:
|
|
52
|
+
config = call_kwargs.get("config", {})
|
|
53
|
+
config = _get_generate_content_config(config)
|
|
54
|
+
yield config
|
|
55
|
+
call_kwargs["config"] = config
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@overload
|
|
59
|
+
def setup_call(
|
|
60
|
+
*,
|
|
61
|
+
model: str,
|
|
62
|
+
client: Client | None,
|
|
63
|
+
fn: Callable[..., Awaitable[GoogleDynamicConfig]],
|
|
64
|
+
fn_args: dict[str, Any],
|
|
65
|
+
dynamic_config: GoogleDynamicConfig,
|
|
66
|
+
tools: list[type[BaseTool] | Callable] | None,
|
|
67
|
+
json_mode: bool,
|
|
68
|
+
call_params: GoogleCallParams | CommonCallParams,
|
|
69
|
+
response_model: type[BaseModel] | None,
|
|
70
|
+
stream: bool | StreamConfig,
|
|
71
|
+
) -> tuple[
|
|
72
|
+
AsyncCreateFn[GenerateContentResponse, GenerateContentResponse],
|
|
73
|
+
str | None,
|
|
74
|
+
list[ContentDict],
|
|
75
|
+
list[type[GoogleTool]] | None,
|
|
76
|
+
GoogleCallKwargs,
|
|
77
|
+
]: ...
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@overload
|
|
81
|
+
def setup_call(
|
|
82
|
+
*,
|
|
83
|
+
model: str,
|
|
84
|
+
client: Client | None,
|
|
85
|
+
fn: Callable[..., GoogleDynamicConfig],
|
|
86
|
+
fn_args: dict[str, Any],
|
|
87
|
+
dynamic_config: GoogleDynamicConfig,
|
|
88
|
+
tools: list[type[BaseTool] | Callable] | None,
|
|
89
|
+
json_mode: bool,
|
|
90
|
+
call_params: GoogleCallParams | CommonCallParams,
|
|
91
|
+
response_model: type[BaseModel] | None,
|
|
92
|
+
stream: bool | StreamConfig,
|
|
93
|
+
) -> tuple[
|
|
94
|
+
CreateFn[GenerateContentResponse, GenerateContentResponse],
|
|
95
|
+
str | None,
|
|
96
|
+
list[ContentDict],
|
|
97
|
+
list[type[GoogleTool]] | None,
|
|
98
|
+
GoogleCallKwargs,
|
|
99
|
+
]: ...
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def setup_call(
|
|
103
|
+
*,
|
|
104
|
+
model: str,
|
|
105
|
+
client: Client | None,
|
|
106
|
+
fn: Callable[..., GoogleDynamicConfig | Awaitable[GoogleDynamicConfig]],
|
|
107
|
+
fn_args: dict[str, Any],
|
|
108
|
+
dynamic_config: GoogleDynamicConfig,
|
|
109
|
+
tools: list[type[BaseTool] | Callable] | None,
|
|
110
|
+
json_mode: bool,
|
|
111
|
+
call_params: GoogleCallParams | CommonCallParams,
|
|
112
|
+
response_model: type[BaseModel] | None,
|
|
113
|
+
stream: bool | StreamConfig,
|
|
114
|
+
) -> tuple[
|
|
115
|
+
CreateFn[GenerateContentResponse, GenerateContentResponse]
|
|
116
|
+
| AsyncCreateFn[GenerateContentResponse, GenerateContentResponse],
|
|
117
|
+
str | None,
|
|
118
|
+
list[ContentDict],
|
|
119
|
+
list[type[GoogleTool]] | None,
|
|
120
|
+
GoogleCallKwargs,
|
|
121
|
+
]:
|
|
122
|
+
prompt_template, messages, tool_types, base_call_kwargs = _utils.setup_call(
|
|
123
|
+
fn,
|
|
124
|
+
fn_args,
|
|
125
|
+
dynamic_config,
|
|
126
|
+
tools,
|
|
127
|
+
GoogleTool,
|
|
128
|
+
call_params,
|
|
129
|
+
convert_common_call_params,
|
|
130
|
+
)
|
|
131
|
+
call_kwargs = cast(GoogleCallKwargs, base_call_kwargs)
|
|
132
|
+
messages = cast(list[BaseMessageParam | ContentDict], messages)
|
|
133
|
+
|
|
134
|
+
if client is None:
|
|
135
|
+
client = Client()
|
|
136
|
+
|
|
137
|
+
messages = convert_message_params(messages, client)
|
|
138
|
+
|
|
139
|
+
if messages[0] and messages[0].get("role") == "system":
|
|
140
|
+
with _generate_content_config_context(call_kwargs) as config:
|
|
141
|
+
config.system_instruction = [
|
|
142
|
+
Part.model_validate(part)
|
|
143
|
+
for part in (messages.pop(0).get("parts") or [])
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
if json_mode:
|
|
147
|
+
with _generate_content_config_context(call_kwargs) as config:
|
|
148
|
+
if not tools:
|
|
149
|
+
config.response_mime_type = "application/json"
|
|
150
|
+
messages[-1]["parts"].append( # pyright: ignore [reportTypedDictNotRequiredAccess, reportOptionalMemberAccess, reportArgumentType]
|
|
151
|
+
PartDict(text=_utils.json_mode_content(response_model))
|
|
152
|
+
) # pyright: ignore [reportTypedDictNotRequiredAccess, reportOptionalMemberAccess, reportArgumentType]
|
|
153
|
+
elif response_model:
|
|
154
|
+
assert tool_types, "At least one tool must be provided for extraction."
|
|
155
|
+
with _generate_content_config_context(call_kwargs) as config:
|
|
156
|
+
config.tool_config = ToolConfig(
|
|
157
|
+
function_calling_config=FunctionCallingConfig(
|
|
158
|
+
mode=FunctionCallingConfigMode.ANY,
|
|
159
|
+
allowed_function_names=[tool_types[0]._name()],
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
config_tools = call_kwargs.pop("tools", None)
|
|
163
|
+
if config_tools:
|
|
164
|
+
with _generate_content_config_context(call_kwargs) as config:
|
|
165
|
+
config.tools = cast(ToolListUnion, config_tools)
|
|
166
|
+
|
|
167
|
+
call_kwargs |= {"model": model, "contents": messages}
|
|
168
|
+
|
|
169
|
+
create = (
|
|
170
|
+
get_async_create_fn(
|
|
171
|
+
client.aio.models.generate_content,
|
|
172
|
+
client.aio.models.generate_content_stream,
|
|
173
|
+
)
|
|
174
|
+
if fn_is_async(fn)
|
|
175
|
+
else get_create_fn(
|
|
176
|
+
client.models.generate_content, client.models.generate_content_stream
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return create, prompt_template, messages, tool_types, call_kwargs
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""usage docs: learn/calls.md#provider-specific-parameters"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from google.genai.types import (
|
|
6
|
+
GenerateContentConfigOrDict,
|
|
7
|
+
)
|
|
8
|
+
from typing_extensions import NotRequired
|
|
9
|
+
|
|
10
|
+
from ..base import BaseCallParams
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GoogleCallParams(BaseCallParams):
|
|
14
|
+
"""The parameters to use when calling the Google API.
|
|
15
|
+
|
|
16
|
+
[Google API Reference](https://ai.google.dev/google-api/docs/text-generation?lang=python)
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
config: ...
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
config: NotRequired[GenerateContentConfigOrDict]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""This module contains the `GoogleCallResponse` class.
|
|
2
|
+
|
|
3
|
+
usage docs: learn/calls.md#handling-responses
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from functools import cached_property
|
|
7
|
+
|
|
8
|
+
from google.genai.types import (
|
|
9
|
+
ContentDict,
|
|
10
|
+
ContentListUnion,
|
|
11
|
+
ContentListUnionDict,
|
|
12
|
+
FunctionResponseDict,
|
|
13
|
+
GenerateContentResponse,
|
|
14
|
+
PartDict,
|
|
15
|
+
# Import manually SchemaDict to avoid Pydantic error
|
|
16
|
+
SchemaDict, # noqa: F401
|
|
17
|
+
Tool,
|
|
18
|
+
)
|
|
19
|
+
from pydantic import computed_field
|
|
20
|
+
|
|
21
|
+
from .. import BaseMessageParam
|
|
22
|
+
from ..base import BaseCallResponse, transform_tool_outputs
|
|
23
|
+
from ..base.types import FinishReason
|
|
24
|
+
from ._utils import calculate_cost
|
|
25
|
+
from ._utils._convert_finish_reason_to_common_finish_reasons import (
|
|
26
|
+
_convert_finish_reasons_to_common_finish_reasons,
|
|
27
|
+
)
|
|
28
|
+
from ._utils._message_param_converter import GoogleMessageParamConverter
|
|
29
|
+
from .call_params import GoogleCallParams
|
|
30
|
+
from .dynamic_config import GoogleDynamicConfig
|
|
31
|
+
from .tool import GoogleTool
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class GoogleCallResponse(
|
|
35
|
+
BaseCallResponse[
|
|
36
|
+
GenerateContentResponse,
|
|
37
|
+
GoogleTool,
|
|
38
|
+
Tool,
|
|
39
|
+
GoogleDynamicConfig,
|
|
40
|
+
ContentListUnion | ContentListUnionDict,
|
|
41
|
+
GoogleCallParams,
|
|
42
|
+
ContentDict,
|
|
43
|
+
]
|
|
44
|
+
):
|
|
45
|
+
"""A convenience wrapper around the Google API response.
|
|
46
|
+
|
|
47
|
+
When calling the Google API using a function decorated with `google_call`, the
|
|
48
|
+
response will be a `GoogleCallResponse` instance with properties that allow for
|
|
49
|
+
more convenient access to commonly used attributes.
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from mirascope.core import prompt_template
|
|
56
|
+
from mirascope.core.google import google_call
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@google_call("google-1.5-flash")
|
|
60
|
+
def recommend_book(genre: str) -> str:
|
|
61
|
+
return f"Recommend a {genre} book"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
response = recommend_book("fantasy") # response is an `GoogleCallResponse` instance
|
|
65
|
+
print(response.content)
|
|
66
|
+
```
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
_provider = "google"
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def content(self) -> str:
|
|
73
|
+
"""Returns the contained string content for the 0th choice."""
|
|
74
|
+
return self.response.candidates[0].content.parts[0].text # pyright: ignore [reportOptionalSubscript, reportReturnType, reportOptionalMemberAccess, reportOptionalIterable]
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def finish_reasons(self) -> list[str]:
|
|
78
|
+
"""Returns the finish reasons of the response."""
|
|
79
|
+
|
|
80
|
+
return [
|
|
81
|
+
candidate.finish_reason.value
|
|
82
|
+
for candidate in (self.response.candidates or [])
|
|
83
|
+
if candidate and candidate.finish_reason is not None
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def model(self) -> str:
|
|
88
|
+
"""Returns the model name.
|
|
89
|
+
|
|
90
|
+
google.generativeai does not return model, so we return the model provided by
|
|
91
|
+
the user.
|
|
92
|
+
"""
|
|
93
|
+
return self._model
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def id(self) -> str | None:
|
|
97
|
+
"""Returns the id of the response.
|
|
98
|
+
|
|
99
|
+
google.generativeai does not return an id
|
|
100
|
+
"""
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def usage(self) -> None:
|
|
105
|
+
"""Returns the usage of the chat completion.
|
|
106
|
+
|
|
107
|
+
google.generativeai does not have Usage, so we return None
|
|
108
|
+
"""
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def input_tokens(self) -> None:
|
|
113
|
+
"""Returns the number of input tokens."""
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def output_tokens(self) -> None:
|
|
118
|
+
"""Returns the number of output tokens."""
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def cost(self) -> float | None:
|
|
123
|
+
"""Returns the cost of the call."""
|
|
124
|
+
return calculate_cost(self.input_tokens, self.output_tokens, self.model)
|
|
125
|
+
|
|
126
|
+
@computed_field
|
|
127
|
+
@cached_property
|
|
128
|
+
def message_param(self) -> ContentDict:
|
|
129
|
+
"""Returns the models's response as a message parameter."""
|
|
130
|
+
return {"role": "model", "parts": self.response.candidates[0].content.parts} # pyright: ignore [reportReturnType, reportOptionalSubscript, reportOptionalMemberAccess]
|
|
131
|
+
|
|
132
|
+
@computed_field
|
|
133
|
+
@cached_property
|
|
134
|
+
def tools(self) -> list[GoogleTool] | None:
|
|
135
|
+
"""Returns the list of tools for the 0th candidate's 0th content part."""
|
|
136
|
+
if self.tool_types is None:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
extracted_tools = []
|
|
140
|
+
for part in self.response.candidates[0].content.parts: # pyright: ignore [reportReturnType, reportOptionalSubscript, reportOptionalMemberAccess, reportOptionalIterable]
|
|
141
|
+
tool_call = part.function_call
|
|
142
|
+
for tool_type in self.tool_types:
|
|
143
|
+
if tool_call.name == tool_type._name(): # pyright: ignore [reportOptionalMemberAccess]
|
|
144
|
+
extracted_tools.append(tool_type.from_tool_call(tool_call)) # pyright: ignore [reportArgumentType]
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
return extracted_tools
|
|
148
|
+
|
|
149
|
+
@computed_field
|
|
150
|
+
@cached_property
|
|
151
|
+
def tool(self) -> GoogleTool | None:
|
|
152
|
+
"""Returns the 0th tool for the 0th candidate's 0th content part.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
ValidationError: if the tool call doesn't match the tool's schema.
|
|
156
|
+
"""
|
|
157
|
+
tools = self.tools
|
|
158
|
+
if tools:
|
|
159
|
+
return tools[0]
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
@transform_tool_outputs
|
|
164
|
+
def tool_message_params(
|
|
165
|
+
cls, tools_and_outputs: list[tuple[GoogleTool, str]]
|
|
166
|
+
) -> list[ContentDict]:
|
|
167
|
+
"""Returns the tool message parameters for tool call results.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
tools_and_outputs: The list of tools and their outputs from which the tool
|
|
171
|
+
message parameters should be constructed.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
The list of constructed `FunctionResponse` parameters.
|
|
175
|
+
"""
|
|
176
|
+
return [
|
|
177
|
+
{
|
|
178
|
+
"role": "user",
|
|
179
|
+
"parts": [
|
|
180
|
+
PartDict(
|
|
181
|
+
function_response=FunctionResponseDict(
|
|
182
|
+
name=tool._name(), response={"result": output}
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
for tool, output in tools_and_outputs
|
|
186
|
+
],
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def common_finish_reasons(self) -> list[FinishReason] | None:
|
|
192
|
+
return _convert_finish_reasons_to_common_finish_reasons(self.finish_reasons)
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def common_message_param(self) -> BaseMessageParam:
|
|
196
|
+
return GoogleMessageParamConverter.from_provider([self.message_param])[0]
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def common_user_message_param(self) -> BaseMessageParam | None:
|
|
200
|
+
if not self.user_message_param:
|
|
201
|
+
return None
|
|
202
|
+
return GoogleMessageParamConverter.from_provider([self.user_message_param])[0]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""This module contains the `GoogleCallResponseChunk` class.
|
|
2
|
+
|
|
3
|
+
usage docs: learn/streams.md#handling-streamed-responses
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
from google.genai.types import FinishReason as GoogleFinishReason
|
|
9
|
+
from google.genai.types import GenerateContentResponse
|
|
10
|
+
|
|
11
|
+
from mirascope.core.base.types import FinishReason
|
|
12
|
+
|
|
13
|
+
from ..base import BaseCallResponseChunk
|
|
14
|
+
from ._utils._convert_finish_reason_to_common_finish_reasons import (
|
|
15
|
+
_convert_finish_reasons_to_common_finish_reasons,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GoogleCallResponseChunk(
|
|
20
|
+
BaseCallResponseChunk[GenerateContentResponse, GoogleFinishReason]
|
|
21
|
+
):
|
|
22
|
+
"""A convenience wrapper around the Google API streamed response chunks.
|
|
23
|
+
|
|
24
|
+
When calling the Google API using a function decorated with `google_call` and
|
|
25
|
+
`stream` set to `True`, the stream will contain `GoogleCallResponseChunk` instances
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from mirascope.core import prompt_template
|
|
31
|
+
from mirascope.core.google import google_call
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@google_call("google-1.5-flash", stream=True)
|
|
35
|
+
def recommend_book(genre: str) -> str:
|
|
36
|
+
return f"Recommend a {genre} book"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
stream = recommend_book("fantasy") # response is an `GoogleStream`
|
|
40
|
+
for chunk, _ in stream:
|
|
41
|
+
print(chunk.content, end="", flush=True)
|
|
42
|
+
```
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def content(self) -> str:
|
|
47
|
+
"""Returns the chunk content for the 0th choice."""
|
|
48
|
+
return self.chunk.candidates[0].content.parts[0].text # pyright: ignore [reportOptionalMemberAccess, reportOptionalSubscript, reportReturnType]
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def finish_reasons(self) -> list[GoogleFinishReason]:
|
|
52
|
+
"""Returns the finish reasons of the response."""
|
|
53
|
+
return [
|
|
54
|
+
candidate.finish_reason
|
|
55
|
+
for candidate in (self.chunk.candidates or [])
|
|
56
|
+
if candidate.finish_reason is not None
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def model(self) -> None:
|
|
61
|
+
"""Returns the model name.
|
|
62
|
+
|
|
63
|
+
google.generativeai does not return model, so we return None
|
|
64
|
+
"""
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def id(self) -> str | None:
|
|
69
|
+
"""Returns the id of the response.
|
|
70
|
+
|
|
71
|
+
google.generativeai does not return an id
|
|
72
|
+
"""
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def usage(self) -> None:
|
|
77
|
+
"""Returns the usage of the chat completion.
|
|
78
|
+
|
|
79
|
+
google.generativeai does not have Usage, so we return None
|
|
80
|
+
"""
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def input_tokens(self) -> None:
|
|
85
|
+
"""Returns the number of input tokens."""
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def output_tokens(self) -> None:
|
|
90
|
+
"""Returns the number of output tokens."""
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def common_finish_reasons(self) -> list[FinishReason] | None:
|
|
95
|
+
return _convert_finish_reasons_to_common_finish_reasons(
|
|
96
|
+
cast(list[str], self.finish_reasons)
|
|
97
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""This module defines the function return type for functions as LLM calls."""
|
|
2
|
+
|
|
3
|
+
from google.genai import Client
|
|
4
|
+
from google.genai.types import ContentListUnion, ContentListUnionDict
|
|
5
|
+
|
|
6
|
+
from ..base import BaseDynamicConfig, BaseMessageParam
|
|
7
|
+
from .call_params import GoogleCallParams
|
|
8
|
+
|
|
9
|
+
GoogleDynamicConfig = BaseDynamicConfig[
|
|
10
|
+
ContentListUnion | ContentListUnionDict | BaseMessageParam, GoogleCallParams, Client
|
|
11
|
+
]
|
|
12
|
+
"""The function return type for functions wrapped with the `google_call` decorator.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from mirascope.core import prompt_template
|
|
18
|
+
from mirascope.core.google import GoogleDynamicConfig, google_call
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@google_call("google-1.5-flash")
|
|
22
|
+
@prompt_template("Recommend a {capitalized_genre} book")
|
|
23
|
+
def recommend_book(genre: str) -> GoogleDynamicConfig:
|
|
24
|
+
return {"computed_fields": {"capitalized_genre": genre.capitalize()}}
|
|
25
|
+
```
|
|
26
|
+
"""
|