Skip to content

Python: fix(python): normalize Azure AI agent response_format#13891

Open
MukundaKatta wants to merge 3 commits intomicrosoft:mainfrom
MukundaKatta:codex/semantic-kernel-azure-response-format
Open

Python: fix(python): normalize Azure AI agent response_format#13891
MukundaKatta wants to merge 3 commits intomicrosoft:mainfrom
MukundaKatta:codex/semantic-kernel-azure-response-format

Conversation

@MukundaKatta
Copy link
Copy Markdown

Summary

  • normalize dict-based structured output configs into Azure SDK ResponseFormatJsonSchemaType objects before creating runs
  • avoid passing raw dict response_format values into Azure Monitor-instrumented agent clients
  • add a focused unit test around the option generation path

Testing

  • python3 -m py_compile python/semantic_kernel/agents/azure_ai/agent_thread_actions.py python/tests/unit/agents/azure_ai_agent/test_agent_thread_actions.py
  • python3 -m pytest tests/unit/agents/azure_ai_agent/test_agent_thread_actions.py -k response_format (fails locally because repo test setup imports openai, which is not installed in this environment)

@MukundaKatta MukundaKatta requested a review from a team as a code owner April 20, 2026 03:03
@moonbox3 moonbox3 added the python Pull requests for the Python Semantic Kernel label Apr 20, 2026
@github-actions github-actions Bot changed the title fix(python): normalize Azure AI agent response_format Python: fix(python): normalize Azure AI agent response_format Apr 20, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Automated Code Review

Reviewers: 4 | Confidence: 89%

✓ Correctness

The diff adds a _normalize_response_format classmethod that converts dict-based json_schema response formats into strongly-typed ResponseFormatJsonSchemaType objects before passing them to the Azure SDK. The normalization is integrated into _merge_options and applied to both run-level and agent-level response formats. The implementation correctly handles all edge cases (None, already-typed, non-dict, non-json_schema dicts, dicts with non-dict json_schema). The new test verifies the main use case of dict-to-typed conversion via _generate_options. The import of ResponseFormatJsonSchema is consistent with existing usage in the sample code. No correctness issues found.

✓ Security Reliability

This change adds a _normalize_response_format helper that converts dict-based JSON schema response formats into typed ResponseFormatJsonSchemaType SDK objects, and integrates it into _merge_options. The normalization logic is defensively written with multiple guard clauses that correctly handle None, already-typed objects, non-dict inputs, dicts without the expected structure, and dicts with a non-dict json_schema field. The AgentsApiResponseFormatOption type alias (str | ResponseFormatJsonSchemaType) means agent.definition.response_format could also be a plain string, which the normalizer handles correctly by returning it as-is (the isinstance(response_format, dict) check fails for strings). No security or reliability issues were identified.

✓ Test Coverage

The PR adds _normalize_response_format to convert dict-based response formats into ResponseFormatJsonSchemaType objects and includes one integration test covering the happy path via _generate_options. While the happy path is covered, several branches of _normalize_response_format lack tests (None input, already-typed input, non-json_schema dicts, missing/non-dict json_schema), and the test doesn't verify all propagated fields (description, schema). There is also no test exercising the run-level response_format parameter normalization path.

✓ Design Approach

The change adds a _normalize_response_format helper that converts a raw dict with type: "json_schema" to the SDK's ResponseFormatJsonSchemaType object, and calls it in _merge_options before returning the merged options. The approach is sound: agent.definition.response_format can arrive as a plain dict from the Azure SDK deserialization layer, and normalizing it at the single merge/consume point (_merge_options) is a defensible place. There are no fragile assumptions, no leaky abstractions, and no symptom-level masking. One minor observation worth noting: the _merge_options signature still declares response_format: ResponseFormatJsonSchemaType | None, which is narrower than both AgentsApiResponseFormatOption = str | ResponseFormatJsonSchemaType (used by all three public invoke/get_response methods in azure_ai_agent.py) and _normalize_response_format's own input type ResponseFormatJsonSchemaType | dict[str, Any] | None. The str branch silently passes through _normalize_response_format (it hits the not isinstance(response_format, dict) guard and returns as-is), so there is no runtime breakage, but the annotation is misleading. This is a pre-existing mismatch that the diff does not worsen, but it would be cleaner to align the annotation with the actual accepted types.

Suggestions

  • Add a test that passes a dict response_format as a run-level override to _generate_options (or _merge_options), verifying the run-level normalization path.
  • The _merge_options response_format parameter annotation is ResponseFormatJsonSchemaType | None, but callers pass str | ResponseFormatJsonSchemaType | None. Now that _normalize_response_format formally accepts dict[str, Any] as well, consider updating the annotation to AgentsApiResponseFormatOption | dict[str, Any] | None so the declared type matches what is actually handled.

Automated review by MukundaKatta's agents

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the Azure AI Agent run-option construction path to normalize dict-based structured output response_format configurations into Azure SDK model objects before invoking runs.create/stream, and adds a unit test to cover that normalization.

Changes:

  • Normalize dict-based response_format (specifically type: "json_schema") into ResponseFormatJsonSchemaType in AgentThreadActions option merging.
  • Ensure response_format passed to run creation is always the normalized value from merged agent/run options.
  • Add a focused unit test asserting _generate_options converts a dict response_format into ResponseFormatJsonSchemaType.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
