Skip to content

Commit 2b22417

Browse files
committed
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
1 parent 50c8c3a commit 2b22417

File tree

5 files changed

+144
-8
lines changed

5 files changed

+144
-8
lines changed

python/semantic_kernel/connectors/ai/google/shared_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ def _sanitize_node(node: dict[str, Any]) -> dict[str, Any]:
7979
node["nullable"] = True
8080
node["type"] = non_null[0] if non_null else "string"
8181

82-
# --- handle ``anyOf`` / ``oneOf`` ---
83-
for key in ("anyOf", "oneOf"):
82+
# --- handle ``anyOf`` / ``oneOf`` / ``allOf`` ---
83+
for key in ("anyOf", "oneOf", "allOf"):
8484
variants = node.get(key)
8585
if not variants:
8686
continue

python/semantic_kernel/connectors/ai/google/vertex_ai/services/utils.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import json
44
import logging
5-
from typing import TYPE_CHECKING
5+
from typing import TYPE_CHECKING, Any
66

77
from google.cloud.aiplatform_v1beta1.types.content import Candidate
88
from vertexai.generative_models import FunctionDeclaration, Part, Tool, ToolConfig
@@ -136,10 +136,11 @@ def format_tool_message(message: ChatMessageContent) -> list[Part]:
136136

137137
def kernel_function_metadata_to_vertex_ai_function_call_format(metadata: KernelFunctionMetadata) -> FunctionDeclaration:
138138
"""Convert the kernel function metadata to function calling format."""
139-
properties = {}
140-
for param in metadata.parameters:
141-
prop_schema = sanitize_schema_for_google_ai(param.schema_data) if param.schema_data else param.schema_data
142-
properties[param.name] = prop_schema
139+
properties: dict[str, Any] = {}
140+
if metadata.parameters:
141+
for param in metadata.parameters:
142+
prop_schema = sanitize_schema_for_google_ai(param.schema_data) if param.schema_data else param.schema_data
143+
properties[param.name] = prop_schema
143144
return FunctionDeclaration(
144145
name=metadata.custom_fully_qualified_name(GEMINI_FUNCTION_NAME_SEPARATOR),
145146
description=metadata.description or "",

python/tests/unit/connectors/ai/google/google_ai/services/test_google_ai_utils.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
finish_reason_from_google_ai_to_semantic_kernel,
88
format_assistant_message,
99
format_user_message,
10+
kernel_function_metadata_to_google_ai_function_call_format,
1011
)
1112
from semantic_kernel.contents.chat_message_content import ChatMessageContent
1213
from semantic_kernel.contents.function_call_content import FunctionCallContent
@@ -16,6 +17,8 @@
1617
from semantic_kernel.contents.utils.author_role import AuthorRole
1718
from semantic_kernel.contents.utils.finish_reason import FinishReason as SemanticKernelFinishReason
1819
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError
20+
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
21+
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
1922

2023

2124
def test_finish_reason_from_google_ai_to_semantic_kernel():
@@ -113,3 +116,42 @@ def test_format_assistant_message_with_unsupported_items() -> None:
113116

114117
with pytest.raises(ServiceInvalidRequestError):
115118
format_assistant_message(assistant_message)
119+
120+
121+
def test_google_ai_function_call_format_sanitizes_anyof_schema() -> None:
122+
"""Integration test: anyOf in param schema_data is sanitized in the output dict."""
123+
metadata = KernelFunctionMetadata(
124+
name="test_func",
125+
description="A test function",
126+
is_prompt=False,
127+
parameters=[
128+
KernelParameterMetadata(
129+
name="messages",
130+
description="The user messages",
131+
is_required=True,
132+
schema_data={
133+
"anyOf": [
134+
{"type": "string"},
135+
{"type": "array", "items": {"type": "string"}},
136+
],
137+
"description": "The user messages",
138+
},
139+
),
140+
],
141+
)
142+
result = kernel_function_metadata_to_google_ai_function_call_format(metadata)
143+
param_schema = result["parameters"]["properties"]["messages"]
144+
assert "anyOf" not in param_schema
145+
assert param_schema["type"] == "string"
146+
147+
148+
def test_google_ai_function_call_format_empty_parameters() -> None:
149+
"""Integration test: metadata with no parameters produces parameters=None."""
150+
metadata = KernelFunctionMetadata(
151+
name="no_params_func",
152+
description="No parameters",
153+
is_prompt=False,
154+
parameters=[],
155+
)
156+
result = kernel_function_metadata_to_google_ai_function_call_format(metadata)
157+
assert result["parameters"] is None

