From bd6b3fae4fc89d0358d2564afdbe4922cd233f14 Mon Sep 17 00:00:00 2001 From: mukunda katta Date: Sun, 19 Apr 2026 20:02:47 -0700 Subject: [PATCH 1/2] fix(python): normalize Azure AI agent response_format --- .../agents/azure_ai/agent_thread_actions.py | 35 ++++++++++++++++++- .../test_agent_thread_actions.py | 20 +++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/python/semantic_kernel/agents/azure_ai/agent_thread_actions.py b/python/semantic_kernel/agents/azure_ai/agent_thread_actions.py index 62fb798bb11b..d9ff16476563 100644 --- a/python/semantic_kernel/agents/azure_ai/agent_thread_actions.py +++ b/python/semantic_kernel/agents/azure_ai/agent_thread_actions.py @@ -13,6 +13,7 @@ BaseAsyncAgentEventHandler, FunctionToolDefinition, RequiredMcpToolCall, + ResponseFormatJsonSchema, ResponseFormatJsonSchemaType, RunStep, RunStepAzureAISearchToolCall, @@ -902,15 +903,47 @@ def _merge_options( Run-level parameters take precedence. """ + normalized_response_format = ( + cls._normalize_response_format(response_format) + if response_format is not None + else cls._normalize_response_format(agent.definition.response_format) + ) return { "model": model if model is not None else agent.definition.model, - "response_format": response_format if response_format is not None else agent.definition.response_format, + "response_format": normalized_response_format, "temperature": temperature if temperature is not None else None, "top_p": top_p if top_p is not None else None, "metadata": metadata if metadata is not None else agent.definition.metadata, **kwargs, } + @classmethod + def _normalize_response_format( + cls: type[_T], response_format: ResponseFormatJsonSchemaType | dict[str, Any] | None + ) -> ResponseFormatJsonSchemaType | dict[str, Any] | None: + """Normalize structured output response formats for Azure SDK consumers.""" + if response_format is None or isinstance(response_format, ResponseFormatJsonSchemaType): + return response_format + + if not isinstance(response_format, dict): + return response_format + + if response_format.get("type") != "json_schema": + return response_format + + json_schema = response_format.get("json_schema") + if not isinstance(json_schema, dict): + return response_format + + return ResponseFormatJsonSchemaType( + json_schema=ResponseFormatJsonSchema( + name=json_schema.get("name"), + description=json_schema.get("description"), + schema=json_schema.get("schema"), + strict=json_schema.get("strict"), + ) + ) + @classmethod def _generate_options(cls: type[_T], **kwargs: Any) -> dict[str, Any]: """Generate a dictionary of options that can be passed directly to create_run.""" diff --git a/python/tests/unit/agents/azure_ai_agent/test_agent_thread_actions.py b/python/tests/unit/agents/azure_ai_agent/test_agent_thread_actions.py index 000491d09021..a43212046497 100644 --- a/python/tests/unit/agents/azure_ai_agent/test_agent_thread_actions.py +++ b/python/tests/unit/agents/azure_ai_agent/test_agent_thread_actions.py @@ -8,6 +8,7 @@ MessageTextDetails, RequiredFunctionToolCall, RequiredFunctionToolCallDetails, + ResponseFormatJsonSchemaType, RunStep, RunStepCodeInterpreterToolCall, RunStepCodeInterpreterToolCallDetails, @@ -80,6 +81,25 @@ class FakeClient: assert FakeAgentClient.create_message.await_count == 0 +def test_agent_thread_actions_generate_options_normalizes_dict_response_format(ai_agent_definition): + agent = AzureAIAgent(client=AsyncMock(spec=AIProjectClient), definition=ai_agent_definition) + agent.definition.response_format = { + "type": "json_schema", + "json_schema": { + "name": "planet_mass", + "description": "Extract planet mass.", + "schema": {"type": "object", "properties": {"mass": {"type": "number"}}}, + "strict": True, + }, + } + + options = AgentThreadActions._generate_options(agent=agent) + + assert isinstance(options["response_format"], ResponseFormatJsonSchemaType) + assert options["response_format"].json_schema.name == "planet_mass" + assert options["response_format"].json_schema.strict is True + + async def test_agent_thread_actions_invoke(ai_project_client: AIProjectClient, ai_agent_definition): agent = AzureAIAgent(client=ai_project_client, definition=ai_agent_definition) From 7ad64470312809c6be2e4e5fa33e5b4e36f1407f Mon Sep 17 00:00:00 2001 From: Mukunda Rao Katta Date: Wed, 22 Apr 2026 15:30:32 -0700 Subject: [PATCH 2/2] fix(azure-ai): widen response_format types + normalize json_object/text dict shapes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per @Copilot review: - widen _merge_options and _normalize_response_format to accept str | ResponseFormatJsonSchemaType | dict | None, matching AzureAIAgent's AgentsApiResponseFormatOption - map {'type': 'json_object'} and {'type': 'text'} dict shapes to their canonical string form so the Azure SDK sees the expected type - narrow return type to str | ResponseFormatJsonSchemaType | None (dict is only passed through as an unrecognized escape hatch, never intentionally) Avoids importing AgentsApiResponseFormatOption directly (would be circular — azure_ai_agent imports from this module). The inline union keeps type checkers satisfied without duplicating the alias definition. --- .../agents/azure_ai/agent_thread_actions.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/python/semantic_kernel/agents/azure_ai/agent_thread_actions.py b/python/semantic_kernel/agents/azure_ai/agent_thread_actions.py index d9ff16476563..778b044b4b3c 100644 --- a/python/semantic_kernel/agents/azure_ai/agent_thread_actions.py +++ b/python/semantic_kernel/agents/azure_ai/agent_thread_actions.py @@ -893,7 +893,7 @@ def _merge_options( *, agent: "AzureAIAgent", model: str | None = None, - response_format: ResponseFormatJsonSchemaType | None = None, + response_format: str | ResponseFormatJsonSchemaType | dict[str, Any] | None = None, temperature: float | None = None, top_p: float | None = None, metadata: dict[str, str] | None = None, @@ -919,8 +919,8 @@ def _merge_options( @classmethod def _normalize_response_format( - cls: type[_T], response_format: ResponseFormatJsonSchemaType | dict[str, Any] | None - ) -> ResponseFormatJsonSchemaType | dict[str, Any] | None: + cls: type[_T], response_format: str | ResponseFormatJsonSchemaType | dict[str, Any] | None + ) -> str | ResponseFormatJsonSchemaType | None: """Normalize structured output response formats for Azure SDK consumers.""" if response_format is None or isinstance(response_format, ResponseFormatJsonSchemaType): return response_format @@ -928,7 +928,13 @@ def _normalize_response_format( if not isinstance(response_format, dict): return response_format - if response_format.get("type") != "json_schema": + # Map simple dict shapes to the string form the Azure SDK expects. + # {"type": "json_object"} / {"type": "text"} both have canonical string equivalents. + rf_type = response_format.get("type") + if rf_type in ("json_object", "text"): + return rf_type + + if rf_type != "json_schema": return response_format json_schema = response_format.get("json_schema")