python/semantic_kernel/agents/azure_ai/agent_thread_actions.py Adds _normalize_response_format and applies it during option merging so Azure SDK receives normalized response_format.
python/tests/unit/agents/azure_ai_agent/test_agent_thread_actions.py Adds a unit test validating dict response_format gets normalized to ResponseFormatJsonSchemaType.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 890 to +924
@classmethod
def _merge_options(
cls: type[_T],
*,
agent: "AzureAIAgent",
model: str | None = None,
response_format: ResponseFormatJsonSchemaType | None = None,
temperature: float | None = None,
top_p: float | None = None,
metadata: dict[str, str] | None = None,
**kwargs: Any,
) -> dict[str, Any]:
"""Merge run-time options with the agent-level 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."""
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.

_merge_options/_normalize_response_format currently assume response_format is ResponseFormatJsonSchemaType | dict | None, but AzureAIAgent passes response_format through as str | ResponseFormatJsonSchemaType (see AgentsApiResponseFormatOption), and this method already returns non-dict values unchanged. To keep type hints accurate (and avoid needing type: ignore), consider widening the accepted/returned types here to include str (and ideally align with AzureAIAgent.AgentsApiResponseFormatOption).

Copilot uses AI. Check for mistakes.
Comment on lines +928 to +937
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

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.
…xt dict shapes

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.
@MukundaKatta
Copy link
Copy Markdown
Author

Addressed @copilot's review in 7ad6447:

Type widening: _merge_options and _normalize_response_format now accept str | ResponseFormatJsonSchemaType | dict[str, Any] | None — matching the shape AzureAIAgent passes through via AgentsApiResponseFormatOption. Avoided importing the alias directly because azure_ai_agent already imports from this module (would be circular); the inline union keeps type checkers happy without duplicating the alias.

Unknown dict shapes: {"type": "json_object"} and {"type": "text"} now normalize to their canonical string form ("json_object" / "text") that the Azure SDK expects. Only truly unrecognized dict shapes fall through unchanged (escape hatch for future SDK additions).

Return type narrowed to str | ResponseFormatJsonSchemaType | None since we no longer intentionally pass dicts downstream for recognized types.

Copy link
Copy Markdown
Collaborator

@moonbox3 moonbox3 left a comment

Choose a reason for hiding this comment

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

Automated Code Review

Reviewers: 4 | Confidence: 82%

✓ Correctness

The PR correctly widens _merge_options's response_format parameter and introduces _normalize_response_format to convert dict-shaped response formats into the Azure SDK types before they reach runs.create/stream. The import of ResponseFormatJsonSchema from azure.ai.agents.models is consistent with existing usage in the samples codebase. The normalization logic is sound for the cases it handles: None, ResponseFormatJsonSchemaType, bare strings, and the json_object/text/json_schema dict variants. The new test covers the primary agent-level dict normalization path. The return type annotation on _normalize_response_format still omits dict[str, Any] for the two fallthrough branches (rf_type != 'json_schema' and json_schema not being a dict), but that concern is already captured in the existing unresolved comment at line 937 and is not a new regression. No new blocking correctness issues were found.

✓ Security Reliability

The diff widens _merge_options/_normalize_response_format to accept str | ResponseFormatJsonSchemaType | dict[str, Any] | None, addressing the first unresolved thread. It maps {"type": "json_object"} and {"type": "text"} dict shapes to their string equivalents and constructs a typed ResponseFormatJsonSchemaType for json_schema dicts, which directly aligns with AgentsApiResponseFormatOption = str | ResponseFormatJsonSchemaType (azure_ai_agent.py:79). No injection risks, resource leaks, or credential exposure were found. The second unresolved thread (raw dict passthrough for unrecognized/malformed type values) is partially mitigated but not fully resolved — unknown type values and json_schema dicts with a non-dict json_schema field still fall through as raw dicts. One reliability concern exists: json_schema.get("name") can be None if the caller omits name, which may fail during SDK serialization since name appears to be a required field in ResponseFormatJsonSchema (per the sample at azure_ai_agent_structured_outputs.py:49-53), but SDK enforcement could not be confirmed without the installed package source.

✗ Test Coverage

The PR introduces _normalize_response_format with 7 distinct branches, but the single new test only exercises branch 7 (dict with type == "json_schema"ResponseFormatJsonSchemaType). The explicitly documented transformations for {"type": "json_object"} and {"type": "text"} (which map a dict to the canonical string the Azure SDK expects) are entirely untested, as are the passthrough branches for ResponseFormatJsonSchemaType, bare strings, unknown dict types, and a malformed json_schema value. Additionally, the test exercises normalization only via agent.definition.response_format; the run-level response_format argument path through _merge_options has no coverage.

✓ Design Approach

No additional design-approach issues found in this diff beyond the already-open response_format validation discussion. Normalizing response_format in AgentThreadActions is a reasonable boundary fix here because that is the only local place where agent.definition.response_format is consumed before runs.create/stream, and the added test covers the dict-backed json_schema case.

Suggestions

  • Add a direct unit test for _normalize_response_format covering each branch: None, a ResponseFormatJsonSchemaType instance, a bare string, {"type": "json_object"}, {"type": "text"}, an unknown dict type, a malformed json_schema value (non-dict inner field), and the json_schema happy-path already covered.
  • Add a test where a run-level response_format dict is passed directly to _generate_options/_merge_options (not only via agent.definition.response_format) to verify the run-level normalization path (diff line ~905–908).

Automated review by moonbox3's agents

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Pull requests for the Python Semantic Kernel

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants