From 85a027ebb2dedd2ec18041d5c69ee0a26532f1f4 Mon Sep 17 00:00:00 2001 From: Mark Daoust Date: Tue, 15 Apr 2025 11:47:39 -0700 Subject: [PATCH 01/10] Update version and fix samples for 0.8.5 release Change-Id: Ie0b0f883cbe5c8e5427c1d59c1b96bea0dd62fde --- google/generativeai/version.py | 2 +- setup.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/google/generativeai/version.py b/google/generativeai/version.py index b5271a21d..6df9e6f74 100644 --- a/google/generativeai/version.py +++ b/google/generativeai/version.py @@ -14,4 +14,4 @@ # limitations under the License. from __future__ import annotations -__version__ = "0.8.4" +__version__ = "0.8.5" diff --git a/setup.py b/setup.py index ee7160eaf..d8ab792a4 100644 --- a/setup.py +++ b/setup.py @@ -36,10 +36,7 @@ def get_version(): version = get_version() -if version[0] == "0": - release_status = "Development Status :: 4 - Beta" -else: - release_status = "Development Status :: 5 - Production/Stable" +release_status = "Development Status :: 7 - Inactive" dependencies = [ "google-ai-generativelanguage==0.6.15", @@ -86,6 +83,7 @@ def get_version(): "Programming Language :: Python :: 3.10", # Colab "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Typing :: Typed", From 47b0a1806c9fcf8aad7e892d72c3d97307008629 Mon Sep 17 00:00:00 2001 From: Mark Daoust Date: Tue, 15 Apr 2025 11:52:58 -0700 Subject: [PATCH 02/10] Fix samples for 0.8.5 release Change-Id: I48f3389565d68bc10f1608980298e6f1e384fc37 --- samples/tuned_models.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/samples/tuned_models.py b/samples/tuned_models.py index 970919115..66656678a 100644 --- a/samples/tuned_models.py +++ b/samples/tuned_models.py @@ -22,8 +22,10 @@ class UnitTests(absltest.TestCase): - def test_tuned_models_create(self): - # [START tuned_models_create] + @classmethod + def setUpClass(cls): + # Code to run once before all tests in the class + # [START tuned_models_create] import google.generativeai as genai import time @@ -53,7 +55,7 @@ def test_tuned_models_create(self): # You can use a tuned model here too. Set `source_model="tunedModels/..."` display_name="increment", source_model=base_model, - epoch_count=20, + epoch_count=5, batch_size=4, learning_rate=0.001, training_data=training_data, @@ -62,22 +64,25 @@ def test_tuned_models_create(self): for status in operation.wait_bar(): time.sleep(10) - result = operation.result() - print(result) + tuned_model = operation.result() + print(tuned_model) # # You can plot the loss curve with: # snapshots = pd.DataFrame(result.tuning_task.snapshots) # sns.lineplot(data=snapshots, x='epoch', y='mean_loss') - model = genai.GenerativeModel(model_name=result.name) + model = genai.GenerativeModel(model_name=tuned_model.name) result = model.generate_content("III") print(result.text) # IV # [END tuned_models_create] + + cls.tuned_model_name = tuned_model_name = tuned_model.name + def test_tuned_models_generate_content(self): # [START tuned_models_generate_content] import google.generativeai as genai - model = genai.GenerativeModel(model_name="tunedModels/my-increment-model") + model = genai.GenerativeModel(model_name=self.tuned_model_name) result = model.generate_content("III") print(result.text) # "IV" # [END tuned_models_generate_content] @@ -86,7 +91,7 @@ def test_tuned_models_get(self): # [START tuned_models_get] import google.generativeai as genai - model_info = genai.get_model("tunedModels/my-increment-model") + model_info = genai.get_model(self.tuned_model_name) print(model_info) # [END tuned_models_get] @@ -100,6 +105,7 @@ def test_tuned_models_list(self): def test_tuned_models_delete(self): import time + import google.generativeai as genai base_model = "models/gemini-1.5-flash-001-tuning" training_data = samples / "increment_tuning_data.json" @@ -109,7 +115,7 @@ def test_tuned_models_delete(self): # You can use a tuned model here too. Set `source_model="tunedModels/..."` display_name="increment", source_model=base_model, - epoch_count=20, + epoch_count=5, batch_size=4, learning_rate=0.001, training_data=training_data, @@ -135,7 +141,7 @@ def test_tuned_models_permissions_create(self): # [START tuned_models_permissions_create] import google.generativeai as genai - model_info = genai.get_model("tunedModels/my-increment-model") + model_info = genai.get_model(self.tuned_model_name) # [START_EXCLUDE] for p in model_info.permissions.list(): if p.role.name != "OWNER": @@ -161,7 +167,7 @@ def test_tuned_models_permissions_list(self): # [START tuned_models_permissions_list] import google.generativeai as genai - model_info = genai.get_model("tunedModels/my-increment-model") + model_info = genai.get_model(self.tuned_model_name) # [START_EXCLUDE] for p in model_info.permissions.list(): @@ -190,7 +196,7 @@ def test_tuned_models_permissions_get(self): # [START tuned_models_permissions_get] import google.generativeai as genai - model_info = genai.get_model("tunedModels/my-increment-model") + model_info = genai.get_model(self.tuned_model_name) # [START_EXCLUDE] for p in model_info.permissions.list(): @@ -214,7 +220,7 @@ def test_tuned_models_permissions_update(self): # [START tuned_models_permissions_update] import google.generativeai as genai - model_info = genai.get_model("tunedModels/my-increment-model") + model_info = genai.get_model(self.tuned_model_name) # [START_EXCLUDE] for p in model_info.permissions.list(): @@ -235,7 +241,7 @@ def test_tuned_models_permission_delete(self): # [START tuned_models_permissions_delete] import google.generativeai as genai - model_info = genai.get_model("tunedModels/my-increment-model") + model_info = genai.get_model(self.tuned_model_name) # [START_EXCLUDE] for p in model_info.permissions.list(): if p.role.name != "OWNER": From cc129fe3ba60a3cd83e324b53243da8c91d9239c Mon Sep 17 00:00:00 2001 From: Mark Daoust Date: Tue, 15 Apr 2025 11:59:56 -0700 Subject: [PATCH 03/10] format Change-Id: Ibfe397dc19d055867bb8c0e87f9ba107867e4289 --- samples/tuned_models.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/samples/tuned_models.py b/samples/tuned_models.py index 66656678a..df12903ac 100644 --- a/samples/tuned_models.py +++ b/samples/tuned_models.py @@ -25,7 +25,7 @@ class UnitTests(absltest.TestCase): @classmethod def setUpClass(cls): # Code to run once before all tests in the class - # [START tuned_models_create] + # [START tuned_models_create] import google.generativeai as genai import time @@ -74,9 +74,8 @@ def setUpClass(cls): result = model.generate_content("III") print(result.text) # IV # [END tuned_models_create] - - cls.tuned_model_name = tuned_model_name = tuned_model.name + cls.tuned_model_name = tuned_model_name = tuned_model.name def test_tuned_models_generate_content(self): # [START tuned_models_generate_content] From a152fe8b123435bd3ce173ae17802882c28c4f6a Mon Sep 17 00:00:00 2001 From: Mark Daoust Date: Tue, 15 Apr 2025 12:27:04 -0700 Subject: [PATCH 04/10] Fix shawoding caused by an empty GEMINI_API_KEY Change-Id: I1df220253baf1b2171891ba380a0a3dde8e09a8b --- google/generativeai/client.py | 4 ++-- tests/test_client.py | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/google/generativeai/client.py b/google/generativeai/client.py index c9c5c8c5b..113592594 100644 --- a/google/generativeai/client.py +++ b/google/generativeai/client.py @@ -185,12 +185,12 @@ def configure( "Invalid configuration: Please set either `api_key` or `client_options['api_key']`, but not both." ) else: - if api_key is None: + if not api_key: # If no key is provided explicitly, attempt to load one from the # environment. api_key = os.getenv("GEMINI_API_KEY") - if api_key is None: + if not api_key: # If the GEMINI_API_KEY doesn't exist, attempt to load the # GOOGLE_API_KEY from the environment. api_key = os.getenv("GOOGLE_API_KEY") diff --git a/tests/test_client.py b/tests/test_client.py index 9162c3d75..e6e4acfd4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -29,6 +29,18 @@ def test_api_key_passed_via_client_options(self): client_opts = client._client_manager.client_config["client_options"] self.assertEqual(client_opts.api_key, "AIzA_client_opts") + @mock.patch.dict(os.environ, {"GEMINI_API_KEY": "AIzA_env"}) + def test_api_key_from_environment(self): + # Default to API key loaded from environment. + client.configure() + client_opts = client._client_manager.client_config["client_options"] + self.assertEqual(client_opts.api_key, "AIzA_env") + + # But not when a key is provided explicitly. + client.configure(api_key="AIzA_client") + client_opts = client._client_manager.client_config["client_options"] + self.assertEqual(client_opts.api_key, "AIzA_client") + @mock.patch.dict(os.environ, {"GOOGLE_API_KEY": "AIzA_env"}) def test_api_key_from_environment(self): # Default to API key loaded from environment. @@ -41,6 +53,30 @@ def test_api_key_from_environment(self): client_opts = client._client_manager.client_config["client_options"] self.assertEqual(client_opts.api_key, "AIzA_client") + @mock.patch.dict(os.environ, {"GEMINI_API_KEY": "", "GOOGLE_API_KEY": "AIzA_env"}) + def test_empty_gemini_api_key_doesnt_shadow(self): + # Default to API key loaded from environment. + client.configure() + client_opts = client._client_manager.client_config["client_options"] + self.assertEqual(client_opts.api_key, "AIzA_env") + + # But not when a key is provided explicitly. + client.configure(api_key="AIzA_client") + client_opts = client._client_manager.client_config["client_options"] + self.assertEqual(client_opts.api_key, "AIzA_client") + + @mock.patch.dict(os.environ, {"GEMINI_API_KEY": "", "GOOGLE_API_KEY": "AIzA_env"}) + def test_empty_google_api_key_doesnt_shadow(self): + # Default to API key loaded from environment. + client.configure() + client_opts = client._client_manager.client_config["client_options"] + self.assertEqual(client_opts.api_key, "AIzA_env") + + # But not when a key is provided explicitly. + client.configure(api_key="AIzA_client") + client_opts = client._client_manager.client_config["client_options"] + self.assertEqual(client_opts.api_key, "AIzA_client") + def test_api_key_cannot_be_set_twice(self): client_opts = client_options.ClientOptions(api_key="AIzA_client_opts") From 0327a141853e2e3bad40fda6705c860a458d7197 Mon Sep 17 00:00:00 2001 From: Mark Daoust Date: Tue, 15 Apr 2025 12:44:05 -0700 Subject: [PATCH 05/10] fix failing tests Change-Id: I0fdfc084e8c5e88bab8257eef22065ec5a6189ea --- google/generativeai/types/content_types.py | 11 +++++++++++ tests/test_content.py | 3 +++ 2 files changed, 14 insertions(+) diff --git a/google/generativeai/types/content_types.py b/google/generativeai/types/content_types.py index f3db610e1..c1ceb8086 100644 --- a/google/generativeai/types/content_types.py +++ b/google/generativeai/types/content_types.py @@ -483,6 +483,17 @@ def strip_titles(schema): if items is not None: strip_titles(items) +def strip_additional_properties(schema): + schema.pop("additionalProperties", None) + + properties = schema.get("properties", None) + if properties is not None: + for name, value in properties.items(): + strip_additional_properties(value) + + items = schema.get("items", None) + if items is not None: + strip_additional_properties(items) def add_object_type(schema): properties = schema.get("properties", None) diff --git a/tests/test_content.py b/tests/test_content.py index 2031e40ae..913e4bef9 100644 --- a/tests/test_content.py +++ b/tests/test_content.py @@ -663,6 +663,9 @@ def test_auto_schema(self, annotation, expected): def fun(a: annotation): pass + if annotation == dict[str, Any]: + breakpoint() + cfd = content_types.FunctionDeclaration.from_function(fun) got = cfd.parameters.properties["a"] self.assertEqual(got, expected) From 31860840cbf2b3bdaa46c57aa9235f3314f5bc4e Mon Sep 17 00:00:00 2001 From: Mark Daoust Date: Tue, 15 Apr 2025 13:02:59 -0700 Subject: [PATCH 06/10] fix tests broken by pydantic 2.11 Change-Id: Ie697e2f43b2608cadebfd14d806f3898b064cc3b --- google/generativeai/responder.py | 417 --------------------- google/generativeai/types/content_types.py | 1 + tests/test_content.py | 3 - tests/test_responder.py | 255 ------------- 4 files changed, 1 insertion(+), 675 deletions(-) delete mode 100644 tests/test_responder.py diff --git a/google/generativeai/responder.py b/google/generativeai/responder.py index dd388c6a6..39fb9dd2f 100644 --- a/google/generativeai/responder.py +++ b/google/generativeai/responder.py @@ -66,96 +66,6 @@ def to_type(x: TypeOptions) -> Type: return _TYPE_TYPE[x] -def _generate_schema( - f: Callable[..., Any], - *, - descriptions: Mapping[str, str] | None = None, - required: Sequence[str] | None = None, -) -> dict[str, Any]: - """Generates the OpenAPI Schema for a python function. - - Args: - f: The function to generate an OpenAPI Schema for. - descriptions: Optional. A `{name: description}` mapping for annotating input - arguments of the function with user-provided descriptions. It - defaults to an empty dictionary (i.e. there will not be any - description for any of the inputs). - required: Optional. For the user to specify the set of required arguments in - function calls to `f`. If unspecified, it will be automatically - inferred from `f`. - - Returns: - dict[str, Any]: The OpenAPI Schema for the function `f` in JSON format. - """ - if descriptions is None: - descriptions = {} - if required is None: - required = [] - defaults = dict(inspect.signature(f).parameters) - fields_dict = { - name: ( - # 1. We infer the argument type here: use Any rather than None so - # it will not try to auto-infer the type based on the default value. - (param.annotation if param.annotation != inspect.Parameter.empty else Any), - pydantic.Field( - # 2. We do not support default values for now. - # default=( - # param.default if param.default != inspect.Parameter.empty - # else None - # ), - # 3. We support user-provided descriptions. - description=descriptions.get(name, None), - ), - ) - for name, param in defaults.items() - # We do not support *args or **kwargs - if param.kind - in ( - inspect.Parameter.POSITIONAL_OR_KEYWORD, - inspect.Parameter.KEYWORD_ONLY, - inspect.Parameter.POSITIONAL_ONLY, - ) - } - parameters = pydantic.create_model(f.__name__, **fields_dict).model_json_schema() - # Postprocessing - # 4. Suppress unnecessary title generation: - # * https://github.com/pydantic/pydantic/issues/1051 - # * http://cl/586221780 - parameters.pop("title", None) - for name, function_arg in parameters.get("properties", {}).items(): - function_arg.pop("title", None) - annotation = defaults[name].annotation - # 5. Nullable fields: - # * https://github.com/pydantic/pydantic/issues/1270 - # * https://stackoverflow.com/a/58841311 - # * https://github.com/pydantic/pydantic/discussions/4872 - if typing.get_origin(annotation) is typing.Union and type(None) in typing.get_args( - annotation - ): - function_arg["nullable"] = True - # 6. Annotate required fields. - if required: - # We use the user-provided "required" fields if specified. - parameters["required"] = required - else: - # Otherwise we infer it from the function signature. - parameters["required"] = [ - k - for k in defaults - if ( - defaults[k].default == inspect.Parameter.empty - and defaults[k].kind - in ( - inspect.Parameter.POSITIONAL_OR_KEYWORD, - inspect.Parameter.KEYWORD_ONLY, - inspect.Parameter.POSITIONAL_ONLY, - ) - ) - ] - schema = dict(name=f.__name__, description=f.__doc__, parameters=parameters) - return schema - - def _rename_schema_fields(schema: dict[str, Any]): if schema is None: return schema @@ -183,330 +93,3 @@ def _rename_schema_fields(schema: dict[str, Any]): return schema - -class FunctionDeclaration: - def __init__(self, *, name: str, description: str, parameters: dict[str, Any] | None = None): - """A class wrapping a `protos.FunctionDeclaration`, describes a function for `genai.GenerativeModel`'s `tools`.""" - self._proto = protos.FunctionDeclaration( - name=name, description=description, parameters=_rename_schema_fields(parameters) - ) - - @property - def name(self) -> str: - return self._proto.name - - @property - def description(self) -> str: - return self._proto.description - - @property - def parameters(self) -> protos.Schema: - return self._proto.parameters - - @classmethod - def from_proto(cls, proto) -> FunctionDeclaration: - self = cls(name="", description="", parameters={}) - self._proto = proto - return self - - def to_proto(self) -> protos.FunctionDeclaration: - return self._proto - - @staticmethod - def from_function(function: Callable[..., Any], descriptions: dict[str, str] | None = None): - """Builds a `CallableFunctionDeclaration` from a python function. - - The function should have type annotations. - - This method is able to generate the schema for arguments annotated with types: - - `AllowedTypes = float | int | str | list[AllowedTypes] | dict` - - This method does not yet build a schema for `TypedDict`, that would allow you to specify the dictionary - contents. But you can build these manually. - """ - - if descriptions is None: - descriptions = {} - - schema = _generate_schema(function, descriptions=descriptions) - - return CallableFunctionDeclaration(**schema, function=function) - - -StructType = dict[str, "ValueType"] -ValueType = Union[float, str, bool, StructType, list["ValueType"], None] - - -class CallableFunctionDeclaration(FunctionDeclaration): - """An extension of `FunctionDeclaration` that can be built from a Python function, and is callable. - - Note: The Python function must have type annotations. - """ - - def __init__( - self, - *, - name: str, - description: str, - parameters: dict[str, Any] | None = None, - function: Callable[..., Any], - ): - super().__init__(name=name, description=description, parameters=parameters) - self.function = function - - def __call__(self, fc: protos.FunctionCall) -> protos.FunctionResponse: - result = self.function(**fc.args) - if not isinstance(result, dict): - result = {"result": result} - return protos.FunctionResponse(name=fc.name, response=result) - - -FunctionDeclarationType = Union[ - FunctionDeclaration, - protos.FunctionDeclaration, - dict[str, Any], - Callable[..., Any], -] - - -def _make_function_declaration( - fun: FunctionDeclarationType, -) -> FunctionDeclaration | protos.FunctionDeclaration: - if isinstance(fun, (FunctionDeclaration, protos.FunctionDeclaration)): - return fun - elif isinstance(fun, dict): - if "function" in fun: - return CallableFunctionDeclaration(**fun) - else: - return FunctionDeclaration(**fun) - elif callable(fun): - return CallableFunctionDeclaration.from_function(fun) - else: - raise TypeError( - f"Invalid argument type: Expected an instance of `genai.FunctionDeclarationType`. Received type: {type(fun).__name__}.", - fun, - ) - - -def _encode_fd(fd: FunctionDeclaration | protos.FunctionDeclaration) -> protos.FunctionDeclaration: - if isinstance(fd, protos.FunctionDeclaration): - return fd - - return fd.to_proto() - - -class Tool: - """A wrapper for `protos.Tool`, Contains a collection of related `FunctionDeclaration` objects.""" - - def __init__(self, function_declarations: Iterable[FunctionDeclarationType]): - # The main path doesn't use this but is seems useful. - self._function_declarations = [_make_function_declaration(f) for f in function_declarations] - self._index = {} - for fd in self._function_declarations: - name = fd.name - if name in self._index: - raise ValueError("") - self._index[fd.name] = fd - - self._proto = protos.Tool( - function_declarations=[_encode_fd(fd) for fd in self._function_declarations] - ) - - @property - def function_declarations(self) -> list[FunctionDeclaration | protos.FunctionDeclaration]: - return self._function_declarations - - def __getitem__( - self, name: str | protos.FunctionCall - ) -> FunctionDeclaration | protos.FunctionDeclaration: - if not isinstance(name, str): - name = name.name - - return self._index[name] - - def __call__(self, fc: protos.FunctionCall) -> protos.FunctionResponse | None: - declaration = self[fc] - if not callable(declaration): - return None - - return declaration(fc) - - def to_proto(self): - return self._proto - - -class ToolDict(TypedDict): - function_declarations: list[FunctionDeclarationType] - - -ToolType = Union[ - Tool, protos.Tool, ToolDict, Iterable[FunctionDeclarationType], FunctionDeclarationType -] - - -def _make_tool(tool: ToolType) -> Tool: - if isinstance(tool, Tool): - return tool - elif isinstance(tool, protos.Tool): - return Tool(function_declarations=tool.function_declarations) - elif isinstance(tool, dict): - if "function_declarations" in tool: - return Tool(**tool) - else: - fd = tool - return Tool(function_declarations=[protos.FunctionDeclaration(**fd)]) - elif isinstance(tool, Iterable): - return Tool(function_declarations=tool) - else: - try: - return Tool(function_declarations=[tool]) - except Exception as e: - raise TypeError( - f"Invalid argument type: Expected an instance of `genai.ToolType`. Received type: {type(tool).__name__}.", - tool, - ) from e - - -class FunctionLibrary: - """A container for a set of `Tool` objects, manages lookup and execution of their functions.""" - - def __init__(self, tools: Iterable[ToolType]): - tools = _make_tools(tools) - self._tools = list(tools) - self._index = {} - for tool in self._tools: - for declaration in tool.function_declarations: - name = declaration.name - if name in self._index: - raise ValueError( - f"Invalid operation: A `FunctionDeclaration` named '{name}' is already defined. Each `FunctionDeclaration` must have a unique name." - ) - self._index[declaration.name] = declaration - - def __getitem__( - self, name: str | protos.FunctionCall - ) -> FunctionDeclaration | protos.FunctionDeclaration: - if not isinstance(name, str): - name = name.name - - return self._index[name] - - def __call__(self, fc: protos.FunctionCall) -> protos.Part | None: - declaration = self[fc] - if not callable(declaration): - return None - - response = declaration(fc) - return protos.Part(function_response=response) - - def to_proto(self): - return [tool.to_proto() for tool in self._tools] - - -ToolsType = Union[Iterable[ToolType], ToolType] - - -def _make_tools(tools: ToolsType) -> list[Tool]: - if isinstance(tools, Iterable) and not isinstance(tools, Mapping): - tools = [_make_tool(t) for t in tools] - if len(tools) > 1 and all(len(t.function_declarations) == 1 for t in tools): - # flatten into a single tool. - tools = [_make_tool([t.function_declarations[0] for t in tools])] - return tools - else: - tool = tools - return [_make_tool(tool)] - - -FunctionLibraryType = Union[FunctionLibrary, ToolsType] - - -def to_function_library(lib: FunctionLibraryType | None) -> FunctionLibrary | None: - if lib is None: - return lib - elif isinstance(lib, FunctionLibrary): - return lib - else: - return FunctionLibrary(tools=lib) - - -FunctionCallingMode = protos.FunctionCallingConfig.Mode - -# fmt: off -_FUNCTION_CALLING_MODE = { - 1: FunctionCallingMode.AUTO, - FunctionCallingMode.AUTO: FunctionCallingMode.AUTO, - "mode_auto": FunctionCallingMode.AUTO, - "auto": FunctionCallingMode.AUTO, - - 2: FunctionCallingMode.ANY, - FunctionCallingMode.ANY: FunctionCallingMode.ANY, - "mode_any": FunctionCallingMode.ANY, - "any": FunctionCallingMode.ANY, - - 3: FunctionCallingMode.NONE, - FunctionCallingMode.NONE: FunctionCallingMode.NONE, - "mode_none": FunctionCallingMode.NONE, - "none": FunctionCallingMode.NONE, -} -# fmt: on - -FunctionCallingModeType = Union[FunctionCallingMode, str, int] - - -def to_function_calling_mode(x: FunctionCallingModeType) -> FunctionCallingMode: - if isinstance(x, str): - x = x.lower() - return _FUNCTION_CALLING_MODE[x] - - -class FunctionCallingConfigDict(TypedDict): - mode: FunctionCallingModeType - allowed_function_names: list[str] - - -FunctionCallingConfigType = Union[ - FunctionCallingModeType, FunctionCallingConfigDict, protos.FunctionCallingConfig -] - - -def to_function_calling_config(obj: FunctionCallingConfigType) -> protos.FunctionCallingConfig: - if isinstance(obj, protos.FunctionCallingConfig): - return obj - elif isinstance(obj, (FunctionCallingMode, str, int)): - obj = {"mode": to_function_calling_mode(obj)} - elif isinstance(obj, dict): - obj = obj.copy() - mode = obj.pop("mode") - obj["mode"] = to_function_calling_mode(mode) - else: - raise TypeError( - "Invalid argument type: Could not convert input to `protos.FunctionCallingConfig`." - f" Received type: {type(obj).__name__}.", - obj, - ) - - return protos.FunctionCallingConfig(obj) - - -class ToolConfigDict: - function_calling_config: FunctionCallingConfigType - - -ToolConfigType = Union[ToolConfigDict, protos.ToolConfig] - - -def to_tool_config(obj: ToolConfigType) -> protos.ToolConfig: - if isinstance(obj, protos.ToolConfig): - return obj - elif isinstance(obj, dict): - fcc = obj.pop("function_calling_config") - fcc = to_function_calling_config(fcc) - obj["function_calling_config"] = fcc - return protos.ToolConfig(**obj) - else: - raise TypeError( - "Invalid argument type: Could not convert input to `protos.ToolConfig`. " - f"Received type: {type(obj).__name__}.", - ) diff --git a/google/generativeai/types/content_types.py b/google/generativeai/types/content_types.py index c1ceb8086..ec94a74ce 100644 --- a/google/generativeai/types/content_types.py +++ b/google/generativeai/types/content_types.py @@ -435,6 +435,7 @@ def _build_schema(fname, fields_dict): # * https://github.com/pydantic/pydantic/issues/1051 # * http://cl/586221780 strip_titles(parameters) + strip_additional_properties(parameters) return parameters diff --git a/tests/test_content.py b/tests/test_content.py index 913e4bef9..2031e40ae 100644 --- a/tests/test_content.py +++ b/tests/test_content.py @@ -663,9 +663,6 @@ def test_auto_schema(self, annotation, expected): def fun(a: annotation): pass - if annotation == dict[str, Any]: - breakpoint() - cfd = content_types.FunctionDeclaration.from_function(fun) got = cfd.parameters.properties["a"] self.assertEqual(got, expected) diff --git a/tests/test_responder.py b/tests/test_responder.py deleted file mode 100644 index d2818da8a..000000000 --- a/tests/test_responder.py +++ /dev/null @@ -1,255 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import pathlib -from typing import Any - -from absl.testing import absltest -from absl.testing import parameterized -from google.generativeai import protos -from google.generativeai import responder - - -HERE = pathlib.Path(__file__).parent -TEST_PNG_PATH = HERE / "test_img.png" -TEST_PNG_URL = "https://storage.googleapis.com/generativeai-downloads/data/test_img.png" -TEST_PNG_DATA = TEST_PNG_PATH.read_bytes() - -TEST_JPG_PATH = HERE / "test_img.jpg" -TEST_JPG_URL = "https://storage.googleapis.com/generativeai-downloads/data/test_img.jpg" -TEST_JPG_DATA = TEST_JPG_PATH.read_bytes() - - -# simple test function -def datetime(): - "Returns the current UTC date and time." - - -class UnitTests(parameterized.TestCase): - @parameterized.named_parameters( - [ - "FunctionLibrary", - responder.FunctionLibrary( - tools=protos.Tool( - function_declarations=[ - protos.FunctionDeclaration( - name="datetime", description="Returns the current UTC date and time." - ) - ] - ) - ), - ], - [ - "IterableTool-Tool", - [ - responder.Tool( - function_declarations=[ - protos.FunctionDeclaration( - name="datetime", description="Returns the current UTC date and time." - ) - ] - ) - ], - ], - [ - "IterableTool-protos.Tool", - [ - protos.Tool( - function_declarations=[ - protos.FunctionDeclaration( - name="datetime", - description="Returns the current UTC date and time.", - ) - ] - ) - ], - ], - [ - "IterableTool-ToolDict", - [ - dict( - function_declarations=[ - dict( - name="datetime", - description="Returns the current UTC date and time.", - ) - ] - ) - ], - ], - [ - "IterableTool-IterableFD", - [ - [ - protos.FunctionDeclaration( - name="datetime", - description="Returns the current UTC date and time.", - ) - ] - ], - ], - [ - "IterableTool-FD", - [ - protos.FunctionDeclaration( - name="datetime", - description="Returns the current UTC date and time.", - ) - ], - ], - [ - "Tool", - responder.Tool( - function_declarations=[ - protos.FunctionDeclaration( - name="datetime", description="Returns the current UTC date and time." - ) - ] - ), - ], - [ - "protos.Tool", - protos.Tool( - function_declarations=[ - protos.FunctionDeclaration( - name="datetime", description="Returns the current UTC date and time." - ) - ] - ), - ], - [ - "ToolDict", - dict( - function_declarations=[ - dict(name="datetime", description="Returns the current UTC date and time.") - ] - ), - ], - [ - "IterableFD-FD", - [ - responder.FunctionDeclaration( - name="datetime", description="Returns the current UTC date and time." - ) - ], - ], - [ - "IterableFD-CFD", - [ - responder.CallableFunctionDeclaration( - name="datetime", - description="Returns the current UTC date and time.", - function=datetime, - ) - ], - ], - [ - "IterableFD-dict", - [dict(name="datetime", description="Returns the current UTC date and time.")], - ], - ["IterableFD-Callable", [datetime]], - [ - "FD", - responder.FunctionDeclaration( - name="datetime", description="Returns the current UTC date and time." - ), - ], - [ - "CFD", - responder.CallableFunctionDeclaration( - name="datetime", - description="Returns the current UTC date and time.", - function=datetime, - ), - ], - [ - "protos.FD", - protos.FunctionDeclaration( - name="datetime", description="Returns the current UTC date and time." - ), - ], - ["dict", dict(name="datetime", description="Returns the current UTC date and time.")], - ["Callable", datetime], - ) - def test_to_tools(self, tools): - function_library = responder.to_function_library(tools) - if function_library is None: - raise ValueError("This shouldn't happen") - tools = function_library.to_proto() - - tools = type(tools[0]).to_dict(tools[0]) - tools["function_declarations"][0].pop("parameters", None) - - expected = dict( - function_declarations=[ - dict(name="datetime", description="Returns the current UTC date and time.") - ] - ) - - self.assertEqual(tools, expected) - - def test_two_fun_is_one_tool(self): - def a(): - pass - - def b(): - pass - - function_library = responder.to_function_library([a, b]) - if function_library is None: - raise ValueError("This shouldn't happen") - tools = function_library.to_proto() - - self.assertLen(tools, 1) - self.assertLen(tools[0].function_declarations, 2) - - @parameterized.named_parameters( - ["int", int, protos.Schema(type=protos.Type.INTEGER)], - ["float", float, protos.Schema(type=protos.Type.NUMBER)], - ["str", str, protos.Schema(type=protos.Type.STRING)], - [ - "list", - list[str], - protos.Schema( - type=protos.Type.ARRAY, - items=protos.Schema(type=protos.Type.STRING), - ), - ], - [ - "list-list-int", - list[list[int]], - protos.Schema( - type=protos.Type.ARRAY, - items=protos.Schema( - protos.Schema( - type=protos.Type.ARRAY, - items=protos.Schema(type=protos.Type.INTEGER), - ), - ), - ), - ], - ["dict", dict, protos.Schema(type=protos.Type.OBJECT)], - ["dict-str-any", dict[str, Any], protos.Schema(type=protos.Type.OBJECT)], - ) - def test_auto_schema(self, annotation, expected): - def fun(a: annotation): - pass - - cfd = responder.FunctionDeclaration.from_function(fun) - got = cfd.parameters.properties["a"] - self.assertEqual(got, expected) - - -if __name__ == "__main__": - absltest.main() From 40019a91d69a57c6c74d461c369eb2047579e99d Mon Sep 17 00:00:00 2001 From: Mark Daoust Date: Tue, 15 Apr 2025 13:08:25 -0700 Subject: [PATCH 07/10] format Change-Id: I51110cf338bd61d8cf40b3e73ece12b34c951d0c --- google/generativeai/responder.py | 1 - google/generativeai/types/content_types.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/google/generativeai/responder.py b/google/generativeai/responder.py index 39fb9dd2f..defa5bb3e 100644 --- a/google/generativeai/responder.py +++ b/google/generativeai/responder.py @@ -92,4 +92,3 @@ def _rename_schema_fields(schema: dict[str, Any]): schema["properties"] = {k: _rename_schema_fields(v) for k, v in properties.items()} return schema - diff --git a/google/generativeai/types/content_types.py b/google/generativeai/types/content_types.py index ec94a74ce..80f60d2b2 100644 --- a/google/generativeai/types/content_types.py +++ b/google/generativeai/types/content_types.py @@ -484,6 +484,7 @@ def strip_titles(schema): if items is not None: strip_titles(items) + def strip_additional_properties(schema): schema.pop("additionalProperties", None) @@ -496,6 +497,7 @@ def strip_additional_properties(schema): if items is not None: strip_additional_properties(items) + def add_object_type(schema): properties = schema.get("properties", None) if properties is not None: From 8aadb4d034b7b128109ad9de851b698fbf5c96ef Mon Sep 17 00:00:00 2001 From: Mark Daoust Date: Tue, 15 Apr 2025 14:45:46 -0700 Subject: [PATCH 08/10] sync readme with https://github.com/google-gemini/deprecated-generative-ai-js/pull/462 Change-Id: I6bc43a471b5558f373f6c514a9dc56a49dd9e677 --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 23f2308f6..93fa3c55e 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,20 @@ With Gemini 2.0, we took the chance to create a single unified SDK for all devel The full migration guide from the old SDK to new SDK is available in the [Gemini API docs](https://ai.google.dev/gemini-api/docs/migrate). -We won't be adding anything to this SDK or making any further changes. The Gemini API docs are fully updated to show examples of the new Google Gen AI SDK. We know how disruptive an SDK change can be and don't take this change lightly, but our goal is to create an extremely simple and clear path for developers to build with our models so it felt necessary to make this change. +The Gemini API docs are fully updated to show examples of the new Google Gen AI SDK. We know how disruptive an SDK change can be and don't take this change lightly, but our goal is to create an extremely simple and clear path for developers to build with our models so it felt necessary to make this change. Thank you for building with Gemini and [let us know](https://discuss.ai.google.dev/c/gemini-api/4) if you need any help! +**Please be advised that this repository is now considered legacy.** For the latest features, performance improvements, and active development, we strongly recommend migrating to the official **[Google Generative AI SDK for JavaScript](https://github.com/googleapis/python-genai)**. + +**Support Plan for this Repository:** + +* **Limited Maintenance:** Development is now restricted to **critical bug fixes only**. No new features will be added. +* **Purpose:** This limited support aims to provide stability for users while they transition to the new SDK. +* **End-of-Life Date:** All support for this repository (including bug fixes) will permanently end on **August 31st, 2025**. + +We encourage all users to begin planning their migration to the [Google Generative AI SDK](https://github.com/googleapis/python-genai) to ensure continued access to the latest capabilities and support. +