From 5bdf8a774a436a33bb9ceb2ee962e1b2edab011b Mon Sep 17 00:00:00 2001 From: Evan Mattson <35585003+moonbox3@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:42:46 +0900 Subject: [PATCH] Sample to show passing in JSON schema for structured outputs (#2362) --- .../packages/core/agent_framework/_agents.py | 6 +- python/samples/README.md | 1 + .../getting_started/agents/openai/README.md | 1 + ...ai_chat_client_with_runtime_json_schema.py | 110 ++++++++++++++++++ 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 python/samples/getting_started/agents/openai/openai_chat_client_with_runtime_json_schema.py diff --git a/python/packages/core/agent_framework/_agents.py b/python/packages/core/agent_framework/_agents.py index 591d255490..e1f0d4fe4f 100644 --- a/python/packages/core/agent_framework/_agents.py +++ b/python/packages/core/agent_framework/_agents.py @@ -853,6 +853,7 @@ class ChatAgent(BaseAgent): await self._async_exit_stack.enter_async_context(mcp_server) final_tools.extend(mcp_server.functions) + merged_additional_options = additional_chat_options or {} co = run_chat_options & ChatOptions( model_id=model_id, conversation_id=thread.service_thread_id, @@ -871,7 +872,7 @@ class ChatAgent(BaseAgent): tools=final_tools, top_p=top_p, user=user, - **(additional_chat_options or {}), + additional_properties=merged_additional_options, # type: ignore[arg-type] ) # Filter chat_options from kwargs to prevent duplicate keyword argument filtered_kwargs = {k: v for k, v in kwargs.items() if k != "chat_options"} @@ -986,6 +987,7 @@ class ChatAgent(BaseAgent): await self._async_exit_stack.enter_async_context(mcp_server) final_tools.extend(mcp_server.functions) + merged_additional_options = additional_chat_options or {} co = run_chat_options & ChatOptions( conversation_id=thread.service_thread_id, allow_multiple_tool_calls=allow_multiple_tool_calls, @@ -1004,7 +1006,7 @@ class ChatAgent(BaseAgent): tools=final_tools, top_p=top_p, user=user, - **(additional_chat_options or {}), + additional_properties=merged_additional_options, # type: ignore[arg-type] ) # Filter chat_options from kwargs to prevent duplicate keyword argument diff --git a/python/samples/README.md b/python/samples/README.md index 6bc6cce86c..dbfb00e5a7 100644 --- a/python/samples/README.md +++ b/python/samples/README.md @@ -118,6 +118,7 @@ This directory contains samples demonstrating the capabilities of Microsoft Agen | [`getting_started/agents/openai/openai_chat_client_with_local_mcp.py`](./getting_started/agents/openai/openai_chat_client_with_local_mcp.py) | OpenAI Chat Client with Local MCP Example | | [`getting_started/agents/openai/openai_chat_client_with_thread.py`](./getting_started/agents/openai/openai_chat_client_with_thread.py) | OpenAI Chat Client with Thread Management Example | | [`getting_started/agents/openai/openai_chat_client_with_web_search.py`](./getting_started/agents/openai/openai_chat_client_with_web_search.py) | OpenAI Chat Client with Web Search Example | +| [`getting_started/agents/openai/openai_chat_client_with_runtime_json_schema.py`](./getting_started/agents/openai/openai_chat_client_with_runtime_json_schema.py) | OpenAI Chat Client with runtime JSON Schema for structured output without a Pydantic model | | [`getting_started/agents/openai/openai_responses_client_basic.py`](./getting_started/agents/openai/openai_responses_client_basic.py) | OpenAI Responses Client Basic Example | | [`getting_started/agents/openai/openai_responses_client_image_analysis.py`](./getting_started/agents/openai/openai_responses_client_image_analysis.py) | OpenAI Responses Client Image Analysis Example | | [`getting_started/agents/openai/openai_responses_client_image_generation.py`](./getting_started/agents/openai/openai_responses_client_image_generation.py) | OpenAI Responses Client Image Generation Example | diff --git a/python/samples/getting_started/agents/openai/README.md b/python/samples/getting_started/agents/openai/README.md index db71816558..5e0f399e11 100644 --- a/python/samples/getting_started/agents/openai/README.md +++ b/python/samples/getting_started/agents/openai/README.md @@ -19,6 +19,7 @@ This folder contains examples demonstrating different ways to create and use age | [`openai_chat_client_with_local_mcp.py`](openai_chat_client_with_local_mcp.py) | Shows how to integrate OpenAI agents with local Model Context Protocol (MCP) servers for enhanced functionality and tool integration. | | [`openai_chat_client_with_thread.py`](openai_chat_client_with_thread.py) | Demonstrates thread management with OpenAI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. | | [`openai_chat_client_with_web_search.py`](openai_chat_client_with_web_search.py) | Shows how to use web search capabilities with OpenAI agents to retrieve and use information from the internet in responses. | +| [`openai_chat_client_with_runtime_json_schema.py`](openai_chat_client_with_runtime_json_schema.py) | Shows how to supply a runtime JSON Schema via `additional_chat_options` for structured output without defining a Pydantic model. | | [`openai_responses_client_basic.py`](openai_responses_client_basic.py) | The simplest way to create an agent using `ChatAgent` with `OpenAIResponsesClient`. Shows both streaming and non-streaming responses for structured response generation with OpenAI models. | | [`openai_responses_client_image_analysis.py`](openai_responses_client_image_analysis.py) | Demonstrates how to use vision capabilities with agents to analyze images. | | [`openai_responses_client_image_generation.py`](openai_responses_client_image_generation.py) | Demonstrates how to use image generation capabilities with OpenAI agents to create images based on text descriptions. Requires PIL (Pillow) for image display. | diff --git a/python/samples/getting_started/agents/openai/openai_chat_client_with_runtime_json_schema.py b/python/samples/getting_started/agents/openai/openai_chat_client_with_runtime_json_schema.py new file mode 100644 index 0000000000..0551ec1bfc --- /dev/null +++ b/python/samples/getting_started/agents/openai/openai_chat_client_with_runtime_json_schema.py @@ -0,0 +1,110 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import json + +from agent_framework.openai import OpenAIChatClient + +""" +OpenAI Chat Client Runtime JSON Schema Example + +Demonstrates structured outputs when the schema is only known at runtime. +Uses additional_chat_options to pass a JSON Schema payload directly to OpenAI +without defining a Pydantic model up front. +""" + + +runtime_schema = { + "title": "WeatherDigest", + "type": "object", + "properties": { + "location": {"type": "string"}, + "conditions": {"type": "string"}, + "temperature_c": {"type": "number"}, + "advisory": {"type": "string"}, + }, + # OpenAI strict mode requires every property to appear in required. + "required": ["location", "conditions", "temperature_c", "advisory"], + "additionalProperties": False, +} + + +async def non_streaming_example() -> None: + print("=== Non-streaming runtime JSON schema example ===") + + agent = OpenAIChatClient().create_agent( + name="RuntimeSchemaAgent", + instructions="Return only JSON that matches the provided schema. Do not add commentary.", + ) + + query = "Give a brief weather digest for Seattle." + print(f"User: {query}") + + response = await agent.run( + query, + additional_chat_options={ + "response_format": { + "type": "json_schema", + "json_schema": { + "name": runtime_schema["title"], + "strict": True, + "schema": runtime_schema, + }, + }, + }, + ) + + print("Model output:") + print(response.text) + + parsed = json.loads(response.text) + print("Parsed dict:") + print(parsed) + + +async def streaming_example() -> None: + print("=== Streaming runtime JSON schema example ===") + + agent = OpenAIChatClient().create_agent( + name="RuntimeSchemaAgent", + instructions="Return only JSON that matches the provided schema. Do not add commentary.", + ) + + query = "Give a brief weather digest for Portland." + print(f"User: {query}") + + chunks = [] + async for chunk in agent.run_stream( + query, + additional_chat_options={ + "response_format": { + "type": "json_schema", + "json_schema": { + "name": runtime_schema["title"], + "strict": True, + "schema": runtime_schema, + }, + }, + }, + ): + if chunk.text: + chunks.append(chunk.text) + + raw_text = "".join(chunks) + print("Model output:") + print(raw_text) + + parsed = json.loads(raw_text) + print("Parsed dict:") + print(parsed) + + +async def main() -> None: + print("=== OpenAI Chat Client with runtime JSON Schema ===") + + await non_streaming_example() + await streaming_example() + + +if __name__ == "__main__": + asyncio.run(main())