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..778b044b4b3c 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, @@ -892,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, @@ -902,15 +903,53 @@ 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: 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 + + if not isinstance(response_format, dict): + return response_format + + # 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") + 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)