python/tests/unit/connectors/ai/google/test_shared_utils.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,56 @@ def test_sanitize_schema_agent_messages_param():
211211
assert "anyOf" not in result
212212
assert result["type"] == "string"
213213
assert result["description"] == "The user messages for the agent."
214+
215+
216+
def test_sanitize_schema_allof():
217+
"""allOf should be handled like anyOf/oneOf, picking the first variant."""
218+
schema = {
219+
"allOf": [
220+
{"type": "object", "properties": {"name": {"type": "string"}}},
221+
{"type": "object", "properties": {"age": {"type": "integer"}}},
222+
],
223+
}
224+
result = sanitize_schema_for_google_ai(schema)
225+
assert "allOf" not in result
226+
assert result["type"] == "object"
227+
assert "name" in result["properties"]
228+
229+
230+
def test_sanitize_schema_allof_with_null():
231+
"""allOf with a null variant should produce nullable: true."""
232+
schema = {
233+
"allOf": [{"type": "string"}, {"type": "null"}],
234+
}
235+
result = sanitize_schema_for_google_ai(schema)
236+
assert "allOf" not in result
237+
assert result["type"] == "string"
238+
assert result["nullable"] is True
239+
240+
241+
def test_sanitize_schema_all_null_type_list():
242+
"""type: ["null"] should fall back to type: "string" + nullable: true."""
243+
schema = {"type": ["null"]}
244+
result = sanitize_schema_for_google_ai(schema)
245+
assert result == {"type": "string", "nullable": True}
246+
247+
248+
def test_sanitize_schema_all_null_anyof():
249+
"""anyOf where all variants are null should fall back to type: "string"."""
250+
schema = {"anyOf": [{"type": "null"}]}
251+
result = sanitize_schema_for_google_ai(schema)
252+
assert result == {"type": "string", "nullable": True}
253+
254+
255+
def test_sanitize_schema_chosen_variant_keeps_own_description():
256+
"""When the chosen anyOf variant has its own description, do not overwrite it."""
257+
schema = {
258+
"anyOf": [
259+
{"type": "string", "description": "inner desc"},
260+
{"type": "null"},
261+
],
262+
"description": "outer desc",
263+
}
264+
result = sanitize_schema_for_google_ai(schema)
265+
assert result["description"] == "inner desc"
266+
assert result["nullable"] is True

python/tests/unit/connectors/ai/google/vertex_ai/services/test_vertex_ai_utils.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
import pytest
44
from google.cloud.aiplatform_v1beta1.types.content import Candidate
5-
from vertexai.generative_models import Part
5+
from vertexai.generative_models import FunctionDeclaration, Part
66

77
from semantic_kernel.connectors.ai.google.vertex_ai.services.utils import (
88
finish_reason_from_vertex_ai_to_semantic_kernel,
99
format_assistant_message,
1010
format_user_message,
11+
kernel_function_metadata_to_vertex_ai_function_call_format,
1112
)
1213
from semantic_kernel.contents.chat_message_content import ChatMessageContent
1314
from semantic_kernel.contents.function_call_content import FunctionCallContent
@@ -17,6 +18,8 @@
1718
from semantic_kernel.contents.utils.author_role import AuthorRole
1819
from semantic_kernel.contents.utils.finish_reason import FinishReason
1920
from semantic_kernel.exceptions.service_exceptions import ServiceInvalidRequestError
21+
from semantic_kernel.functions.kernel_function_metadata import KernelFunctionMetadata
22+
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
2023

2124

2225
def test_finish_reason_from_vertex_ai_to_semantic_kernel():
@@ -110,3 +113,40 @@ def test_format_assistant_message_with_unsupported_items() -> None:
110113

111114
with pytest.raises(ServiceInvalidRequestError):
112115
format_assistant_message(assistant_message)
116+
117+
118+
def test_vertex_ai_function_call_format_sanitizes_anyof_schema() -> None:
119+
"""Integration test: anyOf in param schema_data is sanitized in the FunctionDeclaration."""
120+
metadata = KernelFunctionMetadata(
121+
name="test_func",
122+
description="A test function",
123+
is_prompt=False,
124+
parameters=[
125+
KernelParameterMetadata(
126+
name="messages",
127+
description="The user messages",
128+
is_required=True,
129+
schema_data={
130+
"anyOf": [
131+
{"type": "string"},
132+
{"type": "array", "items": {"type": "string"}},
133+
],
134+
"description": "The user messages",
135+
},
136+
),
137+
],
138+
)
139+
result = kernel_function_metadata_to_vertex_ai_function_call_format(metadata)
140+
assert isinstance(result, FunctionDeclaration)
141+
142+
143+
def test_vertex_ai_function_call_format_empty_parameters() -> None:
144+
"""Integration test: metadata with no parameters produces empty properties, no crash."""
145+
metadata = KernelFunctionMetadata(
146+
name="no_params_func",
147+
description="No parameters",
148+
is_prompt=False,
149+
parameters=[],
150+
)
151+
result = kernel_function_metadata_to_vertex_ai_function_call_format(metadata)
152+
assert isinstance(result, FunctionDeclaration)

0 commit comments

Comments
 (0)