diff --git a/python/packages/azure-ai/agent_framework_azure_ai/__init__.py b/python/packages/azure-ai/agent_framework_azure_ai/__init__.py index c0cd4d249c..e90f3e6337 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/__init__.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/__init__.py @@ -4,7 +4,7 @@ from ._agent_provider import AzureAIAgentsProvider from ._chat_client import AzureAIAgentClient, AzureAIAgentOptions -from ._client import AzureAIClient +from ._client import AzureAIClient, AzureAIProjectAgentOptions from ._project_provider import AzureAIProjectAgentProvider from ._shared import AzureAISettings @@ -18,6 +18,7 @@ "AzureAIAgentOptions", "AzureAIAgentsProvider", "AzureAIClient", + "AzureAIProjectAgentOptions", "AzureAIProjectAgentProvider", "AzureAISettings", "__version__", diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py index 8bc5318b99..cad5e32e02 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -2,7 +2,7 @@ import sys from collections.abc import Callable, Mapping, MutableMapping, MutableSequence, Sequence -from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypedDict, TypeVar, cast +from typing import Any, ClassVar, Generic, TypedDict, TypeVar, cast from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, @@ -20,12 +20,14 @@ ) from agent_framework.exceptions import ServiceInitializationError from agent_framework.observability import use_instrumentation +from agent_framework.openai import OpenAIResponsesOptions from agent_framework.openai._responses_client import OpenAIBaseResponsesClient from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ( MCPTool, PromptAgentDefinition, PromptAgentDefinitionText, + RaiConfig, ) from azure.core.credentials_async import AsyncTokenCredential from azure.core.exceptions import ResourceNotFoundError @@ -33,9 +35,6 @@ from ._shared import AzureAISettings, create_text_format_config -if TYPE_CHECKING: - from agent_framework.openai import OpenAIResponsesOptions - if sys.version_info >= (3, 13): from typing import TypeVar # type: ignore # pragma: no cover else: @@ -52,10 +51,18 @@ logger = get_logger("agent_framework.azure") + +class AzureAIProjectAgentOptions(OpenAIResponsesOptions): + """Azure AI Project Agent options.""" + + rai_config: RaiConfig + """Configuration for Responsible AI (RAI) content filtering and safety features.""" + + TAzureAIClientOptions = TypeVar( "TAzureAIClientOptions", bound=TypedDict, # type: ignore[valid-type] - default="OpenAIResponsesOptions", + default="AzureAIProjectAgentOptions", covariant=True, ) @@ -397,6 +404,7 @@ async def _prepare_options( "model", "tools", "response_format", + "rai_config", "temperature", "top_p", "text", 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 edad03f5b4..fb1db84824 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 @@ -2,7 +2,7 @@ import sys from collections.abc import Callable, MutableMapping, Sequence -from typing import TYPE_CHECKING, Any, Generic, TypedDict +from typing import Any, Generic, TypedDict from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, @@ -26,12 +26,9 @@ from azure.core.credentials_async import AsyncTokenCredential from pydantic import ValidationError -from ._client import AzureAIClient +from ._client import AzureAIClient, AzureAIProjectAgentOptions from ._shared import AzureAISettings, create_text_format_config, from_azure_ai_tools, to_azure_ai_tools -if TYPE_CHECKING: - from agent_framework.openai import OpenAIResponsesOptions - if sys.version_info >= (3, 13): from typing import Self, TypeVar # pragma: no cover else: @@ -46,7 +43,7 @@ TOptions_co = TypeVar( "TOptions_co", bound=TypedDict, # type: ignore[valid-type] - default="OpenAIResponsesOptions", + default="AzureAIProjectAgentOptions", covariant=True, ) @@ -193,9 +190,10 @@ async def create_agent( "or set 'AZURE_AI_MODEL_DEPLOYMENT_NAME' environment variable." ) - # Extract response_format from default_options if present + # Extract options from default_options if present opts = dict(default_options) if default_options else {} response_format = opts.get("response_format") + rai_config = opts.get("rai_config") args: dict[str, Any] = {"model": resolved_model} @@ -205,6 +203,8 @@ async def create_agent( args["text"] = PromptAgentDefinitionText( format=create_text_format_config(response_format) # type: ignore[arg-type] ) + if rai_config: + args["rai_config"] = rai_config # Normalize tools once and reuse for both Azure AI API and ChatAgent normalized_tools = normalize_tools(tools) diff --git a/python/packages/azure-ai/tests/test_provider.py b/python/packages/azure-ai/tests/test_provider.py index e3dfa0995a..627f5a96ea 100644 --- a/python/packages/azure-ai/tests/test_provider.py +++ b/python/packages/azure-ai/tests/test_provider.py @@ -207,6 +207,49 @@ async def test_provider_create_agent_missing_model(mock_project_client: MagicMoc await provider.create_agent(name="test-agent") +async def test_provider_create_agent_with_rai_config( + mock_project_client: MagicMock, + azure_ai_unit_test_env: dict[str, str], +) -> None: + """Test AzureAIProjectAgentProvider.create_agent passes rai_config from default_options.""" + with patch("agent_framework_azure_ai._project_provider.AzureAISettings") as mock_settings: + mock_settings.return_value.project_endpoint = azure_ai_unit_test_env["AZURE_AI_PROJECT_ENDPOINT"] + mock_settings.return_value.model_deployment_name = azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"] + + provider = AzureAIProjectAgentProvider(project_client=mock_project_client) + + # Mock agent creation response + mock_agent_version = MagicMock(spec=AgentVersionDetails) + mock_agent_version.id = "agent-id" + mock_agent_version.name = "test-agent" + mock_agent_version.version = "1.0" + mock_agent_version.description = None + mock_agent_version.definition = MagicMock(spec=PromptAgentDefinition) + mock_agent_version.definition.model = "gpt-4" + mock_agent_version.definition.instructions = None + mock_agent_version.definition.temperature = None + mock_agent_version.definition.top_p = None + mock_agent_version.definition.tools = [] + + mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent_version) + + # Create a mock RaiConfig-like object + mock_rai_config = MagicMock() + mock_rai_config.rai_policy_name = "policy-name" + + # Call create_agent with rai_config in default_options + await provider.create_agent( + name="test-agent", + model="gpt-4", + default_options={"rai_config": mock_rai_config}, + ) + + # Verify rai_config was passed to PromptAgentDefinition + call_args = mock_project_client.agents.create_version.call_args + definition = call_args[1]["definition"] + assert definition.rai_config is mock_rai_config + + async def test_provider_get_agent_with_name(mock_project_client: MagicMock) -> None: """Test AzureAIProjectAgentProvider.get_agent with name parameter.""" provider = AzureAIProjectAgentProvider(project_client=mock_project_client) diff --git a/python/packages/core/agent_framework/azure/__init__.py b/python/packages/core/agent_framework/azure/__init__.py index ea94d83f0e..21ca71d85b 100644 --- a/python/packages/core/agent_framework/azure/__init__.py +++ b/python/packages/core/agent_framework/azure/__init__.py @@ -9,6 +9,7 @@ "AgentResponseCallbackProtocol": ("agent_framework_azurefunctions", "agent-framework-azurefunctions"), "AzureAIAgentClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), "AzureAIAgentOptions": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "AzureAIProjectAgentOptions": ("agent_framework_azure_ai", "agent-framework-azure-ai"), "AzureAIClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), "AzureAIProjectAgentProvider": ("agent_framework_azure_ai", "agent-framework-azure-ai"), "AzureAISearchContextProvider": ("agent_framework_azure_ai_search", "agent-framework-azure-ai-search"), diff --git a/python/packages/core/agent_framework/azure/__init__.pyi b/python/packages/core/agent_framework/azure/__init__.pyi index 155ad5067f..a7e311f315 100644 --- a/python/packages/core/agent_framework/azure/__init__.pyi +++ b/python/packages/core/agent_framework/azure/__init__.pyi @@ -4,6 +4,7 @@ from agent_framework_azure_ai import ( AzureAIAgentClient, AzureAIAgentsProvider, AzureAIClient, + AzureAIProjectAgentOptions, AzureAIProjectAgentProvider, AzureAISettings, ) @@ -28,6 +29,7 @@ __all__ = [ "AzureAIAgentClient", "AzureAIAgentsProvider", "AzureAIClient", + "AzureAIProjectAgentOptions", "AzureAIProjectAgentProvider", "AzureAISearchContextProvider", "AzureAISearchSettings", diff --git a/python/samples/getting_started/agents/azure_ai/README.md b/python/samples/getting_started/agents/azure_ai/README.md index f60b64cf18..719f233b36 100644 --- a/python/samples/getting_started/agents/azure_ai/README.md +++ b/python/samples/getting_started/agents/azure_ai/README.md @@ -17,6 +17,7 @@ This folder contains examples demonstrating different ways to create and use age | [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use the `HostedCodeInterpreterTool` with Azure AI agents to write and execute Python code for mathematical problem solving and data analysis. | | [`azure_ai_with_code_interpreter_file_generation.py`](azure_ai_with_code_interpreter_file_generation.py) | Shows how to retrieve file IDs from code interpreter generated files using both streaming and non-streaming approaches. | | [`azure_ai_with_code_interpreter_file_download.py`](azure_ai_with_code_interpreter_file_download.py) | Shows how to download files generated by code interpreter using the OpenAI containers API. | +| [`azure_ai_with_content_filtering.py`](azure_ai_with_content_filtering.py) | Shows how to enable content filtering (RAI policy) on Azure AI agents using `RaiConfig`. Requires creating an RAI policy in Azure AI Foundry portal first. | | [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with a pre-existing agent by providing the agent name and version to the Azure AI client. Demonstrates agent reuse patterns for production scenarios. | | [`azure_ai_with_existing_conversation.py`](azure_ai_with_existing_conversation.py) | Demonstrates how to use an existing conversation created on the service side with Azure AI agents. Shows two approaches: specifying conversation ID at the client level and using AgentThread with an existing conversation ID. | | [`azure_ai_with_application_endpoint.py`](azure_ai_with_application_endpoint.py) | Demonstrates calling the Azure AI application-scoped endpoint. | diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_content_filtering.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_content_filtering.py new file mode 100644 index 0000000000..72597b1cd8 --- /dev/null +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_content_filtering.py @@ -0,0 +1,66 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio + +from agent_framework.azure import AzureAIProjectAgentProvider +from azure.ai.projects.models import RaiConfig +from azure.identity.aio import AzureCliCredential + +""" +Azure AI Agent with Content Filtering (RAI Policy) Example + +This sample demonstrates how to enable content filtering on Azure AI agents using RaiConfig. + +Prerequisites: +1. Create an RAI Policy in Azure AI Foundry portal: + - Go to Azure AI Foundry > Your Project > Guardrails + Controls > Content Filters + - Create a new content filter or use an existing one + - Note the policy name + +2. Set environment variables: + - AZURE_AI_PROJECT_ENDPOINT: Your Azure AI Foundry project endpoint + - AZURE_AI_MODEL_DEPLOYMENT_NAME: Your model deployment name + +3. Run `az login` to authenticate +""" + + +async def main() -> None: + print("=== Azure AI Agent with Content Filtering ===\n") + + # Replace with your RAI policy from Azure AI Foundry portal + rai_policy_name = ( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/" + "Microsoft.CognitiveServices/accounts/{accountName}/raiPolicies/{policyName}" + ) + + async with ( + AzureCliCredential() as credential, + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + # Create agent with content filtering enabled via default_options + agent = await provider.create_agent( + name="ContentFilteredAgent", + instructions="You are a helpful assistant.", + default_options={"rai_config": RaiConfig(rai_policy_name=rai_policy_name)}, + ) + + # Test with a normal query + query = "What is the capital of France?" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result}\n") + + # Test with a query that might trigger content filtering + # (depending on your RAI policy configuration) + query2 = "Tell me something inappropriate." + print(f"User: {query2}") + try: + result2 = await agent.run(query2) + print(f"Agent: {result2}\n") + except Exception as e: + print(f"Content filter triggered: {e}\n") + + +if __name__ == "__main__": + asyncio.run(main())