google-genai 1.10.0__py3-none-any.whl → 1.11.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.
@@ -29,21 +29,31 @@ import json
29
29
  import logging
30
30
  import math
31
31
  import os
32
+ import ssl
32
33
  import sys
33
34
  import time
34
35
  from typing import Any, AsyncIterator, Optional, Tuple, Union
35
- from urllib.parse import urlparse, urlunparse
36
+ from urllib.parse import urlparse
37
+ from urllib.parse import urlunparse
38
+
36
39
  import anyio
40
+ import certifi
37
41
  import google.auth
38
42
  import google.auth.credentials
39
43
  from google.auth.credentials import Credentials
40
44
  from google.auth.transport.requests import Request
41
45
  import httpx
42
- from pydantic import BaseModel, Field, ValidationError
46
+ from pydantic import BaseModel
47
+ from pydantic import Field
48
+ from pydantic import ValidationError
49
+
43
50
  from . import _common
44
51
  from . import errors
45
52
  from . import version
46
- from .types import HttpOptions, HttpOptionsDict, HttpOptionsOrDict
53
+ from .types import HttpOptions
54
+ from .types import HttpOptionsDict
55
+ from .types import HttpOptionsOrDict
56
+
47
57
 
48
58
  logger = logging.getLogger('google_genai._api_client')
49
59
  CHUNK_SIZE = 8 * 1024 * 1024 # 8 MB chunk size
@@ -414,7 +424,7 @@ class BaseApiClient:
414
424
  if not self.api_key:
415
425
  raise ValueError(
416
426
  'Missing key inputs argument! To use the Google AI API,'
417
- 'provide (`api_key`) arguments. To use the Google Cloud API,'
427
+ ' provide (`api_key`) arguments. To use the Google Cloud API,'
418
428
  ' provide (`vertexai`, `project` & `location`) arguments.'
419
429
  )
420
430
  self._http_options.base_url = 'https://generativelanguage.googleapis.com/'
@@ -432,9 +442,67 @@ class BaseApiClient:
432
442
  else:
433
443
  if self._http_options.headers is not None:
434
444
  _append_library_version_headers(self._http_options.headers)
435
- # Initialize the httpx client.
436
- self._httpx_client = SyncHttpxClient()
437
- self._async_httpx_client = AsyncHttpxClient()
445
+
446
+ client_args, async_client_args = self._ensure_ssl_ctx(self._http_options)
447
+ self._httpx_client = SyncHttpxClient(**client_args)
448
+ self._async_httpx_client = AsyncHttpxClient(**async_client_args)
449
+
450
+ @staticmethod
451
+ def _ensure_ssl_ctx(options: HttpOptions) -> (
452
+ Tuple[dict[str, Any], dict[str, Any]]):
453
+ """Ensures the SSL context is present in the client args.
454
+
455
+ Creates a default SSL context if one is not provided.
456
+
457
+ Args:
458
+ options: The http options to check for SSL context.
459
+
460
+ Returns:
461
+ A tuple of sync/async httpx client args.
462
+ """
463
+
464
+ verify = 'verify'
465
+ args = options.client_args
466
+ async_args = options.async_client_args
467
+ ctx = (
468
+ args.get(verify) if args else None
469
+ or async_args.get(verify) if async_args else None
470
+ )
471
+
472
+ if not ctx:
473
+ # Initialize the SSL context for the httpx client.
474
+ # Unlike requests, the httpx package does not automatically pull in the
475
+ # environment variables SSL_CERT_FILE or SSL_CERT_DIR. They need to be
476
+ # enabled explicitly.
477
+ ctx = ssl.create_default_context(
478
+ cafile=os.environ.get('SSL_CERT_FILE', certifi.where()),
479
+ capath=os.environ.get('SSL_CERT_DIR'),
480
+ )
481
+
482
+ def _maybe_set(
483
+ args: Optional[dict[str, Any]],
484
+ ctx: ssl.SSLContext,
485
+ ) -> dict[str, Any]:
486
+ """Sets the SSL context in the client args if not set.
487
+
488
+ Does not override the SSL context if it is already set.
489
+
490
+ Args:
491
+ args: The client args to to check for SSL context.
492
+ ctx: The SSL context to set.
493
+
494
+ Returns:
495
+ The client args with the SSL context included.
496
+ """
497
+ if not args or not args.get(verify):
498
+ args = (args or {}).copy()
499
+ args[verify] = ctx
500
+ return args
501
+
502
+ return (
503
+ _maybe_set(args, ctx),
504
+ _maybe_set(async_args, ctx),
505
+ )
438
506
 
