Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions python/packages/foundry/agent_framework_foundry/_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ def _validate_hosted_tool_payload(sanitized: Mapping[str, Any]) -> None:
)


def _to_plain_dict(value: Any) -> Any:
"""Recursively convert Azure SDK models and mappings into plain dictionaries."""
if hasattr(value, "as_dict") and callable(value.as_dict):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This invokes as_dict() before honoring the accepted Mapping contract, so an otherwise valid hosted-tool mapping subclass with an unrelated as_dict signature now raises TypeError during request preparation; previously it was copied with dict(mapping). Please recurse through Mapping entries first and reserve as_dict() conversion for non-mapping SDK model values.

return _to_plain_dict(value.as_dict())
if isinstance(value, Mapping):
return {k: _to_plain_dict(v) for k, v in value.items()}
if isinstance(value, (list, tuple)):
return [_to_plain_dict(v) for v in value]
return value


def _sanitize_foundry_response_tool(tool_item: Any) -> Any: # pyright: ignore[reportUnusedFunction]
"""Return a Responses-API-safe tool payload for Foundry hosted tools.

Expand All @@ -50,9 +61,12 @@ def _sanitize_foundry_response_tool(tool_item: Any) -> Any: # pyright: ignore[r
defaulted (``file_search`` without ``vector_store_ids``, ``mcp`` without
either ``server_url`` or ``project_connection_id``) raise ``ValueError``
with a message that points at the tool definition.
4. Nested Azure SDK models (such as ``BingCustomSearchToolParameters`` or
``AutoCodeInterpreterToolParam``) are recursively converted into plain
dictionaries so the resulting payload is JSON serializable.
"""
if isinstance(tool_item, FoundryMCPTool):
sanitized: dict[str, Any] = dict(cast("Mapping[str, Any]", tool_item))
sanitized: dict[str, Any] = _to_plain_dict(tool_item)
sanitized.pop("name", None)
sanitized.pop("description", None)
_validate_hosted_tool_payload(sanitized)
Expand All @@ -61,7 +75,7 @@ def _sanitize_foundry_response_tool(tool_item: Any) -> Any: # pyright: ignore[r
if isinstance(tool_item, Mapping):
mapping = cast("Mapping[str, Any]", tool_item)
if "type" in mapping and mapping.get("type") not in {"function", "custom"}:
sanitized = dict(mapping)
sanitized = _to_plain_dict(mapping)
sanitized.pop("name", None)
sanitized.pop("description", None)
if sanitized.get("type") == "code_interpreter" and "container" not in sanitized:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1320,6 +1320,43 @@ def test_prepare_tools_for_openai_strips_name_from_non_function_hosted_tool_dict
assert "description" not in response_tools[1]


def test_prepare_tools_for_openai_serializes_bing_custom_search_tool() -> None:
"""Hosted Bing custom search tool contains nested models that must be JSON serializable."""
project_client = MagicMock()
project_client.get_openai_client.return_value = _make_mock_openai_client()
client = FoundryChatClient(project_client=project_client, model="test-model")

tool = FoundryChatClient.get_bing_custom_search_tool(
connection_id="/subscriptions/x/connections/bing",
instance_name="inst",
market="zh-TW",
)

response_tools = client._prepare_tools_for_openai([tool])

assert len(response_tools) == 1
# Ensure nested Azure SDK models were converted and are JSON serializable
dumped = json.dumps(response_tools)
assert "bing_custom_search_preview" in dumped
assert "project_connection_id" in dumped
assert "inst" in dumped


def test_prepare_tools_for_openai_serializes_code_interpreter_with_auto_param() -> None:
"""Hosted code interpreter tool from factory must be JSON serializable."""
project_client = MagicMock()
project_client.get_openai_client.return_value = _make_mock_openai_client()
client = FoundryChatClient(project_client=project_client, model="test-model")

tool = FoundryChatClient.get_code_interpreter_tool()
response_tools = client._prepare_tools_for_openai([tool])

assert len(response_tools) == 1
dumped = json.dumps(response_tools)
assert "code_interpreter" in dumped
assert "auto" in dumped


@pytest.mark.flaky
@pytest.mark.integration
@skip_if_foundry_integration_tests_disabled
Expand Down
Loading