From 3557298bd6623a6a6adc3437cd022bc456fa070b 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 30efe4113167..4b64c98f65c7 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 @@ -147,16 +148,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 49cd0a8217ab..3e7076cb8be2 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, @@ -137,12 +138,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 0ef6a998710afe9dfdf601e2240ef9296eaffe67 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 | 9 ++-- .../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, 143 insertions(+), 7 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 3e7076cb8be2..584abbab8e8b 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 @@ -138,10 +138,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 d44bded94eff..181d9f5d470e 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(): @@ -157,3 +160,42 @@ def test_format_assistant_message_without_thought_signature() -> None: assert formatted[0].function_call.name == "test_function" assert formatted[0].function_call.args == {"arg1": "value1"} assert not getattr(formatted[0], "thought_signature", None) + + +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 3f059c1e94ce..882fec8915e6 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(): @@ -157,3 +160,40 @@ def test_format_assistant_message_without_thought_signature() -> None: assert formatted[0].function_call.args == {"arg1": "value1"} part_dict = formatted[0].to_dict() assert "thought_signature" not in part_dict + + +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 f913cc0db55cb64f3ae2c1750bfbab7d4bb2fb63 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 dba1cf8fbd6a6abe77028cedbcc532b0ba6a7d72 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 ce6bb7ab6a956d7276f6040f088aea564cfd74f6 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 4b64c98f65c7..0fd460b32433 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 @@ -152,12 +152,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 584abbab8e8b..832899417ef9 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 @@ -141,6 +141,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( @@ -149,7 +151,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], }, )