Skip to content
Closed
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
43 changes: 41 additions & 2 deletions python/semantic_kernel/agents/azure_ai/agent_thread_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
BaseAsyncAgentEventHandler,
FunctionToolDefinition,
RequiredMcpToolCall,
ResponseFormatJsonSchema,
ResponseFormatJsonSchemaType,
RunStep,
RunStepAzureAISearchToolCall,
Expand Down Expand Up @@ -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,
Expand All @@ -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

Comment on lines +928 to +943
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

_normalize_response_format returns the original dict for any dict shapes it doesn't recognize (including {"type": "json_object"} or other unexpected type values). Since AgentThreadActions.invoke/_generate_options feed this value directly into agent.client.agents.runs.create/stream, this can still result in passing raw dicts into the Azure SDK/monitoring pipeline. If the intent is to avoid dicts entirely, normalize additional supported shapes (e.g., map {"type": "json_object"} to the string form the SDK expects) and/or raise a clear error for unsupported dict formats instead of passing them through.

Copilot uses AI. Check for mistakes.
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."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
MessageTextDetails,
RequiredFunctionToolCall,
RequiredFunctionToolCallDetails,
ResponseFormatJsonSchemaType,
RunStep,
RunStepCodeInterpreterToolCall,
RunStepCodeInterpreterToolCallDetails,
Expand Down Expand Up @@ -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)

Expand Down
Loading