439
507
  def _websocket_base_url(self):
440
508
  url_parts = urlparse(self._http_options.base_url)
@@ -78,6 +78,7 @@ def format_destination(
78
78
 
79
79
  def get_function_map(
80
80
  config: Optional[types.GenerateContentConfigOrDict] = None,
81
+ is_caller_method_async: bool = False,
81
82
  ) -> dict[str, Callable]:
82
83
  """Returns a function map from the config."""
83
84
  function_map: dict[str, Callable] = {}
@@ -87,7 +88,7 @@ def get_function_map(
87
88
  if config_model.tools:
88
89
  for tool in config_model.tools:
89
90
  if callable(tool):
90
- if inspect.iscoroutinefunction(tool):
91
+ if inspect.iscoroutinefunction(tool) and not is_caller_method_async:
91
92
  raise errors.UnsupportedFunctionError(
92
93
  f'Function {tool.__name__} is a coroutine function, which is not'
93
94
  ' supported for automatic function calling. Please manually'
@@ -199,11 +200,11 @@ def convert_if_exist_pydantic_model(
199
200
  return value
200
201
 
201
202
 
202
- def invoke_function_from_dict_args(
203
- args: Dict[str, Any], function_to_invoke: Callable
204
- ) -> Any:
205
- signature = inspect.signature(function_to_invoke)
206
- func_name = function_to_invoke.__name__
203
+ def convert_argument_from_function(
204
+ args: dict[str, Any], function: Callable
205
+ ) -> dict[str, Any]:
206
+ signature = inspect.signature(function)
207
+ func_name = function.__name__
207
208
  converted_args = {}
208
209
  for param_name, param in signature.parameters.items():
209
210
  if param_name in args:
@@ -213,13 +214,34 @@ def invoke_function_from_dict_args(
213
214
  param_name,
214
215
  func_name,
215
216
  )
217
+ return converted_args
218
+
219
+
220
+ def invoke_function_from_dict_args(
221
+ args: Dict[str, Any], function_to_invoke: Callable
222
+ ) -> Any:
223
+ converted_args = convert_argument_from_function(args, function_to_invoke)
216
224
  try:
217
225
  return function_to_invoke(**converted_args)
218
226
  except Exception as e:
219
227
  raise errors.FunctionInvocationError(
220
- f'Failed to invoke function {func_name} with converted arguments'
221
- f' {converted_args} from model returned function call argument'
222
- f' {args} because of error {e}'
228
+ f'Failed to invoke function {function_to_invoke.__name__} with'
229
+ f' converted arguments {converted_args} from model returned function'
230
+ f' call argument {args} because of error {e}'
231
+ )
232
+
233
+
234
+ async def invoke_function_from_dict_args_async(
235
+ args: Dict[str, Any], function_to_invoke: Callable
236
+ ) -> Any:
237
+ converted_args = convert_argument_from_function(args, function_to_invoke)
238
+ try:
239
+ return await function_to_invoke(**converted_args)
240
+ except Exception as e:
241
+ raise errors.FunctionInvocationError(
242
+ f'Failed to invoke function {function_to_invoke.__name__} with'
243
+ f' converted arguments {converted_args} from model returned function'
244
+ f' call argument {args} because of error {e}'
223
245
  )
224
246
 
225
247
 
@@ -256,6 +278,44 @@ def get_function_response_parts(
256
278
  func_response_parts.append(func_response_part)
257
279
  return func_response_parts
258
280
 
281
+ async def get_function_response_parts_async(
282
+ response: types.GenerateContentResponse,
283
+ function_map: dict[str, Callable],
284
+ ) -> list[types.Part]:
285
+ """Returns the function response parts from the response."""
286
+ func_response_parts = []
287
+ if (
288
+ response.candidates is not None
289
+ and isinstance(response.candidates[0].content, types.Content)
290
+ and response.candidates[0].content.parts is not None
291
+ ):
292
+ for part in response.candidates[0].content.parts:
293
+ if not part.function_call:
294
+ continue
295
+ func_name = part.function_call.name
296
+ if func_name is not None and part.function_call.args is not None:
297
+ func = function_map[func_name]
298
+ args = convert_number_values_for_dict_function_call_args(
299
+ part.function_call.args
300
+ )
301
+ func_response: dict[str, Any]
302
+ try:
303
+ if inspect.iscoroutinefunction(func):
304
+ func_response = {
305
+ 'result': await invoke_function_from_dict_args_async(args, func)
306
+ }
307
+ else:
308
+ func_response = {
309
+ 'result': invoke_function_from_dict_args(args, func)
310
+ }
311
+ except Exception as e: # pylint: disable=broad-except
312
+ func_response = {'error': str(e)}
313
+ func_response_part = types.Part.from_function_response(
314
+ name=func_name, response=func_response
315
+ )
316
+ func_response_parts.append(func_response_part)
317
+ return func_response_parts
318
+
259
319
 
260
320
  def should_disable_afc(
261
321
  config: Optional[types.GenerateContentConfigOrDict] = None,
@@ -663,7 +663,9 @@ def process_schema(
663
663
  # provided directly to response_schema, it may use `any_of` instead of `anyOf.
664
664
  # Otherwise, model_json_schema() uses `anyOf`.
665
665
  for from_name, to_name in [
666
+ ('additional_properties', 'additionalProperties'),
666
667
  ('any_of', 'anyOf'),
668
+ ('prefix_items', 'prefixItems'),
667
669
  ('property_ordering', 'propertyOrdering'),
668
670
  ]:
669
671
  if (value := schema.pop(from_name, None)) is not None:
@@ -723,9 +725,16 @@ def process_schema(
723
725
  and 'propertyOrdering' not in schema
724
726
  ):
725
727
  schema['property_ordering'] = list(properties.keys())
728
+ if (additional := schema.get('additionalProperties')) is not None:
729
+ # It is legal to set 'additionalProperties' to a bool:
730
+ # https://json-schema.org/understanding-json-schema/reference/object#additionalproperties
731
+ if isinstance(additional, dict):
732
+ schema['additionalProperties'] = _recurse(additional)
726
733
  elif schema_type == 'ARRAY':
727
734
  if (items := schema.get('items')) is not None:
728
735
  schema['items'] = _recurse(items)
736
+ if (prefixes := schema.get('prefixItems')) is not None:
737
+ schema['prefixItems'] = [_recurse(prefix) for prefix in prefixes]
729
738
 
730
739
 
731
740
  def _process_enum(
@@ -845,6 +854,8 @@ def t_tool(client: _api_client.BaseApiClient, origin) -> Optional[types.Tool]:
845
854
  )
846
855
  ]
847
856
  )
857
+ elif isinstance(origin, dict):
858
+ return types.Tool.model_validate(origin)
848
859
  else:
849
860
  return origin
850
861
 
@@ -1017,3 +1028,121 @@ def t_bytes(api_client: _api_client.BaseApiClient, data: bytes) -> str:
1017
1028
  if not isinstance(data, bytes):
1018
1029
  return data
1019
1030
  return base64.b64encode(data).decode('ascii')
1031
+
1032
+
1033
+ def t_content_strict(content: types.ContentOrDict) -> types.Content:
1034
+ if isinstance(content, dict):
1035
+ return types.Content.model_validate(content)
1036
+ elif isinstance(content, types.Content):
1037
+ return content
1038
+ else:
1039
+ raise ValueError(
1040
+ f'Could not convert input (type "{type(content)}") to '
1041
+ '`types.Content`'
1042
+ )
1043
+
1044
+
1045
+ def t_contents_strict(
1046
+ contents: Union[Sequence[types.ContentOrDict], types.ContentOrDict],
1047
+ ) -> list[types.Content]:
1048
+ if isinstance(contents, Sequence):
1049
+ return [t_content_strict(content) for content in contents]
1050
+ else:
1051
+ return [t_content_strict(contents)]
1052
+
1053
+
1054
+ def t_client_content(
1055
+ turns: Optional[
1056
+ Union[Sequence[types.ContentOrDict], types.ContentOrDict]
1057
+ ] = None,
1058
+ turn_complete: bool = True,
1059
+ ) -> types.LiveClientContent:
1060
+ if turns is None:
1061
+ return types.LiveClientContent(turn_complete=turn_complete)
1062
+
1063
+ try:
1064
+ return types.LiveClientContent(
1065
+ turns=t_contents_strict(contents=turns),
1066
+ turn_complete=turn_complete,
1067
+ )
1068
+ except Exception as e:
1069
+ raise ValueError(
1070
+ f'Could not convert input (type "{type(turns)}") to '
1071
+ '`types.LiveClientContent`'
1072
+ ) from e
1073
+
1074
+
1075
+ def t_realtime_input(
1076
+ media: BlobUnion,
1077
+ ) -> types.LiveClientRealtimeInput:
1078
+ try:
1079
+ return types.LiveClientRealtimeInput(media_chunks=[t_blob(blob=media)])
1080
+ except Exception as e:
1081
+ raise ValueError(
1082
+ f'Could not convert input (type "{type(input)}") to '
1083
+ '`types.LiveClientRealtimeInput`'
1084
+ ) from e
1085
+
1086
+
1087
+ def t_tool_response(
1088
+ input: Union[
1089
+ types.FunctionResponseOrDict,
1090
+ Sequence[types.FunctionResponseOrDict],
1091
+ ],
1092
+ ) -> types.LiveClientToolResponse:
1093
+ if not input:
1094
+ raise ValueError(f'A tool response is required, got: \n{input}')
1095
+
1096
+ try:
1097
+ return types.LiveClientToolResponse(
1098
+ function_responses=t_function_responses(function_responses=input)
1099
+ )
1100
+ except Exception as e:
1101
+ raise ValueError(
1102
+ f'Could not convert input (type "{type(input)}") to '
1103
+ '`types.LiveClientToolResponse`'
1104
+ ) from e
1105
+
1106
+
1107
+ def t_live_speech_config(
1108
+ origin: Union[types.SpeechConfigUnionDict, Any],
1109
+ ) -> Optional[types.SpeechConfig]:
1110
+ if not origin:
1111
+ return None
1112
+ if isinstance(origin, types.SpeechConfig):
1113
+ return origin
1114
+ if isinstance(origin, str):
1115
+ # There is no way to know if the string is a voice name or a language code.
1116
+ raise ValueError(
1117
+ f'Unsupported speechConfig type: {type(origin)}. There is no way to'
1118
+ ' know if the string is a voice name or a language code.'
1119
+ )
1120
+ if isinstance(origin, dict):
1121
+ speech_config = types.SpeechConfig()
1122
+ if (
1123
+ 'voice_config' in origin
1124
+ and origin['voice_config'] is not None
1125
+ and 'prebuilt_voice_config' in origin['voice_config']
1126
+ and origin['voice_config']['prebuilt_voice_config'] is not None
1127
+ and 'voice_name' in origin['voice_config']['prebuilt_voice_config']
1128
+ ):
1129
+ speech_config.voice_config = types.VoiceConfig(
1130
+ prebuilt_voice_config=types.PrebuiltVoiceConfig(
1131
+ voice_name=origin['voice_config']['prebuilt_voice_config'].get(
1132
+ 'voice_name'
1133
+ )
1134
+ )
1135
+ )
1136
+ if 'language_code' in origin and origin['language_code'] is not None:
1137
+ speech_config.language_code = origin['language_code']
1138
+ if (
1139
+ speech_config.voice_config is None
1140
+ and speech_config.language_code is None
1141
+ ):
1142
+ raise ValueError(
1143
+ 'Unsupported speechConfig type: {type(origin)}. At least one of'
1144
+ ' voice_config or language_code must be set.'
1145
+ )
1146
+ return speech_config
1147
+ raise ValueError(f'Unsupported speechConfig type: {type(origin)}')
1148
+