From 0ceaf6096e344d501b71da14c672c8fab0ee2c74 Mon Sep 17 00:00:00 2001 From: liweiguang Date: Tue, 3 Mar 2026 01:23:30 +0800 Subject: [PATCH 1/5] Python: fix Google AI/Vertex AI crash on anyOf schema (fixes #12442) The Google AI protobuf Schema does not support anyOf/oneOf fields or type-as-array. When ChatCompletionAgent instances are used as plugins, their parameter schemas (e.g. str | list[str]) include anyOf which causes ValueError during protobuf conversion. Add sanitize_schema_for_google_ai() to shared_utils that recursively rewrites anyOf/oneOf/type-array into nullable + single-type format. Apply it in both Google AI and Vertex AI function call format converters. Co-Authored-By: Claude Opus 4.6 --- .../ai/google/google_ai/services/utils.py | 20 +-- .../connectors/ai/google/shared_utils.py | 63 ++++++++++ .../ai/google/vertex_ai/services/utils.py | 7 +- .../connectors/ai/google/test_shared_utils.py | 117 ++++++++++++++++++ 4 files changed, 199 insertions(+), 8 deletions(-) diff --git a/python/semantic_kernel/connectors/ai/google/google_ai/services/utils.py b/python/semantic_kernel/connectors/ai/google/google_ai/services/utils.py index 10963cb24127..ca4381a8f3b8 100644 --- a/python/semantic_kernel/connectors/ai/google/google_ai/services/utils.py +++ b/python/semantic_kernel/connectors/ai/google/google_ai/services/utils.py @@ -13,6 +13,7 @@ from semantic_kernel.connectors.ai.google.shared_utils import ( FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE, GEMINI_FUNCTION_NAME_SEPARATOR, + sanitize_schema_for_google_ai, ) from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.function_call_content import FunctionCallContent @@ -135,16 +136,21 @@ def format_tool_message(message: ChatMessageContent) -> list[Part]: def kernel_function_metadata_to_google_ai_function_call_format(metadata: KernelFunctionMetadata) -> dict[str, Any]: """Convert the kernel function metadata to function calling format.""" - return { - "name": metadata.custom_fully_qualified_name(GEMINI_FUNCTION_NAME_SEPARATOR), - "description": metadata.description or "", - "parameters": { + parameters: dict[str, Any] | None = None + if metadata.parameters: + properties = {} + for param in metadata.parameters: + prop_schema = sanitize_schema_for_google_ai(param.schema_data) if param.schema_data else param.schema_data + properties[param.name] = prop_schema + parameters = { "type": "object", - "properties": {param.name: param.schema_data for param in metadata.parameters}, + "properties": properties, "required": [p.name for p in metadata.parameters if p.is_required], } - if metadata.parameters - else None, + return { + "name": metadata.custom_fully_qualified_name(GEMINI_FUNCTION_NAME_SEPARATOR), + "description": metadata.description or "", + "parameters": parameters, } diff --git a/python/semantic_kernel/connectors/ai/google/shared_utils.py b/python/semantic_kernel/connectors/ai/google/shared_utils.py index 468bfc38ae57..6b9370b3e1b8 100644 --- a/python/semantic_kernel/connectors/ai/google/shared_utils.py +++ b/python/semantic_kernel/connectors/ai/google/shared_utils.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft. All rights reserved. import logging +from copy import deepcopy +from typing import Any from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceType from semantic_kernel.const import DEFAULT_FULLY_QUALIFIED_NAME_SEPARATOR @@ -51,6 +53,67 @@ def format_gemini_function_name_to_kernel_function_fully_qualified_name(gemini_f return gemini_function_name +def sanitize_schema_for_google_ai(schema: dict[str, Any] | None) -> dict[str, Any] | None: + """Sanitize a JSON schema dict so it is compatible with Google AI / Vertex AI. + + The Google AI protobuf ``Schema`` does not support ``anyOf``, ``oneOf``, or + ``allOf``. It also does not accept ``type`` as an array (e.g. + ``["string", "null"]``). This helper recursively rewrites those constructs + into the subset that Google AI understands, using ``nullable`` where + appropriate. + """ + if schema is None: + return None + + schema = deepcopy(schema) + return _sanitize_node(schema) + + +def _sanitize_node(node: dict[str, Any]) -> dict[str, Any]: + """Recursively sanitize a single schema node.""" + # --- handle ``type`` given as a list (e.g. ["string", "null"]) --- + type_val = node.get("type") + if isinstance(type_val, list): + non_null = [t for t in type_val if t != "null"] + if len(type_val) != len(non_null): + node["nullable"] = True + node["type"] = non_null[0] if non_null else "string" + + # --- handle ``anyOf`` / ``oneOf`` --- + for key in ("anyOf", "oneOf"): + variants = node.get(key) + if not variants: + continue + non_null = [v for v in variants if v.get("type") != "null"] + has_null = len(variants) != len(non_null) + if non_null: + chosen = _sanitize_node(non_null[0]) + else: + chosen = {"type": "string"} + # Preserve description from the outer node + desc = node.get("description") + node.clear() + node.update(chosen) + if has_null: + node["nullable"] = True + if desc and "description" not in node: + node["description"] = desc + break # only process the first matching key + + # --- recurse into nested structures --- + props = node.get("properties") + if isinstance(props, dict): + for prop_name, prop_schema in props.items(): + if isinstance(prop_schema, dict): + props[prop_name] = _sanitize_node(prop_schema) + + items = node.get("items") + if isinstance(items, dict): + node["items"] = _sanitize_node(items) + + return node + + def collapse_function_call_results_in_chat_history(chat_history: ChatHistory): """The Gemini API expects the results of parallel function calls to be contained in a single message to be returned. diff --git a/python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py b/python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py index bad9fab5da70..eb4a0663cfcf 100644 --- a/python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py +++ b/python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py @@ -11,6 +11,7 @@ from semantic_kernel.connectors.ai.google.shared_utils import ( FUNCTION_CHOICE_TYPE_TO_GOOGLE_FUNCTION_CALLING_MODE, GEMINI_FUNCTION_NAME_SEPARATOR, + sanitize_schema_for_google_ai, ) from semantic_kernel.connectors.ai.google.vertex_ai.vertex_ai_prompt_execution_settings import ( VertexAIChatPromptExecutionSettings, @@ -135,12 +136,16 @@ def format_tool_message(message: ChatMessageContent) -> list[Part]: def kernel_function_metadata_to_vertex_ai_function_call_format(metadata: KernelFunctionMetadata) -> FunctionDeclaration: """Convert the kernel function metadata to function calling format.""" + properties = {} + for param in metadata.parameters: + prop_schema = sanitize_schema_for_google_ai(param.schema_data) if param.schema_data else param.schema_data + properties[param.name] = prop_schema return FunctionDeclaration( name=metadata.custom_fully_qualified_name(GEMINI_FUNCTION_NAME_SEPARATOR), description=metadata.description or "", parameters={ "type": "object", - "properties": {param.name: param.schema_data for param in metadata.parameters}, + "properties": properties, "required": [p.name for p in metadata.parameters if p.is_required], }, ) diff --git a/python/tests/unit/connectors/ai/google/test_shared_utils.py b/python/tests/unit/connectors/ai/google/test_shared_utils.py index f372684b3f09..3d42c123b3e5 100644 --- a/python/tests/unit/connectors/ai/google/test_shared_utils.py +++ b/python/tests/unit/connectors/ai/google/test_shared_utils.py @@ -10,6 +10,7 @@ collapse_function_call_results_in_chat_history, filter_system_message, format_gemini_function_name_to_kernel_function_fully_qualified_name, + sanitize_schema_for_google_ai, ) from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.contents.chat_message_content import ChatMessageContent @@ -94,3 +95,119 @@ def test_collapse_function_call_results_in_chat_history() -> None: collapse_function_call_results_in_chat_history(chat_history) assert len(chat_history.messages) == 7 assert len(chat_history.messages[1].items) == 2 + + +# --- sanitize_schema_for_google_ai tests --- + + +def test_sanitize_schema_none(): + assert sanitize_schema_for_google_ai(None) is None + + +def test_sanitize_schema_simple_passthrough(): + schema = {"type": "string", "description": "A name"} + result = sanitize_schema_for_google_ai(schema) + assert result == {"type": "string", "description": "A name"} + + +def test_sanitize_schema_type_as_list_with_null(): + """type: ["string", "null"] should become type: "string" + nullable: true.""" + schema = {"type": ["string", "null"], "description": "Optional field"} + result = sanitize_schema_for_google_ai(schema) + assert result == {"type": "string", "nullable": True, "description": "Optional field"} + + +def test_sanitize_schema_type_as_list_without_null(): + """type: ["string", "integer"] should pick the first type.""" + schema = {"type": ["string", "integer"]} + result = sanitize_schema_for_google_ai(schema) + assert result == {"type": "string"} + + +def test_sanitize_schema_anyof_with_null(): + """anyOf with null variant should become the non-null type + nullable.""" + schema = { + "anyOf": [{"type": "string"}, {"type": "null"}], + "description": "Optional param", + } + result = sanitize_schema_for_google_ai(schema) + assert result == {"type": "string", "nullable": True, "description": "Optional param"} + + +def test_sanitize_schema_anyof_without_null(): + """anyOf without null should pick the first variant.""" + schema = { + "anyOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}}, + ], + } + result = sanitize_schema_for_google_ai(schema) + assert result == {"type": "string"} + + +def test_sanitize_schema_oneof(): + """oneOf should be handled the same as anyOf.""" + schema = { + "oneOf": [{"type": "integer"}, {"type": "null"}], + } + result = sanitize_schema_for_google_ai(schema) + assert result == {"type": "integer", "nullable": True} + + +def test_sanitize_schema_nested_properties(): + """anyOf inside nested properties should be sanitized recursively.""" + schema = { + "type": "object", + "properties": { + "name": {"type": "string"}, + "value": {"anyOf": [{"type": "number"}, {"type": "null"}]}, + }, + } + result = sanitize_schema_for_google_ai(schema) + assert result == { + "type": "object", + "properties": { + "name": {"type": "string"}, + "value": {"type": "number", "nullable": True}, + }, + } + + +def test_sanitize_schema_nested_items(): + """anyOf inside array items should be sanitized recursively.""" + schema = { + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + } + result = sanitize_schema_for_google_ai(schema) + assert result == { + "type": "array", + "items": {"type": "string"}, + } + + +def test_sanitize_schema_does_not_mutate_original(): + """The original schema dict should not be modified.""" + schema = { + "anyOf": [{"type": "string"}, {"type": "null"}], + "description": "test", + } + original = {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "test"} + sanitize_schema_for_google_ai(schema) + assert schema == original + + +def test_sanitize_schema_agent_messages_param(): + """Reproducer for issue #12442: str | list[str] parameter schema.""" + schema = { + "anyOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}}, + ], + "description": "The user messages for the agent.", + } + result = sanitize_schema_for_google_ai(schema) + assert "anyOf" not in result + assert result["type"] == "string" + assert result["description"] == "The user messages for the agent." From 4833b675605eb216b349f109a73089ed332b2342 Mon Sep 17 00:00:00 2001 From: OiPunk Date: Thu, 5 Mar 2026 12:37:14 +0800 Subject: [PATCH 2/5] Address review feedback: add allOf handling, align Vertex AI empty params, add integration tests - Add "allOf" to the sanitizer loop alongside anyOf/oneOf so allOf schemas are handled instead of passing through unsanitized - Guard Vertex AI parameters construction when metadata.parameters is empty, aligning behavior with the Google AI version - Add allOf unit tests (with and without null variants) - Add edge-case tests: all-null type list, all-null anyOf, variant with its own description - Add integration tests for both Google AI and Vertex AI format functions to prove end-to-end sanitization --- .../connectors/ai/google/shared_utils.py | 4 +- .../ai/google/vertex_ai/services/utils.py | 11 ++-- .../services/test_google_ai_utils.py | 42 +++++++++++++++ .../connectors/ai/google/test_shared_utils.py | 53 +++++++++++++++++++ .../services/test_vertex_ai_utils.py | 42 ++++++++++++++- 5 files changed, 144 insertions(+), 8 deletions(-) diff --git a/python/semantic_kernel/connectors/ai/google/shared_utils.py b/python/semantic_kernel/connectors/ai/google/shared_utils.py index 6b9370b3e1b8..b37929ffd11e 100644 --- a/python/semantic_kernel/connectors/ai/google/shared_utils.py +++ b/python/semantic_kernel/connectors/ai/google/shared_utils.py @@ -79,8 +79,8 @@ def _sanitize_node(node: dict[str, Any]) -> dict[str, Any]: node["nullable"] = True node["type"] = non_null[0] if non_null else "string" - # --- handle ``anyOf`` / ``oneOf`` --- - for key in ("anyOf", "oneOf"): + # --- handle ``anyOf`` / ``oneOf`` / ``allOf`` --- + for key in ("anyOf", "oneOf", "allOf"): variants = node.get(key) if not variants: continue diff --git a/python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py b/python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py index eb4a0663cfcf..e6c92f867b5a 100644 --- a/python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py +++ b/python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py @@ -2,7 +2,7 @@ import json import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from google.cloud.aiplatform_v1beta1.types.content import Candidate from vertexai.generative_models import FunctionDeclaration, Part, Tool, ToolConfig @@ -136,10 +136,11 @@ def format_tool_message(message: ChatMessageContent) -> list[Part]: def kernel_function_metadata_to_vertex_ai_function_call_format(metadata: KernelFunctionMetadata) -> FunctionDeclaration: """Convert the kernel function metadata to function calling format.""" - properties = {} - for param in metadata.parameters: - prop_schema = sanitize_schema_for_google_ai(param.schema_data) if param.schema_data else param.schema_data - properties[param.name] = prop_schema + properties: dict[str, Any] = {} + if metadata.parameters: + for param in metadata.parameters: + prop_schema = sanitize_schema_for_google_ai(param.schema_data) if param.schema_data else param.schema_data + properties[param.name] = prop_schema return FunctionDeclaration( name=metadata.custom_fully_qualified_name(GEMINI_FUNCTION_NAME_SEPARATOR), description=metadata.description or "", diff --git a/python/tests/unit/connectors/ai/google/google_ai/services/test_google_ai_utils.py b/python/tests/unit/connectors/ai/google/google_ai/services/test_google_ai_utils.py index 0f73472466de..d618871eb154 100644 --- a/python/tests/unit/connectors/ai/google/google_ai/services/test_google_ai_utils.py +++ b/python/tests/unit/connectors/ai/google/google_ai/services/test_google_ai_utils.py @@ -7,6 +7,7 @@ finish_reason_from_google_ai_to_semantic_kernel, format_assistant_message, format_user_message, + kernel_function_metadata_to_google_ai_function_call_format, ) from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.function_call_content import FunctionCallContent @@ -16,6 +17,8 @@ from semantic_kernel.contents.utils.author_role import AuthorRole from semantic_kernel.contents.utils.finish_reason import FinishReason as SemanticKernelFinishReason from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError +from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata +from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata def test_finish_reason_from_google_ai_to_semantic_kernel(): @@ -113,3 +116,42 @@ def test_format_assistant_message_with_unsupported_items() -> None: with pytest.raises(ServiceInvalidRequestError): format_assistant_message(assistant_message) + + +def test_google_ai_function_call_format_sanitizes_anyof_schema() -> None: + """Integration test: anyOf in param schema_data is sanitized in the output dict.""" + metadata = KernelFunctionMetadata( + name="test_func", + description="A test function", + is_prompt=False, + parameters=[ + KernelParameterMetadata( + name="messages", + description="The user messages", + is_required=True, + schema_data={ + "anyOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}}, + ], + "description": "The user messages", + }, + ), + ], + ) + result = kernel_function_metadata_to_google_ai_function_call_format(metadata) + param_schema = result["parameters"]["properties"]["messages"] + assert "anyOf" not in param_schema + assert param_schema["type"] == "string" + + +def test_google_ai_function_call_format_empty_parameters() -> None: + """Integration test: metadata with no parameters produces parameters=None.""" + metadata = KernelFunctionMetadata( + name="no_params_func", + description="No parameters", + is_prompt=False, + parameters=[], + ) + result = kernel_function_metadata_to_google_ai_function_call_format(metadata) + assert result["parameters"] is None diff --git a/python/tests/unit/connectors/ai/google/test_shared_utils.py b/python/tests/unit/connectors/ai/google/test_shared_utils.py index 3d42c123b3e5..e02f8f911e89 100644 --- a/python/tests/unit/connectors/ai/google/test_shared_utils.py +++ b/python/tests/unit/connectors/ai/google/test_shared_utils.py @@ -211,3 +211,56 @@ def test_sanitize_schema_agent_messages_param(): assert "anyOf" not in result assert result["type"] == "string" assert result["description"] == "The user messages for the agent." + + +def test_sanitize_schema_allof(): + """allOf should be handled like anyOf/oneOf, picking the first variant.""" + schema = { + "allOf": [ + {"type": "object", "properties": {"name": {"type": "string"}}}, + {"type": "object", "properties": {"age": {"type": "integer"}}}, + ], + } + result = sanitize_schema_for_google_ai(schema) + assert "allOf" not in result + assert result["type"] == "object" + assert "name" in result["properties"] + + +def test_sanitize_schema_allof_with_null(): + """allOf with a null variant should produce nullable: true.""" + schema = { + "allOf": [{"type": "string"}, {"type": "null"}], + } + result = sanitize_schema_for_google_ai(schema) + assert "allOf" not in result + assert result["type"] == "string" + assert result["nullable"] is True + + +def test_sanitize_schema_all_null_type_list(): + """type: ["null"] should fall back to type: "string" + nullable: true.""" + schema = {"type": ["null"]} + result = sanitize_schema_for_google_ai(schema) + assert result == {"type": "string", "nullable": True} + + +def test_sanitize_schema_all_null_anyof(): + """anyOf where all variants are null should fall back to type: "string".""" + schema = {"anyOf": [{"type": "null"}]} + result = sanitize_schema_for_google_ai(schema) + assert result == {"type": "string", "nullable": True} + + +def test_sanitize_schema_chosen_variant_keeps_own_description(): + """When the chosen anyOf variant has its own description, do not overwrite it.""" + schema = { + "anyOf": [ + {"type": "string", "description": "inner desc"}, + {"type": "null"}, + ], + "description": "outer desc", + } + result = sanitize_schema_for_google_ai(schema) + assert result["description"] == "inner desc" + assert result["nullable"] is True diff --git a/python/tests/unit/connectors/ai/google/vertex_ai/services/test_vertex_ai_utils.py b/python/tests/unit/connectors/ai/google/vertex_ai/services/test_vertex_ai_utils.py index 510b03379ea2..88d582d94c90 100644 --- a/python/tests/unit/connectors/ai/google/vertex_ai/services/test_vertex_ai_utils.py +++ b/python/tests/unit/connectors/ai/google/vertex_ai/services/test_vertex_ai_utils.py @@ -2,12 +2,13 @@ import pytest from google.cloud.aiplatform_v1beta1.types.content import Candidate -from vertexai.generative_models import Part +from vertexai.generative_models import FunctionDeclaration, Part from semantic_kernel.connectors.ai.google.vertex_ai.services.utils import ( finish_reason_from_vertex_ai_to_semantic_kernel, format_assistant_message, format_user_message, + kernel_function_metadata_to_vertex_ai_function_call_format, ) from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.function_call_content import FunctionCallContent @@ -17,6 +18,8 @@ from semantic_kernel.contents.utils.author_role import AuthorRole from semantic_kernel.contents.utils.finish_reason import FinishReason from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError +from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata +from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata def test_finish_reason_from_vertex_ai_to_semantic_kernel(): @@ -110,3 +113,40 @@ def test_format_assistant_message_with_unsupported_items() -> None: with pytest.raises(ServiceInvalidRequestError): format_assistant_message(assistant_message) + + +def test_vertex_ai_function_call_format_sanitizes_anyof_schema() -> None: + """Integration test: anyOf in param schema_data is sanitized in the FunctionDeclaration.""" + metadata = KernelFunctionMetadata( + name="test_func", + description="A test function", + is_prompt=False, + parameters=[ + KernelParameterMetadata( + name="messages", + description="The user messages", + is_required=True, + schema_data={ + "anyOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}}, + ], + "description": "The user messages", + }, + ), + ], + ) + result = kernel_function_metadata_to_vertex_ai_function_call_format(metadata) + assert isinstance(result, FunctionDeclaration) + + +def test_vertex_ai_function_call_format_empty_parameters() -> None: + """Integration test: metadata with no parameters produces empty properties, no crash.""" + metadata = KernelFunctionMetadata( + name="no_params_func", + description="No parameters", + is_prompt=False, + parameters=[], + ) + result = kernel_function_metadata_to_vertex_ai_function_call_format(metadata) + assert isinstance(result, FunctionDeclaration) From 8288f4151ee275e7caf33cd77defeeec8fa4cfa4 Mon Sep 17 00:00:00 2001 From: OiPunk Date: Mon, 9 Mar 2026 11:39:46 +0800 Subject: [PATCH 3/5] Fix ruff SIM108 lint: use ternary for if-else assignment Co-Authored-By: Claude Opus 4.6 --- python/semantic_kernel/connectors/ai/google/shared_utils.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/python/semantic_kernel/connectors/ai/google/shared_utils.py b/python/semantic_kernel/connectors/ai/google/shared_utils.py index b37929ffd11e..1ff2d73c8342 100644 --- a/python/semantic_kernel/connectors/ai/google/shared_utils.py +++ b/python/semantic_kernel/connectors/ai/google/shared_utils.py @@ -86,10 +86,7 @@ def _sanitize_node(node: dict[str, Any]) -> dict[str, Any]: continue non_null = [v for v in variants if v.get("type") != "null"] has_null = len(variants) != len(non_null) - if non_null: - chosen = _sanitize_node(non_null[0]) - else: - chosen = {"type": "string"} + chosen = _sanitize_node(non_null[0]) if non_null else {"type": "string"} # Preserve description from the outer node desc = node.get("description") node.clear() From c9b7d4d1807e279dc6112aebbbef42d1f2115c2b Mon Sep 17 00:00:00 2001 From: OiPunk Date: Mon, 9 Mar 2026 12:56:21 +0800 Subject: [PATCH 4/5] Fix pre-commit linting: capitalize docstrings and add missing docstrings - Capitalize first word of docstrings (D403): anyOf->AnyOf, oneOf->OneOf, allOf->AllOf - Add missing docstrings to test_sanitize_schema_none and test_sanitize_schema_simple_passthrough (D103) Co-Authored-By: Claude Opus 4.6 --- .../connectors/ai/google/test_shared_utils.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/python/tests/unit/connectors/ai/google/test_shared_utils.py b/python/tests/unit/connectors/ai/google/test_shared_utils.py index e02f8f911e89..43c38323f90a 100644 --- a/python/tests/unit/connectors/ai/google/test_shared_utils.py +++ b/python/tests/unit/connectors/ai/google/test_shared_utils.py @@ -101,10 +101,12 @@ def test_collapse_function_call_results_in_chat_history() -> None: def test_sanitize_schema_none(): + """Test that None input returns None.""" assert sanitize_schema_for_google_ai(None) is None def test_sanitize_schema_simple_passthrough(): + """Test that a simple schema passes through unchanged.""" schema = {"type": "string", "description": "A name"} result = sanitize_schema_for_google_ai(schema) assert result == {"type": "string", "description": "A name"} @@ -125,7 +127,7 @@ def test_sanitize_schema_type_as_list_without_null(): def test_sanitize_schema_anyof_with_null(): - """anyOf with null variant should become the non-null type + nullable.""" + """AnyOf with null variant should become the non-null type + nullable.""" schema = { "anyOf": [{"type": "string"}, {"type": "null"}], "description": "Optional param", @@ -135,7 +137,7 @@ def test_sanitize_schema_anyof_with_null(): def test_sanitize_schema_anyof_without_null(): - """anyOf without null should pick the first variant.""" + """AnyOf without null should pick the first variant.""" schema = { "anyOf": [ {"type": "string"}, @@ -147,7 +149,7 @@ def test_sanitize_schema_anyof_without_null(): def test_sanitize_schema_oneof(): - """oneOf should be handled the same as anyOf.""" + """OneOf should be handled the same as anyOf.""" schema = { "oneOf": [{"type": "integer"}, {"type": "null"}], } @@ -156,7 +158,7 @@ def test_sanitize_schema_oneof(): def test_sanitize_schema_nested_properties(): - """anyOf inside nested properties should be sanitized recursively.""" + """AnyOf inside nested properties should be sanitized recursively.""" schema = { "type": "object", "properties": { @@ -175,7 +177,7 @@ def test_sanitize_schema_nested_properties(): def test_sanitize_schema_nested_items(): - """anyOf inside array items should be sanitized recursively.""" + """AnyOf inside array items should be sanitized recursively.""" schema = { "type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, @@ -214,7 +216,7 @@ def test_sanitize_schema_agent_messages_param(): def test_sanitize_schema_allof(): - """allOf should be handled like anyOf/oneOf, picking the first variant.""" + """AllOf should be handled like anyOf/oneOf, picking the first variant.""" schema = { "allOf": [ {"type": "object", "properties": {"name": {"type": "string"}}}, @@ -228,7 +230,7 @@ def test_sanitize_schema_allof(): def test_sanitize_schema_allof_with_null(): - """allOf with a null variant should produce nullable: true.""" + """AllOf with a null variant should produce nullable: true.""" schema = { "allOf": [{"type": "string"}, {"type": "null"}], } @@ -246,7 +248,7 @@ def test_sanitize_schema_all_null_type_list(): def test_sanitize_schema_all_null_anyof(): - """anyOf where all variants are null should fall back to type: "string".""" + """AnyOf where all variants are null should fall back to type: "string".""" schema = {"anyOf": [{"type": "null"}]} result = sanitize_schema_for_google_ai(schema) assert result == {"type": "string", "nullable": True} From 92ea01b218e8a231656c505ba737ca199b720887 Mon Sep 17 00:00:00 2001 From: OiPunk Date: Mon, 9 Mar 2026 20:38:04 +0800 Subject: [PATCH 5/5] Fix mypy type error: handle None parameter names Fix type checking error where param.name could be None when used as dict key. Added None checks before using param.name in both Google AI and Vertex AI utils to satisfy mypy strict type requirements. Co-Authored-By: Claude Opus 4.6 --- .../connectors/ai/google/google_ai/services/utils.py | 4 +++- .../connectors/ai/google/vertex_ai/services/utils.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/python/semantic_kernel/connectors/ai/google/google_ai/services/utils.py b/python/semantic_kernel/connectors/ai/google/google_ai/services/utils.py index ca4381a8f3b8..3f69cf380239 100644 --- a/python/semantic_kernel/connectors/ai/google/google_ai/services/utils.py +++ b/python/semantic_kernel/connectors/ai/google/google_ai/services/utils.py @@ -140,12 +140,14 @@ def kernel_function_metadata_to_google_ai_function_call_format(metadata: KernelF if metadata.parameters: properties = {} for param in metadata.parameters: + if param.name is None: + continue prop_schema = sanitize_schema_for_google_ai(param.schema_data) if param.schema_data else param.schema_data properties[param.name] = prop_schema parameters = { "type": "object", "properties": properties, - "required": [p.name for p in metadata.parameters if p.is_required], + "required": [p.name for p in metadata.parameters if p.is_required and p.name is not None], } return { "name": metadata.custom_fully_qualified_name(GEMINI_FUNCTION_NAME_SEPARATOR), diff --git a/python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py b/python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py index e6c92f867b5a..19784b5a4bf6 100644 --- a/python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py +++ b/python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py @@ -139,6 +139,8 @@ def kernel_function_metadata_to_vertex_ai_function_call_format(metadata: KernelF properties: dict[str, Any] = {} if metadata.parameters: for param in metadata.parameters: + if param.name is None: + continue prop_schema = sanitize_schema_for_google_ai(param.schema_data) if param.schema_data else param.schema_data properties[param.name] = prop_schema return FunctionDeclaration( @@ -147,7 +149,7 @@ def kernel_function_metadata_to_vertex_ai_function_call_format(metadata: KernelF parameters={ "type": "object", "properties": properties, - "required": [p.name for p in metadata.parameters if p.is_required], + "required": [p.name for p in metadata.parameters if p.is_required and p.name is not None], }, )