diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py index 30b3f55653..edad03f5b4 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py @@ -358,6 +358,7 @@ def _to_chat_agent_from_details( agent_name=details.name, agent_version=details.version, agent_description=details.description, + model_deployment_name=details.definition.model, ) # Merge tools: hosted tools from definition + user-provided function tools diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_shared.py b/python/packages/azure-ai/agent_framework_azure_ai/_shared.py index f12c4c4b70..b99a3b5f66 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_shared.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_shared.py @@ -501,6 +501,7 @@ def create_text_format_config( return ResponseTextFormatConfigurationJsonSchema( name=response_format.__name__, schema=schema, + strict=True, ) if isinstance(response_format, Mapping): diff --git a/python/packages/azure-ai/tests/test_provider.py b/python/packages/azure-ai/tests/test_provider.py index ea941c2d89..e3dfa0995a 100644 --- a/python/packages/azure-ai/tests/test_provider.py +++ b/python/packages/azure-ai/tests/test_provider.py @@ -317,11 +317,21 @@ def test_provider_as_agent(mock_project_client: MagicMock) -> None: mock_agent_version.definition.top_p = 0.9 mock_agent_version.definition.tools = [] - agent = provider.as_agent(mock_agent_version) + with patch("agent_framework_azure_ai._project_provider.AzureAIClient") as mock_azure_ai_client: + agent = provider.as_agent(mock_agent_version) - assert isinstance(agent, ChatAgent) - assert agent.name == "test-agent" - assert agent.description == "Test Agent" + assert isinstance(agent, ChatAgent) + assert agent.name == "test-agent" + assert agent.description == "Test Agent" + + # Verify AzureAIClient was called with correct parameters + mock_azure_ai_client.assert_called_once() + call_kwargs = mock_azure_ai_client.call_args[1] + assert call_kwargs["project_client"] is mock_project_client + assert call_kwargs["agent_name"] == "test-agent" + assert call_kwargs["agent_version"] == "1.0" + assert call_kwargs["agent_description"] == "Test Agent" + assert call_kwargs["model_deployment_name"] == "gpt-4" async def test_provider_context_manager(mock_project_client: MagicMock) -> None: @@ -370,6 +380,24 @@ async def test_provider_close_method(mock_project_client: MagicMock) -> None: mock_client.close.assert_called_once() +def test_create_text_format_config_sets_strict_for_pydantic_models() -> None: + """Test that create_text_format_config sets strict=True for Pydantic models.""" + from pydantic import BaseModel + + from agent_framework_azure_ai._shared import create_text_format_config + + class TestSchema(BaseModel): + subject: str + summary: str + + result = create_text_format_config(TestSchema) + + # Verify strict=True is set + assert result["strict"] is True + assert result["name"] == "TestSchema" + assert "schema" in result + + @pytest.mark.flaky @skip_if_azure_ai_integration_tests_disabled async def test_provider_create_and_get_agent_integration() -> None: diff --git a/python/packages/core/agent_framework/openai/_assistants_client.py b/python/packages/core/agent_framework/openai/_assistants_client.py index 94b94c7506..d6dd7c251a 100644 --- a/python/packages/core/agent_framework/openai/_assistants_client.py +++ b/python/packages/core/agent_framework/openai/_assistants_client.py @@ -660,6 +660,7 @@ def _prepare_options( "json_schema": { "name": response_format.__name__, "schema": response_format.model_json_schema(), + "strict": True, }, } diff --git a/python/packages/core/tests/openai/test_openai_assistants_client.py b/python/packages/core/tests/openai/test_openai_assistants_client.py index 27fae35af9..424c1cc044 100644 --- a/python/packages/core/tests/openai/test_openai_assistants_client.py +++ b/python/packages/core/tests/openai/test_openai_assistants_client.py @@ -784,6 +784,27 @@ def test_prepare_options_with_mapping_tool(mock_async_openai: MagicMock) -> None assert run_options["tool_choice"] == "auto" +def test_prepare_options_with_pydantic_response_format(mock_async_openai: MagicMock) -> None: + """Test _prepare_options sets strict=True for Pydantic response_format.""" + from pydantic import BaseModel, ConfigDict + + class TestResponse(BaseModel): + name: str + value: int + model_config = ConfigDict(extra="forbid") + + chat_client = create_test_openai_assistants_client(mock_async_openai) + messages = [ChatMessage(role=Role.USER, text="Test")] + options = {"response_format": TestResponse} + + run_options, _ = chat_client._prepare_options(messages, options) # type: ignore + + assert "response_format" in run_options + assert run_options["response_format"]["type"] == "json_schema" + assert run_options["response_format"]["json_schema"]["name"] == "TestResponse" + assert run_options["response_format"]["json_schema"]["strict"] is True + + def test_prepare_options_with_system_message(mock_async_openai: MagicMock) -> None: """Test _prepare_options with system message converted to instructions.""" chat_client = create_test_openai_assistants_client(mock_async_openai) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_response_format.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_response_format.py index 71323c787a..f446b02c67 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_response_format.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_response_format.py @@ -33,7 +33,7 @@ async def main() -> None: agent = await provider.create_agent( name="ProductMarketerAgent", instructions="Return launch briefs as structured JSON.", - # Specify type to use as response + # Specify Pydantic model for structured output via default_options default_options={"response_format": ReleaseBrief}, ) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_runtime_json_schema.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_runtime_json_schema.py index aa1132298c..21f67a0984 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_runtime_json_schema.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_runtime_json_schema.py @@ -37,17 +37,19 @@ async def main() -> None: AzureCliCredential() as credential, AzureAIProjectAgentProvider(credential=credential) as provider, ): - # Pass response_format at agent creation time using dict schema format + # Pass response_format via default_options using dict schema format agent = await provider.create_agent( name="WeatherDigestAgent", instructions="Return sample weather digest as structured JSON.", - response_format={ - "type": "json_schema", - "json_schema": { - "name": runtime_schema["title"], - "strict": True, - "schema": runtime_schema, - }, + default_options={ + "response_format": { + "type": "json_schema", + "json_schema": { + "name": runtime_schema["title"], + "strict": True, + "schema": runtime_schema, + }, + } }, ) diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_response_format.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_response_format.py index 03220e9716..639ce6ac82 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_response_format.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_response_format.py @@ -9,8 +9,10 @@ """ Azure AI Agent Provider Response Format Example -This sample demonstrates using AzureAIAgentsProvider with default_options -containing response_format for structured outputs. +This sample demonstrates using AzureAIAgentsProvider with response_format +for structured outputs in two ways: +1. Setting default response_format at agent creation time (default_options) +2. Overriding response_format at runtime (options parameter in agent.run) """ @@ -24,31 +26,57 @@ class WeatherInfo(BaseModel): model_config = ConfigDict(extra="forbid") +class CityInfo(BaseModel): + """Structured city information.""" + + city_name: str + population: int + country: str + model_config = ConfigDict(extra="forbid") + + async def main() -> None: - """Example of using default_options with response_format in AzureAIAgentsProvider.""" + """Example of using response_format at creation time and runtime.""" async with ( AzureCliCredential() as credential, AzureAIAgentsProvider(credential=credential) as provider, ): + # Create agent with default response_format (WeatherInfo) agent = await provider.create_agent( - name="WeatherReporter", - instructions="You provide weather reports in structured JSON format.", + name="StructuredReporter", + instructions="Return structured JSON based on the requested format.", default_options={"response_format": WeatherInfo}, ) - query = "What's the weather like in Paris today?" - print(f"User: {query}") + # Request 1: Uses default response_format from agent creation + print("--- Request 1: Using default response_format (WeatherInfo) ---") + query1 = "What's the weather like in Paris today?" + print(f"User: {query1}") + + result1 = await agent.run(query1) + + if isinstance(result1.value, WeatherInfo): + weather = result1.value + print("Agent:") + print(f" Location: {weather.location}") + print(f" Temperature: {weather.temperature}") + print(f" Conditions: {weather.conditions}") + print(f" Recommendation: {weather.recommendation}") + + # Request 2: Override response_format at runtime with CityInfo + print("\n--- Request 2: Runtime override with CityInfo ---") + query2 = "Tell me about Tokyo." + print(f"User: {query2}") - result = await agent.run(query) + result2 = await agent.run(query2, options={"response_format": CityInfo}) - if isinstance(result.value, WeatherInfo): - weather = result.value + if isinstance(result2.value, CityInfo): + city = result2.value print("Agent:") - print(f"Location: {weather.location}") - print(f"Temperature: {weather.temperature}") - print(f"Conditions: {weather.conditions}") - print(f"Recommendation: {weather.recommendation}") + print(f" City: {city.city_name}") + print(f" Population: {city.population}") + print(f" Country: {city.country}") if __name__ == "__main__": diff --git a/python/samples/getting_started/agents/openai/openai_assistants_with_response_format.py b/python/samples/getting_started/agents/openai/openai_assistants_with_response_format.py index da75fcffb5..796bdd803c 100644 --- a/python/samples/getting_started/agents/openai/openai_assistants_with_response_format.py +++ b/python/samples/getting_started/agents/openai/openai_assistants_with_response_format.py @@ -10,8 +10,10 @@ """ OpenAI Assistant Provider Response Format Example -This sample demonstrates using OpenAIAssistantProvider with default_options -containing response_format for structured outputs. +This sample demonstrates using OpenAIAssistantProvider with response_format +for structured outputs in two ways: +1. Setting default response_format at agent creation time (default_options) +2. Overriding response_format at runtime (options parameter in agent.run) """ @@ -25,33 +27,59 @@ class WeatherInfo(BaseModel): model_config = ConfigDict(extra="forbid") +class CityInfo(BaseModel): + """Structured city information.""" + + city_name: str + population: int + country: str + model_config = ConfigDict(extra="forbid") + + async def main() -> None: - """Example of using default_options with response_format in OpenAIAssistantProvider.""" + """Example of using response_format at creation time and runtime.""" async with ( AsyncOpenAI() as client, OpenAIAssistantProvider(client) as provider, ): + # Create agent with default response_format (WeatherInfo) agent = await provider.create_agent( - name="WeatherReporter", + name="StructuredReporter", model=os.environ.get("OPENAI_CHAT_MODEL_ID", "gpt-4"), - instructions="You provide weather reports in structured JSON format.", + instructions="Return structured JSON based on the requested format.", default_options={"response_format": WeatherInfo}, ) try: - query = "What's the weather like in Paris today?" - print(f"User: {query}") + # Request 1: Uses default response_format from agent creation + print("--- Request 1: Using default response_format (WeatherInfo) ---") + query1 = "What's the weather like in Paris today?" + print(f"User: {query1}") - result = await agent.run(query) + result1 = await agent.run(query1) - if isinstance(result.value, WeatherInfo): - weather = result.value + if isinstance(result1.value, WeatherInfo): + weather = result1.value print("Agent:") print(f" Location: {weather.location}") print(f" Temperature: {weather.temperature}") print(f" Conditions: {weather.conditions}") print(f" Recommendation: {weather.recommendation}") + + # Request 2: Override response_format at runtime with CityInfo + print("\n--- Request 2: Runtime override with CityInfo ---") + query2 = "Tell me about Tokyo." + print(f"User: {query2}") + + result2 = await agent.run(query2, options={"response_format": CityInfo}) + + if isinstance(result2.value, CityInfo): + city = result2.value + print("Agent:") + print(f" City: {city.city_name}") + print(f" Population: {city.population}") + print(f" Country: {city.country}") finally: await client.beta.assistants.delete(agent.id)