google-genai 0.6.0__py3-none-any.whl → 0.7.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.
@@ -17,20 +17,28 @@
17
17
 
18
18
  import base64
19
19
  from collections.abc import Iterable, Mapping
20
+ from enum import Enum, EnumMeta
20
21
  import inspect
21
22
  import io
22
23
  import re
23
24
  import time
24
25
  import typing
25
26
  from typing import Any, GenericAlias, Optional, Union
27
+ import sys
28
+
29
+ if typing.TYPE_CHECKING:
30
+ import PIL.Image
26
31
 
27
- import PIL.Image
28
- import PIL.PngImagePlugin
29
32
  import pydantic
30
33
 
31
34
  from . import _api_client
32
35
  from . import types
33
36
 
37
+ if sys.version_info >= (3, 11):
38
+ from types import UnionType
39
+ else:
40
+ UnionType = typing._UnionGenericAlias
41
+
34
42
 
35
43
  def _resource_name(
36
44
  client: _api_client.ApiClient,
@@ -192,9 +200,15 @@ def t_caches_model(api_client: _api_client.ApiClient, model: str):
192
200
  return model
193
201
 
194
202
 
195
- def pil_to_blob(img):
203
+ def pil_to_blob(img) -> types.Blob:
204
+ try:
205
+ import PIL.PngImagePlugin
206
+ PngImagePlugin = PIL.PngImagePlugin
207
+ except ImportError:
208
+ PngImagePlugin = None
209
+
196
210
  bytesio = io.BytesIO()
197
- if isinstance(img, PIL.PngImagePlugin.PngImageFile) or img.mode == 'RGBA':
211
+ if PngImagePlugin is not None and isinstance(img, PngImagePlugin.PngImageFile) or img.mode == 'RGBA':
198
212
  img.save(bytesio, format='PNG')
199
213
  mime_type = 'image/png'
200
214
  else:
@@ -205,20 +219,26 @@ def pil_to_blob(img):
205
219
  return types.Blob(mime_type=mime_type, data=data)
206
220
 
207
221
 
208
- PartType = Union[types.Part, types.PartDict, str, PIL.Image.Image]
222
+ PartType = Union[types.Part, types.PartDict, str, 'PIL.Image.Image']
209
223
 
210
224
 
211
225
  def t_part(client: _api_client.ApiClient, part: PartType) -> types.Part:
226
+ try:
227
+ import PIL.Image
228
+ PIL_Image = PIL.Image.Image
229
+ except ImportError:
230
+ PIL_Image = None
231
+
212
232
  if not part:
213
233
  raise ValueError('content part is required.')
214
234
  if isinstance(part, str):
215
235
  return types.Part(text=part)
216
- if isinstance(part, PIL.Image.Image):
236
+ if PIL_Image is not None and isinstance(part, PIL_Image):
217
237
  return types.Part(inline_data=pil_to_blob(part))
218
238
  if isinstance(part, types.File):
219
239
  if not part.uri or not part.mime_type:
220
240
  raise ValueError('file uri and mime_type are required.')
221
- return types.Part.from_uri(part.uri, part.mime_type)
241
+ return types.Part.from_uri(file_uri=part.uri, mime_type=part.mime_type)
222
242
  else:
223
243
  return part
224
244
 
@@ -297,104 +317,192 @@ def t_contents(
297
317
  return [t_content(client, contents)]
298
318
 
299
319
 
300
- def process_schema(
301
- data: dict[str, Any], client: Optional[_api_client.ApiClient] = None
302
- ):
303
- if isinstance(data, dict):
304
- # Iterate over a copy of keys to allow deletion
305
- for key in list(data.keys()):
306
- # Only delete 'title'for the Gemini API
307
- if client and not client.vertexai and key == 'title':
308
- del data[key]
309
- else:
310
- process_schema(data[key], client)
311
- elif isinstance(data, list):
312
- for item in data:
313
- process_schema(item, client)
320
+ def handle_null_fields(schema: dict[str, Any]):
321
+ """Process null fields in the schema so it is compatible with OpenAPI.
314
322
 
315
- return data
323
+ The OpenAPI spec does not support 'type: 'null' in the schema. This function
324
+ handles this case by adding 'nullable: True' to the null field and removing
325
+ the {'type': 'null'} entry.
316
326
 
327
+ https://swagger.io/docs/specification/v3_0/data-models/data-types/#null
317
328
 
318
- def _build_schema(fname: str, fields_dict: dict[str, Any]) -> dict[str, Any]:
319
- parameters = pydantic.create_model(fname, **fields_dict).model_json_schema()
320
- defs = parameters.pop('$defs', {})
329
+ Example of schema properties before and after handling null fields:
330
+ Before:
331
+ {
332
+ "name": {
333
+ "title": "Name",
334
+ "type": "string"
335
+ },
336
+ "total_area_sq_mi": {
337
+ "anyOf": [
338
+ {
339
+ "type": "integer"
340
+ },
341
+ {
342
+ "type": "null"
343
+ }
344
+ ],
345
+ "default": null,
346
+ "title": "Total Area Sq Mi"
347
+ }
348
+ }
321
349
 
322
- for _, value in defs.items():
323
- unpack_defs(value, defs)
350
+ After:
351
+ {
352
+ "name": {
353
+ "title": "Name",
354
+ "type": "string"
355
+ },
356
+ "total_area_sq_mi": {
357
+ "type": "integer",
358
+ "nullable": true,
359
+ "default": null,
360
+ "title": "Total Area Sq Mi"
361
+ }
362
+ }
363
+ """
364
+ if (
365
+ isinstance(schema, dict)
366
+ and 'type' in schema
367
+ and schema['type'] == 'null'
368
+ ):
369
+ schema['nullable'] = True
370
+ del schema['type']
371
+ elif 'anyOf' in schema:
372
+ for item in schema['anyOf']:
373
+ if 'type' in item and item['type'] == 'null':
374
+ schema['nullable'] = True
375
+ schema['anyOf'].remove({'type': 'null'})
376
+ if len(schema['anyOf']) == 1:
377
+ # If there is only one type left after removing null, remove the anyOf field.
378
+ field_type = schema['anyOf'][0]['type']
379
+ schema['type'] = field_type
380
+ del schema['anyOf']
324
381
 
325
- unpack_defs(parameters, defs)
326
- return parameters['properties']['dummy']
327
382
 
383
+ def process_schema(
384
+ schema: dict[str, Any],
385
+ client: Optional[_api_client.ApiClient] = None,
386
+ defs: Optional[dict[str, Any]]=None):
387
+ """Updates the schema and each sub-schema inplace to be API-compatible.
328
388
 
329
- def unpack_defs(schema: dict[str, Any], defs: dict[str, Any]):
330
- """Unpacks the $defs values in the schema generated by pydantic so they can be understood by the API.
389
+ - Removes the `title` field from the schema if the client is not vertexai.
390
+ - Inlines the $defs.
331
391
 
332
- Example of a schema before and after unpacking:
392
+ Example of a schema before and after (with mldev):
333
393
  Before:
334
394
 
335
395
  `schema`
336
396
 
337
- {'properties': {
338
- 'dummy': {
339
- 'items': {
340
- '$ref': '#/$defs/CountryInfo'
341
- },
342
- 'title': 'Dummy',
343
- 'type': 'array'
344
- }
397
+ {
398
+ 'items': {
399
+ '$ref': '#/$defs/CountryInfo'
345
400
  },
346
- 'required': ['dummy'],
347
- 'title': 'dummy',
348
- 'type': 'object'}
401
+ 'title': 'Placeholder',
402
+ 'type': 'array'
403
+ }
404
+
349
405
 
350
406
  `defs`
351
407
 
352
- {'CountryInfo': {'properties': {'continent': {'title': 'Continent', 'type':
353
- 'string'}, 'gdp': {'title': 'Gdp', 'type': 'integer'}}, 'required':
354
- ['continent', 'gdp'], 'title': 'CountryInfo', 'type': 'object'}}
408
+ {
409
+ 'CountryInfo': {
410
+ 'properties': {
411
+ 'continent': {
412
+ 'title': 'Continent',
413
+ 'type': 'string'
414
+ },
415
+ 'gdp': {
416
+ 'title': 'Gdp',
417
+ 'type': 'integer'}
418
+ },
419
+ }
420
+ 'required':['continent', 'gdp'],
421
+ 'title': 'CountryInfo',
422
+ 'type': 'object'
423
+ }
424
+ }
355
425
 
356
426
  After:
357
427
 
358
428
  `schema`
359
- {'properties': {
360
- 'continent': {'title': 'Continent', 'type': 'string'},
361
- 'gdp': {'title': 'Gdp', 'type': 'integer'}
362
- },
363
- 'required': ['continent', 'gdp'],
364
- 'title': 'CountryInfo',
365
- 'type': 'object'
429
+ {
430
+ 'items': {
431
+ 'properties': {
432
+ 'continent': {
433
+ 'type': 'string'
434
+ },
435
+ 'gdp': {
436
+ 'type': 'integer'}
437
+ },
438
+ }
439
+ 'required':['continent', 'gdp'],
440
+ 'type': 'object'
441
+ },
442
+ 'type': 'array'
366
443
  }
367
444
  """
368
- properties = schema.get('properties', None)
369
- if properties is None:
445
+ if client and not client.vertexai:
446
+ schema.pop('title', None)
447
+
448
+ if defs is None:
449
+ defs = schema.pop('$defs', {})
450
+ for _, sub_schema in defs.items():
451
+ process_schema(sub_schema, client, defs)
452
+
453
+ handle_null_fields(schema)
454
+
455
+ any_of = schema.get('anyOf', None)
456
+ if any_of is not None:
457
+ for sub_schema in any_of:
458
+ process_schema(sub_schema, client, defs)
370
459
  return
371
460
 
372
- for name, value in properties.items():
373
- ref_key = value.get('$ref', None)
374
- if ref_key is not None:
375
- ref = defs[ref_key.split('defs/')[-1]]
376
- unpack_defs(ref, defs)
377
- properties[name] = ref
378
- continue
379
-
380
- anyof = value.get('anyOf', None)
381
- if anyof is not None:
382
- for i, atype in enumerate(anyof):
383
- ref_key = atype.get('$ref', None)
384
- if ref_key is not None:
385
- ref = defs[ref_key.split('defs/')[-1]]
386
- unpack_defs(ref, defs)
387
- anyof[i] = ref
388
- continue
389
-
390
- items = value.get('items', None)
391
- if items is not None:
392
- ref_key = items.get('$ref', None)
393
- if ref_key is not None:
461
+ schema_type = schema.get('type', None)
462
+ if isinstance(schema_type, Enum):
463
+ schema_type = schema_type.value
464
+ schema_type = schema_type.upper()
465
+
466
+ if schema_type == 'OBJECT':
467
+ properties = schema.get('properties', None)
468
+ if properties is None:
469
+ return
470
+ for name, sub_schema in properties.items():
471
+ ref_key = sub_schema.get('$ref', None)
472
+ if ref_key is None:
473
+ process_schema(sub_schema, client, defs)
474
+ else:
394
475
  ref = defs[ref_key.split('defs/')[-1]]
395
- unpack_defs(ref, defs)
396
- value['items'] = ref
397
- continue
476
+ process_schema(ref, client, defs)
477
+ properties[name] = ref
478
+ elif schema_type == 'ARRAY':
479
+ sub_schema = schema.get('items', None)
480
+ if sub_schema is None:
481
+ return
482
+ ref_key = sub_schema.get('$ref', None)
483
+ if ref_key is None:
484
+ process_schema(sub_schema, client, defs)
485
+ else:
486
+ ref = defs[ref_key.split('defs/')[-1]]
487
+ process_schema(ref, client, defs)
488
+ schema['items'] = ref
489
+
490
+ def _process_enum(
491
+ enum: EnumMeta, client: Optional[_api_client.ApiClient] = None
492
+ ) -> types.Schema:
493
+ for member in enum:
494
+ if not isinstance(member.value, str):
495
+ raise TypeError(
496
+ f'Enum member {member.name} value must be a string, got'
497
+ f' {type(member.value)}'
498
+ )
499
+ class Placeholder(pydantic.BaseModel):
500
+ placeholder: enum
501
+
502
+ enum_schema = Placeholder.model_json_schema()
503
+ process_schema(enum_schema, client)
504
+ enum_schema = enum_schema['properties']['placeholder']
505
+ return types.Schema.model_validate(enum_schema)
398
506
 
399
507
 
400
508
  def t_schema(
@@ -403,28 +511,39 @@ def t_schema(
403
511
  if not origin:
404
512
  return None
405
513
  if isinstance(origin, dict):
406
- return process_schema(origin, client)
514
+ process_schema(origin, client)
515
+ return types.Schema.model_validate(origin)
516
+ if isinstance(origin, EnumMeta):
517
+ return _process_enum(origin, client)
407
518
  if isinstance(origin, types.Schema):
408
519
  if dict(origin) == dict(types.Schema()):
409
520
  # response_schema value was coerced to an empty Schema instance because it did not adhere to the Schema field annotation
410
521
  raise ValueError(f'Unsupported schema type.')
411
- schema = process_schema(origin.model_dump(exclude_unset=True), client)
522
+ schema = origin.model_dump(exclude_unset=True)
523
+ process_schema(schema, client)
412
524
  return types.Schema.model_validate(schema)
413
- if isinstance(origin, GenericAlias):
414
- if origin.__origin__ is list:
415
- if isinstance(origin.__args__[0], typing.types.UnionType):
416
- raise ValueError(f'Unsupported schema type: GenericAlias {origin}')
417
- if issubclass(origin.__args__[0], pydantic.BaseModel):
418
- # Handle cases where response schema is `list[pydantic.BaseModel]`
419
- list_schema = _build_schema(
420
- 'dummy', {'dummy': (origin, pydantic.Field())}
421
- )
422
- list_schema = process_schema(list_schema, client)
423
- return types.Schema.model_validate(list_schema)
424
- raise ValueError(f'Unsupported schema type: GenericAlias {origin}')
425
- if issubclass(origin, pydantic.BaseModel):
426
- schema = process_schema(origin.model_json_schema(), client)
525
+
526
+ if (
527
+ # in Python 3.9 Generic alias list[int] counts as a type,
528
+ # and breaks issubclass because it's not a class.
529
+ not isinstance(origin, GenericAlias) and
530
+ isinstance(origin, type) and
531
+ issubclass(origin, pydantic.BaseModel)
532
+ ):
533
+ schema = origin.model_json_schema()
534
+ process_schema(schema, client)
427
535
  return types.Schema.model_validate(schema)
536
+ elif (
537
+ isinstance(origin, GenericAlias) or isinstance(origin, type) or isinstance(origin, UnionType)
538
+ ):
539
+ class Placeholder(pydantic.BaseModel):
540
+ placeholder: origin
541
+
542
+ schema = Placeholder.model_json_schema()
543
+ process_schema(schema, client)
544
+ schema = schema['properties']['placeholder']
545
+ return types.Schema.model_validate(schema)
546
+
428
547
  raise ValueError(f'Unsupported schema type: {origin}')
429
548
 
430
549
 
@@ -464,7 +583,9 @@ def t_tool(client: _api_client.ApiClient, origin) -> types.Tool:
464
583
  if inspect.isfunction(origin) or inspect.ismethod(origin):
465
584
  return types.Tool(
466
585
  function_declarations=[
467
- types.FunctionDeclaration.from_callable(client, origin)
586
+ types.FunctionDeclaration.from_callable(
587
+ client=client, callable=origin
588
+ )
468
589
  ]
469
590
  )
470
591
  else: