From e03f6ac7fdc3f49ac664ab481d89c5409d2ab097 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Wed, 31 Dec 2025 13:53:21 -0800 Subject: [PATCH 01/16] Added get_agent method to Azure AI V2 --- .../agent_framework_azure_ai/__init__.py | 4 +- .../agent_framework_azure_ai/_client.py | 271 +++++++++++++++++- .../azure-ai/tests/test_azure_ai_client.py | 191 +++++++++++- .../core/agent_framework/azure/__init__.py | 2 + .../core/agent_framework/azure/__init__.pyi | 4 +- .../azure_ai/azure_ai_with_existing_agent.py | 91 ++++-- 6 files changed, 526 insertions(+), 37 deletions(-) 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 cf2423693d..882fe4bf3b 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/__init__.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/__init__.py @@ -3,7 +3,7 @@ import importlib.metadata from ._chat_client import AzureAIAgentClient -from ._client import AzureAIClient +from ._client import AzureAIAgentProvider, AzureAIClient, get_agent from ._shared import AzureAISettings try: @@ -13,7 +13,9 @@ __all__ = [ "AzureAIAgentClient", + "AzureAIAgentProvider", "AzureAIClient", "AzureAISettings", "__version__", + "get_agent", ] 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 e10fc19068..ebee1e3822 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -1,15 +1,24 @@ # Copyright (c) Microsoft. All rights reserved. import sys -from collections.abc import Mapping, MutableSequence -from typing import Any, ClassVar, TypeVar +from collections.abc import Callable, Mapping, MutableMapping, MutableSequence, Sequence +from typing import Any, ClassVar, TypeVar, cast from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, + AIFunction, + ChatAgent, ChatMessage, ChatOptions, + Contents, + HostedCodeInterpreterTool, + HostedFileContent, + HostedFileSearchTool, HostedMCPTool, + HostedVectorStoreContent, + HostedWebSearchTool, TextContent, + ToolProtocol, get_logger, use_chat_middleware, use_function_invocation, @@ -19,12 +28,20 @@ from agent_framework.openai._responses_client import OpenAIBaseResponsesClient from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ( + AgentObject, + AgentReference, + AgentVersionObject, + CodeInterpreterTool, + FileSearchTool, + FunctionTool, MCPTool, PromptAgentDefinition, PromptAgentDefinitionText, ResponseTextFormatConfigurationJsonObject, ResponseTextFormatConfigurationJsonSchema, ResponseTextFormatConfigurationText, + Tool, + WebSearchPreviewTool, ) from azure.core.credentials_async import AsyncTokenCredential from azure.core.exceptions import ResourceNotFoundError @@ -456,3 +473,253 @@ def _prepare_mcp_tool(tool: HostedMCPTool) -> MCPTool: # type: ignore[override] mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}} return mcp + + +class AzureAIAgentProvider: + """Azure AI Agent provider.""" + + def __init__( + self, + *, + project_client: AIProjectClient | None = None, + project_endpoint: str | None = None, + model_deployment_name: str | None = None, + credential: AsyncTokenCredential | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ) -> None: + """Initialize an Azure AI Agent provider. + + Keyword Args: + project_client: An existing AIProjectClient to use. If not provided, one will be created. + project_endpoint: The Azure AI Project endpoint URL. + Can also be set via environment variable AZURE_AI_PROJECT_ENDPOINT. + Ignored when a project_client is passed. + model_deployment_name: The model deployment name to use for agent creation. + Can also be set via environment variable AZURE_AI_MODEL_DEPLOYMENT_NAME. + credential: Azure async credential to use for authentication. + env_file_path: Path to environment file for loading settings. + env_file_encoding: Encoding of the environment file. + """ + try: + azure_ai_settings = AzureAISettings( + project_endpoint=project_endpoint, + model_deployment_name=model_deployment_name, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create Azure AI settings.", ex) from ex + + # If no project_client is provided, create one + if project_client is None: + if not azure_ai_settings.project_endpoint: + raise ServiceInitializationError( + "Azure AI project endpoint is required. Set via 'project_endpoint' parameter " + "or 'AZURE_AI_PROJECT_ENDPOINT' environment variable." + ) + + # Use provided credential + if not credential: + raise ServiceInitializationError("Azure credential is required when project_client is not provided.") + project_client = AIProjectClient( + endpoint=azure_ai_settings.project_endpoint, + credential=credential, + user_agent=AGENT_FRAMEWORK_USER_AGENT, + ) + + # Initialize instance variables + self.project_client = project_client + self.credential = credential + self.model_id = azure_ai_settings.model_deployment_name + + async def get_agent( + self, + agent_reference: AgentReference | None = None, + agent_name: str | None = None, + agent_object: AgentObject | None = None, + tools: ToolProtocol + | Callable[..., Any] + | MutableMapping[str, Any] + | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | None = None, + ) -> ChatAgent: + """Retrieves an existing Azure AI agent and converts it into a ChatAgent instance. + + Args: + agent_reference: Optional reference containing the agent's name and specific version. + agent_name: Optional name of the agent to retrieve (if reference is not provided). + agent_object: Optional pre-fetched agent object. + tools: A collection of tools to be made available to the agent. + + Returns: + ChatAgent: An initialized `ChatAgent` configured with the retrieved agent's settings and tools. + """ + return await get_agent( + project_client=self.project_client, + agent_reference=agent_reference, + agent_name=agent_name, + agent_object=agent_object, + tools=tools, + ) + + +async def get_agent( + project_client: AIProjectClient, + agent_reference: AgentReference | None = None, + agent_name: str | None = None, + agent_object: AgentObject | None = None, + tools: ToolProtocol + | Callable[..., Any] + | MutableMapping[str, Any] + | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | None = None, +) -> ChatAgent: + """Retrieves an existing Azure AI agent and converts it into a ChatAgent instance. + + Args: + project_client: The client used to interact with the Azure AI project. + agent_reference: Optional reference containing the agent's name and specific version. + agent_name: Optional name of the agent to retrieve (if reference is not provided). + agent_object: Optional pre-fetched agent object. + tools: A collection of tools to be made available to the agent. + + Returns: + ChatAgent: An initialized `ChatAgent` configured with the retrieved agent's settings and tools. + """ + existing_agent: AgentVersionObject + + if agent_reference and agent_reference.version: + existing_agent = await project_client.agents.get_version( + agent_name=agent_reference.name, agent_version=agent_reference.version + ) + else: + if name := (agent_reference.name if agent_reference else agent_name): + agent_object = await project_client.agents.get(agent_name=name) + + if not agent_object: + raise ValueError("Either agent_reference or agent_name or agent_object must be provided to get an agent.") + + existing_agent = agent_object.versions.latest + + if not isinstance(existing_agent.definition, PromptAgentDefinition): + raise ValueError("Agent definition must be PromptAgentDefinition to get a ChatAgent.") + + # Convert tools to unified format and validate function tools. + provided_tools = ChatOptions(tools=tools).tools or [] + provided_tool_names = {tool.name for tool in provided_tools if isinstance(tool, AIFunction)} + + # If function tools exist in agent definition but were not provided to this method, + # we need to raise an error, as it won't be possible to invoke the function. + missing_tools = [ + tool.name + for tool in (existing_agent.definition.tools or []) + if isinstance(tool, FunctionTool) and tool.name not in provided_tool_names + ] + + if missing_tools: + raise ValueError( + f"The following prompt agent definition required tools were not provided: {', '.join(missing_tools)}" + ) + + client = AzureAIClient( + project_client=project_client, + agent_name=existing_agent.name, + agent_version=existing_agent.version, + agent_description=existing_agent.description, + ) + + return ChatAgent( + chat_client=client, + id=existing_agent.id, + name=existing_agent.name, + description=existing_agent.description, + instructions=existing_agent.definition.instructions, + model_id=existing_agent.definition.model, + temperature=existing_agent.definition.temperature, + top_p=existing_agent.definition.top_p, + tools=_parse_tools(existing_agent.definition.tools), + ) + + +def _parse_tools(tools: Sequence[Tool | dict[str, Any]] | None) -> list[ToolProtocol | dict[str, Any]]: + """Parses and converts a sequence of Azure AI tools into Agent Framework compatible tools. + + Args: + tools: A sequence of tool objects or dictionaries + defining the tools to be parsed. Can be None. + + Returns: + list[ToolProtocol | dict[str, Any]]: A list of converted tools compatible with the + Agent Framework. + """ + agent_tools: list[ToolProtocol | dict[str, Any]] = [] + if not tools: + return agent_tools + for tool in tools: + # Handle raw dictionary tools + tool_dict = tool if isinstance(tool, dict) else dict(tool) + tool_type = tool_dict.get("type") + + if tool_type == "mcp": + mcp_tool = cast(MCPTool, tool_dict) + approval_mode = None + if require_approval := mcp_tool.get("require_approval"): + if require_approval == "always": + approval_mode = "always_require" + elif require_approval == "never": + approval_mode = "never_require" + elif isinstance(require_approval, dict): + approval_mode = {} + if "always" in require_approval: + approval_mode["always_require_approval"] = set(require_approval["always"].get("tool_names", [])) # type: ignore + if "never" in require_approval: + approval_mode["never_require_approval"] = set(require_approval["never"].get("tool_names", [])) # type: ignore + + agent_tools.append( + HostedMCPTool( + name=mcp_tool.get("server_label", "").replace("_", " "), + url=mcp_tool.get("server_url", ""), + description=mcp_tool.get("server_description"), + headers=mcp_tool.get("headers"), + allowed_tools=mcp_tool.get("allowed_tools"), + approval_mode=approval_mode, # type: ignore + ) + ) + elif tool_type == "code_interpreter": + ci_tool = cast(CodeInterpreterTool, tool_dict) + container = ci_tool.get("container", {}) + ci_inputs: list[Contents] = [] + if "file_ids" in container: + for file_id in container["file_ids"]: + ci_inputs.append(HostedFileContent(file_id=file_id)) + + agent_tools.append(HostedCodeInterpreterTool(inputs=ci_inputs if ci_inputs else None)) # type: ignore + elif tool_type == "file_search": + fs_tool = cast(FileSearchTool, tool_dict) + fs_inputs: list[Contents] = [] + if "vector_store_ids" in fs_tool: + for vs_id in fs_tool["vector_store_ids"]: + fs_inputs.append(HostedVectorStoreContent(vector_store_id=vs_id)) + + agent_tools.append( + HostedFileSearchTool( + inputs=fs_inputs if fs_inputs else None, # type: ignore + max_results=fs_tool.get("max_num_results"), + ) + ) + elif tool_type == "web_search" or tool_type == "web_search_preview": + ws_tool = cast(WebSearchPreviewTool, tool_dict) + additional_properties: dict[str, Any] = {} + if user_location := ws_tool.get("user_location"): + additional_properties["user_location"] = { + "city": user_location.get("city"), + "country": user_location.get("country"), + "region": user_location.get("region"), + "timezone": user_location.get("timezone"), + } + + agent_tools.append(HostedWebSearchTool(additional_properties=additional_properties)) + else: + agent_tools.append(tool_dict) + return agent_tools diff --git a/python/packages/azure-ai/tests/test_azure_ai_client.py b/python/packages/azure-ai/tests/test_azure_ai_client.py index 028e8fbdb8..46ff7420cb 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_client.py @@ -14,13 +14,29 @@ ChatClientProtocol, ChatMessage, ChatOptions, + HostedCodeInterpreterTool, + HostedFileContent, + HostedFileSearchTool, + HostedMCPTool, + HostedVectorStoreContent, + HostedWebSearchTool, Role, TextContent, ) from agent_framework.exceptions import ServiceInitializationError from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ( + AgentReference, + AgentVersionObject, + ApproximateLocation, + CodeInterpreterTool, + CodeInterpreterToolAuto, + FileSearchTool, + FunctionTool, + MCPTool, + PromptAgentDefinition, ResponseTextFormatConfigurationJsonSchema, + WebSearchPreviewTool, ) from azure.identity.aio import AzureCliCredential from openai.types.responses.parsed_response import ParsedResponse @@ -28,6 +44,7 @@ from pydantic import BaseModel, ConfigDict, Field, ValidationError from agent_framework_azure_ai import AzureAIClient, AzureAISettings +from agent_framework_azure_ai._client import _parse_tools, get_agent # type: ignore skip_if_azure_ai_integration_tests_disabled = pytest.mark.skipif( os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true" @@ -301,7 +318,7 @@ async def test_azure_ai_client_prepare_options_basic(mock_project_client: MagicM return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"}, ), ): - run_options = await client._prepare_options(messages, chat_options) + run_options = await client._prepare_options(messages, chat_options) # type: ignore assert "extra_body" in run_options assert run_options["extra_body"]["agent"]["name"] == "test-agent" @@ -336,7 +353,7 @@ async def test_azure_ai_client_prepare_options_with_application_endpoint( return_value={"name": "test-agent", "version": "1", "type": "agent_reference"}, ), ): - run_options = await client._prepare_options(messages, chat_options) + run_options = await client._prepare_options(messages, chat_options) # type: ignore if expects_agent: assert "extra_body" in run_options @@ -376,7 +393,7 @@ async def test_azure_ai_client_prepare_options_with_application_project_client( return_value={"name": "test-agent", "version": "1", "type": "agent_reference"}, ), ): - run_options = await client._prepare_options(messages, chat_options) + run_options = await client._prepare_options(messages, chat_options) # type: ignore if expects_agent: assert "extra_body" in run_options @@ -392,7 +409,7 @@ async def test_azure_ai_client_initialize_client(mock_project_client: MagicMock) mock_openai_client = MagicMock() mock_project_client.get_openai_client = MagicMock(return_value=mock_openai_client) - await client._initialize_client() + await client._initialize_client() # type: ignore assert client.client is mock_openai_client mock_project_client.get_openai_client.assert_called_once() @@ -736,7 +753,7 @@ async def test_azure_ai_client_prepare_options_excludes_response_format( return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"}, ), ): - run_options = await client._prepare_options(messages, chat_options) + run_options = await client._prepare_options(messages, chat_options) # type: ignore # response_format should be excluded from final run options assert "response_format" not in run_options @@ -756,7 +773,7 @@ def test_get_conversation_id_with_store_true_and_conversation_id() -> None: mock_conversation.id = "conv_67890" mock_response.conversation = mock_conversation - result = client._get_conversation_id(mock_response, store=True) + result = client._get_conversation_id(mock_response, store=True) # type: ignore assert result == "conv_67890" @@ -770,7 +787,7 @@ def test_get_conversation_id_with_store_true_and_no_conversation() -> None: mock_response.id = "resp_12345" mock_response.conversation = None - result = client._get_conversation_id(mock_response, store=True) + result = client._get_conversation_id(mock_response, store=True) # type: ignore assert result == "resp_12345" @@ -786,7 +803,7 @@ def test_get_conversation_id_with_store_true_and_empty_conversation_id() -> None mock_conversation.id = "" mock_response.conversation = mock_conversation - result = client._get_conversation_id(mock_response, store=True) + result = client._get_conversation_id(mock_response, store=True) # type: ignore assert result == "resp_12345" @@ -802,7 +819,7 @@ def test_get_conversation_id_with_store_false() -> None: mock_conversation.id = "conv_67890" mock_response.conversation = mock_conversation - result = client._get_conversation_id(mock_response, store=False) + result = client._get_conversation_id(mock_response, store=False) # type: ignore assert result is None @@ -818,7 +835,7 @@ def test_get_conversation_id_with_parsed_response_and_store_true() -> None: mock_conversation.id = "conv_parsed_67890" mock_response.conversation = mock_conversation - result = client._get_conversation_id(mock_response, store=True) + result = client._get_conversation_id(mock_response, store=True) # type: ignore assert result == "conv_parsed_67890" @@ -832,7 +849,7 @@ def test_get_conversation_id_with_parsed_response_no_conversation() -> None: mock_response.id = "resp_parsed_12345" mock_response.conversation = None - result = client._get_conversation_id(mock_response, store=True) + result = client._get_conversation_id(mock_response, store=True) # type: ignore assert result == "resp_parsed_12345" @@ -922,3 +939,155 @@ async def test_azure_ai_chat_client_agent_with_tools() -> None: assert response.text is not None assert len(response.text) > 0 assert any(word in response.text.lower() for word in ["sunny", "25"]) + + +@pytest.mark.asyncio +async def test_get_agent_parameter_handling(mock_project_client: MagicMock) -> None: + """Test get_agent parameter handling.""" + mock_project_client.agents = AsyncMock() + + # Test with agent_reference + agent_reference = AgentReference(name="test-agent", version="1.0") + mock_agent_version = MagicMock(spec=AgentVersionObject) + mock_agent_version.name = "test-agent" + mock_agent_version.version = "1.0" + mock_agent_version.description = "Test Agent" + mock_agent_version.definition = PromptAgentDefinition(model="test-model") + mock_agent_version.definition.model = "gpt-4" + mock_agent_version.definition.instructions = "Test instructions" + mock_agent_version.definition.tools = [] + + mock_project_client.agents.get_version.return_value = mock_agent_version + + agent = await get_agent(project_client=mock_project_client, agent_reference=agent_reference) + + assert agent.name == "test-agent" + mock_project_client.agents.get_version.assert_called_with(agent_name="test-agent", agent_version="1.0") + + # Test with agent_name + mock_agent_object = MagicMock() + mock_agent_object.versions = MagicMock() + mock_agent_object.versions.latest = mock_agent_version + mock_project_client.agents.get.return_value = mock_agent_object + + agent = await get_agent(project_client=mock_project_client, agent_name="test-agent") + + assert agent.name == "test-agent" + mock_project_client.agents.get.assert_called_with(agent_name="test-agent") + + # Test with agent_object + agent = await get_agent(project_client=mock_project_client, agent_object=mock_agent_object) + + assert agent.name == "test-agent" + + +@pytest.mark.asyncio +async def test_get_agent_missing_parameters(mock_project_client: MagicMock) -> None: + """Test get_agent missing parameters.""" + + with pytest.raises(ValueError, match="Either agent_reference or agent_name or agent_object must be provided"): + await get_agent(project_client=mock_project_client) + + +@pytest.mark.asyncio +async def test_get_agent_missing_tools(mock_project_client: MagicMock) -> None: + """Test get_agent missing tools.""" + mock_project_client.agents = AsyncMock() + + mock_agent_version = MagicMock(spec=AgentVersionObject) + mock_agent_version.name = "test-agent" + mock_agent_version.definition = MagicMock(spec=PromptAgentDefinition) + mock_agent_version.definition.tools = [ + FunctionTool(name="test_tool", parameters=[], strict=True, description="Test tool") + ] + + mock_agent_object = MagicMock() + mock_agent_object.versions = MagicMock() + mock_agent_object.versions.latest = mock_agent_version + + with pytest.raises( + ValueError, match="The following prompt agent definition required tools were not provided: test_tool" + ): + await get_agent(project_client=mock_project_client, agent_object=mock_agent_object) + + +def test_parse_tools() -> None: + """Test _parse_tools.""" + # Test MCP tool + mcp_tool = MCPTool(server_label="test_server", server_url="http://localhost:8080") + parsed_tools = _parse_tools([mcp_tool]) + assert len(parsed_tools) == 1 + assert isinstance(parsed_tools[0], HostedMCPTool) + assert parsed_tools[0].name == "test server" + assert str(parsed_tools[0].url).rstrip("/") == "http://localhost:8080" + + # Test Code Interpreter tool + ci_tool = CodeInterpreterTool(container=CodeInterpreterToolAuto(file_ids=["file-1"])) + parsed_tools = _parse_tools([ci_tool]) + assert len(parsed_tools) == 1 + assert isinstance(parsed_tools[0], HostedCodeInterpreterTool) + assert parsed_tools[0].inputs is not None + assert len(parsed_tools[0].inputs) == 1 + + tool_input = parsed_tools[0].inputs[0] + + assert tool_input and isinstance(tool_input, HostedFileContent) and tool_input.file_id == "file-1" + + # Test File Search tool + fs_tool = FileSearchTool(vector_store_ids=["vs-1"], max_num_results=5) + parsed_tools = _parse_tools([fs_tool]) + assert len(parsed_tools) == 1 + assert isinstance(parsed_tools[0], HostedFileSearchTool) + assert parsed_tools[0].inputs is not None + assert len(parsed_tools[0].inputs) == 1 + + tool_input = parsed_tools[0].inputs[0] + + assert tool_input and isinstance(tool_input, HostedVectorStoreContent) and tool_input.vector_store_id == "vs-1" + assert parsed_tools[0].max_results == 5 + + # Test Web Search tool + ws_tool = WebSearchPreviewTool( + user_location=ApproximateLocation(city="Seattle", country="US", region="WA", timezone="PST") + ) + parsed_tools = _parse_tools([ws_tool]) + assert len(parsed_tools) == 1 + assert isinstance(parsed_tools[0], HostedWebSearchTool) + assert parsed_tools[0].additional_properties + + user_location = parsed_tools[0].additional_properties["user_location"] + + assert user_location["city"] == "Seattle" + assert user_location["country"] == "US" + assert user_location["region"] == "WA" + assert user_location["timezone"] == "PST" + + +@pytest.mark.asyncio +async def test_get_agent_success(mock_project_client: MagicMock) -> None: + """Test get_agent success path.""" + mock_project_client.agents = AsyncMock() + + mock_agent_version = MagicMock(spec=AgentVersionObject) + mock_agent_version.id = "agent-id" + mock_agent_version.name = "test-agent" + mock_agent_version.description = "Test Agent" + mock_agent_version.version = "1.0" + mock_agent_version.definition = MagicMock(spec=PromptAgentDefinition) + mock_agent_version.definition.model = "gpt-4" + mock_agent_version.definition.instructions = "Test instructions" + mock_agent_version.definition.temperature = 0.7 + mock_agent_version.definition.top_p = 0.9 + + mock_project_client.agents.get_version.return_value = mock_agent_version + + agent_reference = AgentReference(name="test-agent", version="1.0") + agent = await get_agent(project_client=mock_project_client, agent_reference=agent_reference) + + assert agent.id == "agent-id" + assert agent.name == "test-agent" + assert agent.description == "Test Agent" + assert agent.chat_options.instructions == "Test instructions" + assert agent.chat_options.model_id == "gpt-4" + assert agent.chat_options.temperature == 0.7 + assert agent.chat_options.top_p == 0.9 diff --git a/python/packages/core/agent_framework/azure/__init__.py b/python/packages/core/agent_framework/azure/__init__.py index 7990361c97..95a63f1e05 100644 --- a/python/packages/core/agent_framework/azure/__init__.py +++ b/python/packages/core/agent_framework/azure/__init__.py @@ -9,6 +9,8 @@ "AgentResponseCallbackProtocol": ("agent_framework_azurefunctions", "agent-framework-azurefunctions"), "AzureAIAgentClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), "AzureAIClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "get_agent": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "AzureAIAgentProvider": ("agent_framework_azure_ai", "agent-framework-azure-ai"), "AzureAISearchContextProvider": ("agent_framework_azure_ai_search", "agent-framework-azure-ai-search"), "AzureAISearchSettings": ("agent_framework_azure_ai_search", "agent-framework-azure-ai-search"), "AzureAISettings": ("agent_framework_azure_ai", "agent-framework-azure-ai"), diff --git a/python/packages/core/agent_framework/azure/__init__.pyi b/python/packages/core/agent_framework/azure/__init__.pyi index add9ea1130..f57c95b4aa 100644 --- a/python/packages/core/agent_framework/azure/__init__.pyi +++ b/python/packages/core/agent_framework/azure/__init__.pyi @@ -1,6 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework_azure_ai import AzureAIAgentClient, AzureAIClient, AzureAISettings +from agent_framework_azure_ai import AzureAIAgentClient, AzureAIAgentProvider, AzureAIClient, AzureAISettings, get_agent from agent_framework_azure_ai_search import AzureAISearchContextProvider, AzureAISearchSettings from agent_framework_azurefunctions import ( AgentCallbackContext, @@ -20,6 +20,7 @@ __all__ = [ "AgentFunctionApp", "AgentResponseCallbackProtocol", "AzureAIAgentClient", + "AzureAIAgentProvider", "AzureAIClient", "AzureAISearchContextProvider", "AzureAISearchSettings", @@ -29,5 +30,6 @@ __all__ = [ "AzureOpenAIResponsesClient", "AzureOpenAISettings", "DurableAIAgent", + "get_agent", "get_entra_auth_token", ] diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py index 7486b19ec7..2d7e127728 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py @@ -4,7 +4,7 @@ import os from agent_framework import ChatAgent -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIAgentProvider, get_agent from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import PromptAgentDefinition from azure.identity.aio import AzureCliCredential @@ -12,19 +12,23 @@ """ Azure AI Agent with Existing Agent Example -This sample demonstrates working with pre-existing Azure AI Agents by providing -agent name and version, showing agent reuse patterns for production scenarios. +This sample demonstrates working with pre-existing Azure AI Agents by using get_agent method +and AzureAIAgentProvider class, showing agent reuse patterns for production scenarios. """ -async def main() -> None: +async def using_method() -> None: + print("=== Get existing Azure AI agent with get_agent method ===") + # Create the client async with ( AzureCliCredential() as credential, AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, ): + # Create remote agent azure_ai_agent = await project_client.agents.create_version( agent_name="MyNewTestAgent", + description="Agent for testing purposes.", definition=PromptAgentDefinition( model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], # Setting specific requirements to verify that this agent is used. @@ -32,27 +36,65 @@ async def main() -> None: ), ) - chat_client = AzureAIClient( - project_client=project_client, - agent_name=azure_ai_agent.name, - # Property agent_version is required for existing agents. - # If this property is not configured, the client will try to create a new agent using - # provided agent_name. - # It's also possible to leave agent_version empty but set use_latest_version=True. - # This will pull latest available agent version and use that version for operations. - agent_version=azure_ai_agent.version, + try: + # Get newly created agent as ChatAgent by using get_agent method + agent: ChatAgent = await get_agent(project_client=project_client, agent_name=azure_ai_agent.name) + + # Verify agent properties + print(f"Agent ID: {agent.id}") + print(f"Agent name: {agent.name}") + print(f"Agent description: {agent.description}") + print(f"Agent instructions: {agent.chat_options.instructions}") + + query = "How are you?" + print(f"User: {query}") + result = await agent.run(query) + # Response that indicates that previously created agent was used: + # "I'm here and ready to help you! How can I assist you today? [END]" + print(f"Agent: {result}\n") + finally: + # Clean up the agent manually + await project_client.agents.delete_version( + agent_name=azure_ai_agent.name, agent_version=azure_ai_agent.version + ) + + +async def using_provider() -> None: + print("=== Get existing Azure AI agent with AzureAIAgentProvider class ===") + + # Create the client + async with ( + AzureCliCredential() as credential, + AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, + ): + # Create remote agent + azure_ai_agent = await project_client.agents.create_version( + agent_name="MyNewTestAgent", + description="Agent for testing purposes.", + definition=PromptAgentDefinition( + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + # Setting specific requirements to verify that this agent is used. + instructions="End each response with [END].", + ), ) try: - async with ChatAgent( - chat_client=chat_client, - ) as agent: - query = "How are you?" - print(f"User: {query}") - result = await agent.run(query) - # Response that indicates that previously created agent was used: - # "I'm here and ready to help you! How can I assist you today? [END]" - print(f"Agent: {result}\n") + # Get newly created agent as ChatAgent by using AzureAIAgentProvider class + provider = AzureAIAgentProvider(project_client=project_client) + agent: ChatAgent = await provider.get_agent(agent_name=azure_ai_agent.name) + + # Verify agent properties + print(f"Agent ID: {agent.id}") + print(f"Agent name: {agent.name}") + print(f"Agent description: {agent.description}") + print(f"Agent instructions: {agent.chat_options.instructions}") + + query = "How are you?" + print(f"User: {query}") + result = await agent.run(query) + # Response that indicates that previously created agent was used: + # "I'm here and ready to help you! How can I assist you today? [END]" + print(f"Agent: {result}\n") finally: # Clean up the agent manually await project_client.agents.delete_version( @@ -60,5 +102,10 @@ async def main() -> None: ) +async def main() -> None: + await using_method() + await using_provider() + + if __name__ == "__main__": asyncio.run(main()) From 7829927db65c84ad62d243c9eac9ae79b53caed6 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:02:40 -0800 Subject: [PATCH 02/16] Small fixes --- python/packages/azure-ai/tests/test_azure_ai_client.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/python/packages/azure-ai/tests/test_azure_ai_client.py b/python/packages/azure-ai/tests/test_azure_ai_client.py index 46ff7420cb..b488c60ade 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_client.py @@ -941,7 +941,6 @@ async def test_azure_ai_chat_client_agent_with_tools() -> None: assert any(word in response.text.lower() for word in ["sunny", "25"]) -@pytest.mark.asyncio async def test_get_agent_parameter_handling(mock_project_client: MagicMock) -> None: """Test get_agent parameter handling.""" mock_project_client.agents = AsyncMock() @@ -981,7 +980,6 @@ async def test_get_agent_parameter_handling(mock_project_client: MagicMock) -> N assert agent.name == "test-agent" -@pytest.mark.asyncio async def test_get_agent_missing_parameters(mock_project_client: MagicMock) -> None: """Test get_agent missing parameters.""" @@ -989,7 +987,6 @@ async def test_get_agent_missing_parameters(mock_project_client: MagicMock) -> N await get_agent(project_client=mock_project_client) -@pytest.mark.asyncio async def test_get_agent_missing_tools(mock_project_client: MagicMock) -> None: """Test get_agent missing tools.""" mock_project_client.agents = AsyncMock() @@ -1063,7 +1060,6 @@ def test_parse_tools() -> None: assert user_location["timezone"] == "PST" -@pytest.mark.asyncio async def test_get_agent_success(mock_project_client: MagicMock) -> None: """Test get_agent success path.""" mock_project_client.agents = AsyncMock() From 235b31d1c957d888d87ce6b448141a4c6e62f15a Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Wed, 31 Dec 2025 14:18:57 -0800 Subject: [PATCH 03/16] Small fix --- python/packages/azure-ai/agent_framework_azure_ai/_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ebee1e3822..96cc96c405 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 Any, ClassVar, TypeVar, cast +from typing import Any, ClassVar, Literal, TypeVar, cast from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, @@ -663,7 +663,7 @@ def _parse_tools(tools: Sequence[Tool | dict[str, Any]] | None) -> list[ToolProt if tool_type == "mcp": mcp_tool = cast(MCPTool, tool_dict) - approval_mode = None + approval_mode: Literal["always_require", "never_require"] | dict[str, set[str]] | None = None if require_approval := mcp_tool.get("require_approval"): if require_approval == "always": approval_mode = "always_require" From 6b11a9eca7d4a03f13e53a4e3b779328a69b4c83 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 5 Jan 2026 13:56:07 -0800 Subject: [PATCH 04/16] Removed AzureAIAgentProvider --- .../agent_framework_azure_ai/__init__.py | 3 +- .../agent_framework_azure_ai/_client.py | 129 ++++++------------ .../core/agent_framework/azure/__init__.py | 1 - .../core/agent_framework/azure/__init__.pyi | 3 +- .../azure_ai/azure_ai_with_existing_agent.py | 54 +------- 5 files changed, 45 insertions(+), 145 deletions(-) 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 882fe4bf3b..530d7b16ca 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/__init__.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/__init__.py @@ -3,7 +3,7 @@ import importlib.metadata from ._chat_client import AzureAIAgentClient -from ._client import AzureAIAgentProvider, AzureAIClient, get_agent +from ._client import AzureAIClient, get_agent from ._shared import AzureAISettings try: @@ -13,7 +13,6 @@ __all__ = [ "AzureAIAgentClient", - "AzureAIAgentProvider", "AzureAIClient", "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 96cc96c405..9699a338b9 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -475,97 +475,8 @@ def _prepare_mcp_tool(tool: HostedMCPTool) -> MCPTool: # type: ignore[override] return mcp -class AzureAIAgentProvider: - """Azure AI Agent provider.""" - - def __init__( - self, - *, - project_client: AIProjectClient | None = None, - project_endpoint: str | None = None, - model_deployment_name: str | None = None, - credential: AsyncTokenCredential | None = None, - env_file_path: str | None = None, - env_file_encoding: str | None = None, - ) -> None: - """Initialize an Azure AI Agent provider. - - Keyword Args: - project_client: An existing AIProjectClient to use. If not provided, one will be created. - project_endpoint: The Azure AI Project endpoint URL. - Can also be set via environment variable AZURE_AI_PROJECT_ENDPOINT. - Ignored when a project_client is passed. - model_deployment_name: The model deployment name to use for agent creation. - Can also be set via environment variable AZURE_AI_MODEL_DEPLOYMENT_NAME. - credential: Azure async credential to use for authentication. - env_file_path: Path to environment file for loading settings. - env_file_encoding: Encoding of the environment file. - """ - try: - azure_ai_settings = AzureAISettings( - project_endpoint=project_endpoint, - model_deployment_name=model_deployment_name, - env_file_path=env_file_path, - env_file_encoding=env_file_encoding, - ) - except ValidationError as ex: - raise ServiceInitializationError("Failed to create Azure AI settings.", ex) from ex - - # If no project_client is provided, create one - if project_client is None: - if not azure_ai_settings.project_endpoint: - raise ServiceInitializationError( - "Azure AI project endpoint is required. Set via 'project_endpoint' parameter " - "or 'AZURE_AI_PROJECT_ENDPOINT' environment variable." - ) - - # Use provided credential - if not credential: - raise ServiceInitializationError("Azure credential is required when project_client is not provided.") - project_client = AIProjectClient( - endpoint=azure_ai_settings.project_endpoint, - credential=credential, - user_agent=AGENT_FRAMEWORK_USER_AGENT, - ) - - # Initialize instance variables - self.project_client = project_client - self.credential = credential - self.model_id = azure_ai_settings.model_deployment_name - - async def get_agent( - self, - agent_reference: AgentReference | None = None, - agent_name: str | None = None, - agent_object: AgentObject | None = None, - tools: ToolProtocol - | Callable[..., Any] - | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] - | None = None, - ) -> ChatAgent: - """Retrieves an existing Azure AI agent and converts it into a ChatAgent instance. - - Args: - agent_reference: Optional reference containing the agent's name and specific version. - agent_name: Optional name of the agent to retrieve (if reference is not provided). - agent_object: Optional pre-fetched agent object. - tools: A collection of tools to be made available to the agent. - - Returns: - ChatAgent: An initialized `ChatAgent` configured with the retrieved agent's settings and tools. - """ - return await get_agent( - project_client=self.project_client, - agent_reference=agent_reference, - agent_name=agent_name, - agent_object=agent_object, - tools=tools, - ) - - async def get_agent( - project_client: AIProjectClient, + project_client: AIProjectClient | None = None, agent_reference: AgentReference | None = None, agent_name: str | None = None, agent_object: AgentObject | None = None, @@ -574,21 +485,57 @@ async def get_agent( | MutableMapping[str, Any] | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] | None = None, + project_endpoint: str | None = None, + credential: AsyncTokenCredential | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, ) -> ChatAgent: """Retrieves an existing Azure AI agent and converts it into a ChatAgent instance. Args: - project_client: The client used to interact with the Azure AI project. + project_client: An existing AIProjectClient to use. If not provided, one will be created. agent_reference: Optional reference containing the agent's name and specific version. agent_name: Optional name of the agent to retrieve (if reference is not provided). agent_object: Optional pre-fetched agent object. tools: A collection of tools to be made available to the agent. + project_endpoint: The Azure AI Project endpoint URL. + Can also be set via environment variable AZURE_AI_PROJECT_ENDPOINT. + Ignored when a project_client is passed. + credential: Azure async credential to use for authentication. + env_file_path: Path to environment file for loading settings. + env_file_encoding: Encoding of the environment file. Returns: ChatAgent: An initialized `ChatAgent` configured with the retrieved agent's settings and tools. """ existing_agent: AgentVersionObject + try: + azure_ai_settings = AzureAISettings( + project_endpoint=project_endpoint, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create Azure AI settings.", ex) from ex + + # If no project_client is provided, create one + if project_client is None: + if not azure_ai_settings.project_endpoint: + raise ServiceInitializationError( + "Azure AI project endpoint is required. Set via 'project_endpoint' parameter " + "or 'AZURE_AI_PROJECT_ENDPOINT' environment variable." + ) + + # Use provided credential + if not credential: + raise ServiceInitializationError("Azure credential is required when project_client is not provided.") + project_client = AIProjectClient( + endpoint=azure_ai_settings.project_endpoint, + credential=credential, + user_agent=AGENT_FRAMEWORK_USER_AGENT, + ) + if agent_reference and agent_reference.version: existing_agent = await project_client.agents.get_version( agent_name=agent_reference.name, agent_version=agent_reference.version diff --git a/python/packages/core/agent_framework/azure/__init__.py b/python/packages/core/agent_framework/azure/__init__.py index 95a63f1e05..21d3d0f3d9 100644 --- a/python/packages/core/agent_framework/azure/__init__.py +++ b/python/packages/core/agent_framework/azure/__init__.py @@ -10,7 +10,6 @@ "AzureAIAgentClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), "AzureAIClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), "get_agent": ("agent_framework_azure_ai", "agent-framework-azure-ai"), - "AzureAIAgentProvider": ("agent_framework_azure_ai", "agent-framework-azure-ai"), "AzureAISearchContextProvider": ("agent_framework_azure_ai_search", "agent-framework-azure-ai-search"), "AzureAISearchSettings": ("agent_framework_azure_ai_search", "agent-framework-azure-ai-search"), "AzureAISettings": ("agent_framework_azure_ai", "agent-framework-azure-ai"), diff --git a/python/packages/core/agent_framework/azure/__init__.pyi b/python/packages/core/agent_framework/azure/__init__.pyi index f57c95b4aa..df74e86b68 100644 --- a/python/packages/core/agent_framework/azure/__init__.pyi +++ b/python/packages/core/agent_framework/azure/__init__.pyi @@ -1,6 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework_azure_ai import AzureAIAgentClient, AzureAIAgentProvider, AzureAIClient, AzureAISettings, get_agent +from agent_framework_azure_ai import AzureAIAgentClient, AzureAIClient, AzureAISettings, get_agent from agent_framework_azure_ai_search import AzureAISearchContextProvider, AzureAISearchSettings from agent_framework_azurefunctions import ( AgentCallbackContext, @@ -20,7 +20,6 @@ __all__ = [ "AgentFunctionApp", "AgentResponseCallbackProtocol", "AzureAIAgentClient", - "AzureAIAgentProvider", "AzureAIClient", "AzureAISearchContextProvider", "AzureAISearchSettings", diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py index 2d7e127728..f636d202f6 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py @@ -4,7 +4,7 @@ import os from agent_framework import ChatAgent -from agent_framework.azure import AzureAIAgentProvider, get_agent +from agent_framework.azure import get_agent from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import PromptAgentDefinition from azure.identity.aio import AzureCliCredential @@ -12,12 +12,12 @@ """ Azure AI Agent with Existing Agent Example -This sample demonstrates working with pre-existing Azure AI Agents by using get_agent method -and AzureAIAgentProvider class, showing agent reuse patterns for production scenarios. +This sample demonstrates working with pre-existing Azure AI Agents by using get_agent method, +showing agent reuse patterns for production scenarios. """ -async def using_method() -> None: +async def using_get_agent_method() -> None: print("=== Get existing Azure AI agent with get_agent method ===") # Create the client @@ -59,52 +59,8 @@ async def using_method() -> None: ) -async def using_provider() -> None: - print("=== Get existing Azure AI agent with AzureAIAgentProvider class ===") - - # Create the client - async with ( - AzureCliCredential() as credential, - AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, - ): - # Create remote agent - azure_ai_agent = await project_client.agents.create_version( - agent_name="MyNewTestAgent", - description="Agent for testing purposes.", - definition=PromptAgentDefinition( - model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - # Setting specific requirements to verify that this agent is used. - instructions="End each response with [END].", - ), - ) - - try: - # Get newly created agent as ChatAgent by using AzureAIAgentProvider class - provider = AzureAIAgentProvider(project_client=project_client) - agent: ChatAgent = await provider.get_agent(agent_name=azure_ai_agent.name) - - # Verify agent properties - print(f"Agent ID: {agent.id}") - print(f"Agent name: {agent.name}") - print(f"Agent description: {agent.description}") - print(f"Agent instructions: {agent.chat_options.instructions}") - - query = "How are you?" - print(f"User: {query}") - result = await agent.run(query) - # Response that indicates that previously created agent was used: - # "I'm here and ready to help you! How can I assist you today? [END]" - print(f"Agent: {result}\n") - finally: - # Clean up the agent manually - await project_client.agents.delete_version( - agent_name=azure_ai_agent.name, agent_version=azure_ai_agent.version - ) - - async def main() -> None: - await using_method() - await using_provider() + await using_get_agent_method() if __name__ == "__main__": From 7e819022149cac00e73c7c4ab15e12b8ffa1c1c4 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 5 Jan 2026 20:39:42 -0800 Subject: [PATCH 05/16] Added create_agent method --- .../agent_framework_azure_ai/_client.py | 389 ++++++++++++++---- .../azure-ai/tests/test_azure_ai_client.py | 14 +- 2 files changed, 317 insertions(+), 86 deletions(-) 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 9699a338b9..122d6f38a9 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -282,40 +282,6 @@ async def close(self) -> None: """Close the project_client.""" await self._close_client_if_needed() - def _create_text_format_config( - self, response_format: Any - ) -> ( - ResponseTextFormatConfigurationJsonSchema - | ResponseTextFormatConfigurationJsonObject - | ResponseTextFormatConfigurationText - ): - """Convert response_format into Azure text format configuration.""" - if isinstance(response_format, type) and issubclass(response_format, BaseModel): - return ResponseTextFormatConfigurationJsonSchema( - name=response_format.__name__, - schema=response_format.model_json_schema(), - ) - - if isinstance(response_format, Mapping): - format_config = self._convert_response_format(response_format) - format_type = format_config.get("type") - if format_type == "json_schema": - config_kwargs: dict[str, Any] = { - "name": format_config.get("name") or "response", - "schema": format_config["schema"], - } - if "strict" in format_config: - config_kwargs["strict"] = format_config["strict"] - if "description" in format_config: - config_kwargs["description"] = format_config["description"] - return ResponseTextFormatConfigurationJsonSchema(**config_kwargs) - if format_type == "json_object": - return ResponseTextFormatConfigurationJsonObject() - if format_type == "text": - return ResponseTextFormatConfigurationText() - - raise ServiceInvalidRequestError("response_format must be a Pydantic model or mapping.") - async def _get_agent_reference_or_create( self, run_options: dict[str, Any], messages_instructions: str | None ) -> dict[str, str]: @@ -360,7 +326,7 @@ async def _get_agent_reference_or_create( if "response_format" in run_options: response_format = run_options["response_format"] - args["text"] = PromptAgentDefinitionText(format=self._create_text_format_config(response_format)) + args["text"] = PromptAgentDefinitionText(format=_create_text_format_config(response_format)) # Combine instructions from messages and options combined_instructions = [ @@ -477,8 +443,8 @@ def _prepare_mcp_tool(tool: HostedMCPTool) -> MCPTool: # type: ignore[override] async def get_agent( project_client: AIProjectClient | None = None, + name: str | None = None, agent_reference: AgentReference | None = None, - agent_name: str | None = None, agent_object: AgentObject | None = None, tools: ToolProtocol | Callable[..., Any] @@ -494,8 +460,8 @@ async def get_agent( Args: project_client: An existing AIProjectClient to use. If not provided, one will be created. + name: Optional name of the agent to retrieve (if reference is not provided). agent_reference: Optional reference containing the agent's name and specific version. - agent_name: Optional name of the agent to retrieve (if reference is not provided). agent_object: Optional pre-fetched agent object. tools: A collection of tools to be made available to the agent. project_endpoint: The Azure AI Project endpoint URL. @@ -508,60 +474,43 @@ async def get_agent( Returns: ChatAgent: An initialized `ChatAgent` configured with the retrieved agent's settings and tools. """ - existing_agent: AgentVersionObject - - try: - azure_ai_settings = AzureAISettings( + # If no project_client is provided, create one + if project_client is None: + project_client = _get_project_client( project_endpoint=project_endpoint, + credential=credential, env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) - except ValidationError as ex: - raise ServiceInitializationError("Failed to create Azure AI settings.", ex) from ex - # If no project_client is provided, create one - if project_client is None: - if not azure_ai_settings.project_endpoint: - raise ServiceInitializationError( - "Azure AI project endpoint is required. Set via 'project_endpoint' parameter " - "or 'AZURE_AI_PROJECT_ENDPOINT' environment variable." - ) - - # Use provided credential - if not credential: - raise ServiceInitializationError("Azure credential is required when project_client is not provided.") - project_client = AIProjectClient( - endpoint=azure_ai_settings.project_endpoint, - credential=credential, - user_agent=AGENT_FRAMEWORK_USER_AGENT, - ) + existing_agent: AgentVersionObject if agent_reference and agent_reference.version: existing_agent = await project_client.agents.get_version( agent_name=agent_reference.name, agent_version=agent_reference.version ) else: - if name := (agent_reference.name if agent_reference else agent_name): - agent_object = await project_client.agents.get(agent_name=name) + if agent_name := (agent_reference.name if agent_reference else name): + agent_object = await project_client.agents.get(agent_name=agent_name) if not agent_object: - raise ValueError("Either agent_reference or agent_name or agent_object must be provided to get an agent.") + raise ValueError("Either name or agent_reference or agent_object must be provided to get an agent.") existing_agent = agent_object.versions.latest if not isinstance(existing_agent.definition, PromptAgentDefinition): raise ValueError("Agent definition must be PromptAgentDefinition to get a ChatAgent.") - # Convert tools to unified format and validate function tools. - provided_tools = ChatOptions(tools=tools).tools or [] - provided_tool_names = {tool.name for tool in provided_tools if isinstance(tool, AIFunction)} + # Normalize and validate function tools. + normalized_tools = ChatOptions(tools=tools).tools or [] + tool_names = {tool.name for tool in normalized_tools if isinstance(tool, AIFunction)} # If function tools exist in agent definition but were not provided to this method, # we need to raise an error, as it won't be possible to invoke the function. missing_tools = [ tool.name for tool in (existing_agent.definition.tools or []) - if isinstance(tool, FunctionTool) and tool.name not in provided_tool_names + if isinstance(tool, FunctionTool) and tool.name not in tool_names ] if missing_tools: @@ -569,27 +518,114 @@ async def get_agent( f"The following prompt agent definition required tools were not provided: {', '.join(missing_tools)}" ) + return _get_agent_from_version_object(project_client, existing_agent) + + +async def create_agent( + name: str, + model: str, + instructions: str | None = None, + description: str | None = None, + temperature: float | None = None, + top_p: float | None = None, + response_format: type[BaseModel] | None = None, + tools: ToolProtocol + | Callable[..., Any] + | MutableMapping[str, Any] + | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | None = None, + project_client: AIProjectClient | None = None, + project_endpoint: str | None = None, + credential: AsyncTokenCredential | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, +) -> ChatAgent: + # If no project_client is provided, create one + if project_client is None: + project_client = _get_project_client( + project_endpoint=project_endpoint, + credential=credential, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + + args: dict[str, Any] = {"model": model} + + if instructions: + args["instructions"] = instructions + if temperature: + args["temperature"] = temperature + if top_p: + args["top_p"] = top_p + if response_format: + args["text"] = PromptAgentDefinitionText(format=_create_text_format_config(response_format)) + if tools: + normalized_tools = ChatOptions(tools=tools).tools or [] + args["tools"] = _to_azure_ai_tools(normalized_tools) + + created_agent = await project_client.agents.create_version( + agent_name=name, + definition=PromptAgentDefinition(**args), + description=description, + ) + + return _get_agent_from_version_object(project_client, created_agent) + + +def _get_agent_from_version_object(project_client: AIProjectClient, version_object: AgentVersionObject) -> ChatAgent: client = AzureAIClient( project_client=project_client, - agent_name=existing_agent.name, - agent_version=existing_agent.version, - agent_description=existing_agent.description, + agent_name=version_object.name, + agent_version=version_object.version, + agent_description=version_object.description, ) return ChatAgent( chat_client=client, - id=existing_agent.id, - name=existing_agent.name, - description=existing_agent.description, - instructions=existing_agent.definition.instructions, - model_id=existing_agent.definition.model, - temperature=existing_agent.definition.temperature, - top_p=existing_agent.definition.top_p, - tools=_parse_tools(existing_agent.definition.tools), + id=version_object.id, + name=version_object.name, + description=version_object.description, + instructions=version_object.definition.instructions, + model_id=version_object.definition.model, + temperature=version_object.definition.temperature, + top_p=version_object.definition.top_p, + tools=_from_azure_ai_tools(version_object.definition.tools), + ) + + +def _get_project_client( + project_endpoint: str | None = None, + credential: AsyncTokenCredential | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, +) -> AIProjectClient: + try: + azure_ai_settings = AzureAISettings( + project_endpoint=project_endpoint, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create Azure AI settings.", ex) from ex + + if not azure_ai_settings.project_endpoint: + raise ServiceInitializationError( + "Azure AI project endpoint is required. Set via 'project_endpoint' parameter " + "or 'AZURE_AI_PROJECT_ENDPOINT' environment variable." + ) + + # Use provided credential + if not credential: + raise ServiceInitializationError("Azure credential is required when project_client is not provided.") + + return AIProjectClient( + endpoint=azure_ai_settings.project_endpoint, + credential=credential, + user_agent=AGENT_FRAMEWORK_USER_AGENT, ) -def _parse_tools(tools: Sequence[Tool | dict[str, Any]] | None) -> list[ToolProtocol | dict[str, Any]]: +def _from_azure_ai_tools(tools: Sequence[Tool | dict[str, Any]] | None) -> list[ToolProtocol | dict[str, Any]]: """Parses and converts a sequence of Azure AI tools into Agent Framework compatible tools. Args: @@ -655,7 +691,7 @@ def _parse_tools(tools: Sequence[Tool | dict[str, Any]] | None) -> list[ToolProt max_results=fs_tool.get("max_num_results"), ) ) - elif tool_type == "web_search" or tool_type == "web_search_preview": + elif tool_type == "web_search_preview": ws_tool = cast(WebSearchPreviewTool, tool_dict) additional_properties: dict[str, Any] = {} if user_location := ws_tool.get("user_location"): @@ -670,3 +706,198 @@ def _parse_tools(tools: Sequence[Tool | dict[str, Any]] | None) -> list[ToolProt else: agent_tools.append(tool_dict) return agent_tools + + +def _to_azure_ai_tools( + tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None, +) -> list[Tool | dict[str, Any]]: + """Converts Agent Framework tools into Azure AI compatible tools. + + Args: + tools: A sequence of Agent Framework tool objects or dictionaries + defining the tools to be converted. Can be None. + + Returns: + list[Tool | dict[str, Any]]: A list of converted tools compatible with Azure AI. + """ + azure_tools: list[Tool | dict[str, Any]] = [] + if not tools: + return azure_tools + + for tool in tools: + if isinstance(tool, ToolProtocol): + match tool: + case HostedMCPTool(): + azure_tools.append(_prepare_mcp_tool_for_azure_ai(tool)) + case HostedCodeInterpreterTool(): + ci_tool: CodeInterpreterTool = {"type": "code_interpreter"} + if tool.inputs: + file_ids: list[str] = [] + for tool_input in tool.inputs: + if isinstance(tool_input, HostedFileContent): + file_ids.append(tool_input.file_id) + if file_ids: + ci_tool["container"] = {"file_ids": file_ids} + azure_tools.append(ci_tool) + case AIFunction(): + params = tool.parameters() + params["additionalProperties"] = False + azure_tools.append( + FunctionTool( + type="function", + strict=False, + name=tool.name, + parameters=params, + description=tool.description, + ) + ) + case HostedFileSearchTool(): + if not tool.inputs: + raise ValueError("HostedFileSearchTool requires inputs to be specified.") + vector_store_ids: list[str] = [ + inp.vector_store_id for inp in tool.inputs if isinstance(inp, HostedVectorStoreContent) + ] + if not vector_store_ids: + raise ValueError( + "HostedFileSearchTool requires inputs to be of type `HostedVectorStoreContent`." + ) + fs_tool: FileSearchTool = { + "type": "file_search", + "vector_store_ids": vector_store_ids, + } + if tool.max_results: + fs_tool["max_num_results"] = tool.max_results + azure_tools.append(fs_tool) + case HostedWebSearchTool(): + ws_tool: WebSearchPreviewTool = {"type": "web_search_preview"} + if tool.additional_properties: + location: dict[str, str] | None = ( + tool.additional_properties.get("user_location", None) + if tool.additional_properties + else None + ) + if location: + ws_tool["user_location"] = { + "city": location.get("city"), + "country": location.get("country"), + "region": location.get("region"), + "timezone": location.get("timezone"), + } + azure_tools.append(ws_tool) + case _: + logger.debug("Unsupported tool passed (type: %s)", type(tool)) + else: + # Handle raw dictionary tools + tool_dict = tool if isinstance(tool, dict) else dict(tool) + azure_tools.append(tool_dict) + + return azure_tools + + +def _prepare_mcp_tool_for_azure_ai(tool: HostedMCPTool) -> MCPTool: + """Convert HostedMCPTool to Azure AI MCPTool format. + + Args: + tool: The HostedMCPTool to convert. + + Returns: + MCPTool: The converted Azure AI MCPTool. + """ + mcp: MCPTool = { + "type": "mcp", + "server_label": tool.name.replace(" ", "_"), + "server_url": str(tool.url), + } + + if tool.description: + mcp["server_description"] = tool.description + + if tool.headers: + mcp["headers"] = tool.headers + + if tool.allowed_tools: + mcp["allowed_tools"] = list(tool.allowed_tools) + + if tool.approval_mode: + match tool.approval_mode: + case str(): + mcp["require_approval"] = "always" if tool.approval_mode == "always_require" else "never" + case _: + if always_require_approvals := tool.approval_mode.get("always_require_approval"): + mcp["require_approval"] = {"always": {"tool_names": list(always_require_approvals)}} + if never_require_approvals := tool.approval_mode.get("never_require_approval"): + mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}} + + return mcp + + +def _create_text_format_config( + response_format: Any, +) -> ( + ResponseTextFormatConfigurationJsonSchema + | ResponseTextFormatConfigurationJsonObject + | ResponseTextFormatConfigurationText +): + """Convert response_format into Azure text format configuration.""" + if isinstance(response_format, type) and issubclass(response_format, BaseModel): + return ResponseTextFormatConfigurationJsonSchema( + name=response_format.__name__, + schema=response_format.model_json_schema(), + ) + + if isinstance(response_format, Mapping): + format_config = _convert_response_format(response_format) + format_type = format_config.get("type") + if format_type == "json_schema": + config_kwargs: dict[str, Any] = { + "name": format_config.get("name") or "response", + "schema": format_config["schema"], + } + if "strict" in format_config: + config_kwargs["strict"] = format_config["strict"] + if "description" in format_config: + config_kwargs["description"] = format_config["description"] + return ResponseTextFormatConfigurationJsonSchema(**config_kwargs) + if format_type == "json_object": + return ResponseTextFormatConfigurationJsonObject() + if format_type == "text": + return ResponseTextFormatConfigurationText() + + raise ServiceInvalidRequestError("response_format must be a Pydantic model or mapping.") + + +def _convert_response_format(response_format: Mapping[str, Any]) -> dict[str, Any]: + """Convert Chat style response_format into Responses text format config.""" + if "format" in response_format and isinstance(response_format["format"], Mapping): + return dict(cast("Mapping[str, Any]", response_format["format"])) + + format_type = response_format.get("type") + if format_type == "json_schema": + schema_section = response_format.get("json_schema", response_format) + if not isinstance(schema_section, Mapping): + raise ServiceInvalidRequestError("json_schema response_format must be a mapping.") + schema_section_typed = cast("Mapping[str, Any]", schema_section) + schema: Any = schema_section_typed.get("schema") + if schema is None: + raise ServiceInvalidRequestError("json_schema response_format requires a schema.") + name: str = str( + schema_section_typed.get("name") + or schema_section_typed.get("title") + or (cast("Mapping[str, Any]", schema).get("title") if isinstance(schema, Mapping) else None) + or "response" + ) + format_config: dict[str, Any] = { + "type": "json_schema", + "name": name, + "schema": schema, + } + if "strict" in schema_section: + format_config["strict"] = schema_section["strict"] + if "description" in schema_section and schema_section["description"] is not None: + format_config["description"] = schema_section["description"] + return format_config + + if format_type in {"json_object", "text"}: + return {"type": format_type} + + raise ServiceInvalidRequestError("Unsupported response_format provided for Azure AI client.") diff --git a/python/packages/azure-ai/tests/test_azure_ai_client.py b/python/packages/azure-ai/tests/test_azure_ai_client.py index b488c60ade..1e529609d6 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_client.py @@ -44,7 +44,7 @@ from pydantic import BaseModel, ConfigDict, Field, ValidationError from agent_framework_azure_ai import AzureAIClient, AzureAISettings -from agent_framework_azure_ai._client import _parse_tools, get_agent # type: ignore +from agent_framework_azure_ai._client import _from_azure_ai_tools, get_agent # type: ignore skip_if_azure_ai_integration_tests_disabled = pytest.mark.skipif( os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true" @@ -969,7 +969,7 @@ async def test_get_agent_parameter_handling(mock_project_client: MagicMock) -> N mock_agent_object.versions.latest = mock_agent_version mock_project_client.agents.get.return_value = mock_agent_object - agent = await get_agent(project_client=mock_project_client, agent_name="test-agent") + agent = await get_agent(project_client=mock_project_client, name="test-agent") assert agent.name == "test-agent" mock_project_client.agents.get.assert_called_with(agent_name="test-agent") @@ -983,7 +983,7 @@ async def test_get_agent_parameter_handling(mock_project_client: MagicMock) -> N async def test_get_agent_missing_parameters(mock_project_client: MagicMock) -> None: """Test get_agent missing parameters.""" - with pytest.raises(ValueError, match="Either agent_reference or agent_name or agent_object must be provided"): + with pytest.raises(ValueError, match="Either name or agent_reference or agent_object must be provided"): await get_agent(project_client=mock_project_client) @@ -1012,7 +1012,7 @@ def test_parse_tools() -> None: """Test _parse_tools.""" # Test MCP tool mcp_tool = MCPTool(server_label="test_server", server_url="http://localhost:8080") - parsed_tools = _parse_tools([mcp_tool]) + parsed_tools = _from_azure_ai_tools([mcp_tool]) assert len(parsed_tools) == 1 assert isinstance(parsed_tools[0], HostedMCPTool) assert parsed_tools[0].name == "test server" @@ -1020,7 +1020,7 @@ def test_parse_tools() -> None: # Test Code Interpreter tool ci_tool = CodeInterpreterTool(container=CodeInterpreterToolAuto(file_ids=["file-1"])) - parsed_tools = _parse_tools([ci_tool]) + parsed_tools = _from_azure_ai_tools([ci_tool]) assert len(parsed_tools) == 1 assert isinstance(parsed_tools[0], HostedCodeInterpreterTool) assert parsed_tools[0].inputs is not None @@ -1032,7 +1032,7 @@ def test_parse_tools() -> None: # Test File Search tool fs_tool = FileSearchTool(vector_store_ids=["vs-1"], max_num_results=5) - parsed_tools = _parse_tools([fs_tool]) + parsed_tools = _from_azure_ai_tools([fs_tool]) assert len(parsed_tools) == 1 assert isinstance(parsed_tools[0], HostedFileSearchTool) assert parsed_tools[0].inputs is not None @@ -1047,7 +1047,7 @@ def test_parse_tools() -> None: ws_tool = WebSearchPreviewTool( user_location=ApproximateLocation(city="Seattle", country="US", region="WA", timezone="PST") ) - parsed_tools = _parse_tools([ws_tool]) + parsed_tools = _from_azure_ai_tools([ws_tool]) assert len(parsed_tools) == 1 assert isinstance(parsed_tools[0], HostedWebSearchTool) assert parsed_tools[0].additional_properties From 48d3bbfb3c7c5696de49cb714ae10acc97647cb6 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 5 Jan 2026 20:54:37 -0800 Subject: [PATCH 06/16] Small fixes --- .../agent_framework_azure_ai/_client.py | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) 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 de1fe7bba6..08912568f4 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -31,7 +31,9 @@ AgentObject, AgentReference, AgentVersionObject, + ApproximateLocation, CodeInterpreterTool, + CodeInterpreterToolAuto, FileSearchTool, FunctionTool, MCPTool, @@ -620,6 +622,9 @@ async def create_agent( def _get_agent_from_version_object(project_client: AIProjectClient, version_object: AgentVersionObject) -> ChatAgent: + if not isinstance(version_object.definition, PromptAgentDefinition): + raise ValueError("Agent definition must be PromptAgentDefinition to get a ChatAgent.") + client = AzureAIClient( project_client=project_client, agent_name=version_object.name, @@ -777,24 +782,23 @@ def _to_azure_ai_tools( case HostedMCPTool(): azure_tools.append(_prepare_mcp_tool_for_azure_ai(tool)) case HostedCodeInterpreterTool(): - ci_tool: CodeInterpreterTool = {"type": "code_interpreter"} + ci_tool: CodeInterpreterTool = CodeInterpreterTool() if tool.inputs: file_ids: list[str] = [] for tool_input in tool.inputs: if isinstance(tool_input, HostedFileContent): file_ids.append(tool_input.file_id) if file_ids: - ci_tool["container"] = {"file_ids": file_ids} + ci_tool.container = CodeInterpreterToolAuto(file_ids=file_ids) azure_tools.append(ci_tool) case AIFunction(): params = tool.parameters() params["additionalProperties"] = False azure_tools.append( FunctionTool( - type="function", - strict=False, name=tool.name, parameters=params, + strict=False, description=tool.description, ) ) @@ -808,15 +812,12 @@ def _to_azure_ai_tools( raise ValueError( "HostedFileSearchTool requires inputs to be of type `HostedVectorStoreContent`." ) - fs_tool: FileSearchTool = { - "type": "file_search", - "vector_store_ids": vector_store_ids, - } + fs_tool: FileSearchTool = FileSearchTool(vector_store_ids=vector_store_ids) if tool.max_results: fs_tool["max_num_results"] = tool.max_results azure_tools.append(fs_tool) case HostedWebSearchTool(): - ws_tool: WebSearchPreviewTool = {"type": "web_search_preview"} + ws_tool: WebSearchPreviewTool = WebSearchPreviewTool() if tool.additional_properties: location: dict[str, str] | None = ( tool.additional_properties.get("user_location", None) @@ -824,12 +825,12 @@ def _to_azure_ai_tools( else None ) if location: - ws_tool["user_location"] = { - "city": location.get("city"), - "country": location.get("country"), - "region": location.get("region"), - "timezone": location.get("timezone"), - } + ws_tool.user_location = ApproximateLocation( + city=location.get("city"), + country=location.get("country"), + region=location.get("region"), + timezone=location.get("timezone"), + ) azure_tools.append(ws_tool) case _: logger.debug("Unsupported tool passed (type: %s)", type(tool)) @@ -850,11 +851,7 @@ def _prepare_mcp_tool_for_azure_ai(tool: HostedMCPTool) -> MCPTool: Returns: MCPTool: The converted Azure AI MCPTool. """ - mcp: MCPTool = { - "type": "mcp", - "server_label": tool.name.replace(" ", "_"), - "server_url": str(tool.url), - } + mcp: MCPTool = MCPTool(server_label=tool.name.replace(" ", "_"), server_url=str(tool.url)) if tool.description: mcp["server_description"] = tool.description From 075753943c1cc588a0ba4ec5fde2ebb4d3a04ea3 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:01:37 -0800 Subject: [PATCH 07/16] Fixed code interpreter tool mapping --- .../packages/azure-ai/agent_framework_azure_ai/_client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 08912568f4..c598ca8553 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -782,14 +782,13 @@ def _to_azure_ai_tools( case HostedMCPTool(): azure_tools.append(_prepare_mcp_tool_for_azure_ai(tool)) case HostedCodeInterpreterTool(): - ci_tool: CodeInterpreterTool = CodeInterpreterTool() + file_ids: list[str] = [] if tool.inputs: - file_ids: list[str] = [] for tool_input in tool.inputs: if isinstance(tool_input, HostedFileContent): file_ids.append(tool_input.file_id) - if file_ids: - ci_tool.container = CodeInterpreterToolAuto(file_ids=file_ids) + container = CodeInterpreterToolAuto(file_ids=file_ids if file_ids else None) + ci_tool: CodeInterpreterTool = CodeInterpreterTool(container=container) azure_tools.append(ci_tool) case AIFunction(): params = tool.parameters() From 348a94d3db042dfee96f2025fa7b66409cfa8936 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:17:22 -0800 Subject: [PATCH 08/16] Added agent provider for V2 client --- .../agent_framework_azure_ai/__init__.py | 5 +- .../agent_framework_azure_ai/_client.py | 490 +----------------- .../agent_framework_azure_ai/_provider.py | 405 +++++++++++++++ .../agent_framework_azure_ai/_shared.py | 301 ++++++++++- .../azure-ai/tests/test_azure_ai_client.py | 110 +--- .../packages/azure-ai/tests/test_provider.py | 410 +++++++++++++++ .../core/agent_framework/azure/__init__.py | 2 +- .../core/agent_framework/azure/__init__.pyi | 4 +- .../agents/azure_ai/azure_ai_basic.py | 22 +- .../azure_ai/azure_ai_use_latest_version.py | 65 ++- .../azure_ai/azure_ai_with_agent_to_agent.py | 12 +- .../azure_ai/azure_ai_with_azure_ai_search.py | 12 +- .../azure_ai_with_bing_custom_search.py | 12 +- .../azure_ai/azure_ai_with_bing_grounding.py | 12 +- .../azure_ai_with_browser_automation.py | 12 +- .../azure_ai_with_code_interpreter.py | 14 +- ...i_with_code_interpreter_file_generation.py | 24 +- .../azure_ai/azure_ai_with_existing_agent.py | 17 +- .../azure_ai_with_existing_conversation.py | 47 +- .../azure_ai_with_explicit_settings.py | 23 +- .../azure_ai/azure_ai_with_file_search.py | 25 +- .../azure_ai/azure_ai_with_hosted_mcp.py | 20 +- .../azure_ai_with_image_generation.py | 13 +- .../azure_ai/azure_ai_with_local_mcp.py | 12 +- .../azure_ai/azure_ai_with_memory_search.py | 30 +- .../azure_ai_with_microsoft_fabric.py | 12 +- .../agents/azure_ai/azure_ai_with_openapi.py | 12 +- .../azure_ai/azure_ai_with_response_format.py | 19 +- .../azure_ai_with_runtime_json_schema.py | 17 +- .../azure_ai/azure_ai_with_sharepoint.py | 12 +- .../agents/azure_ai/azure_ai_with_thread.py | 56 +- .../azure_ai/azure_ai_with_web_search.py | 13 +- 32 files changed, 1406 insertions(+), 834 deletions(-) create mode 100644 python/packages/azure-ai/agent_framework_azure_ai/_provider.py create mode 100644 python/packages/azure-ai/tests/test_provider.py 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 530d7b16ca..50bc775705 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/__init__.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/__init__.py @@ -3,7 +3,8 @@ import importlib.metadata from ._chat_client import AzureAIAgentClient -from ._client import AzureAIClient, get_agent +from ._client import AzureAIClient +from ._provider import AzureAIProjectAgentProvider from ._shared import AzureAISettings try: @@ -14,7 +15,7 @@ __all__ = [ "AzureAIAgentClient", "AzureAIClient", + "AzureAIProjectAgentProvider", "AzureAISettings", "__version__", - "get_agent", ] 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 ad7ed76625..db9ec3950b 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -1,55 +1,33 @@ # Copyright (c) Microsoft. All rights reserved. import sys -from collections.abc import Callable, Mapping, MutableMapping, MutableSequence, Sequence -from typing import Any, ClassVar, Literal, TypeVar, cast +from collections.abc import MutableSequence +from typing import Any, ClassVar, TypeVar, cast from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, - AIFunction, - ChatAgent, ChatMessage, ChatOptions, - Contents, - HostedCodeInterpreterTool, - HostedFileContent, - HostedFileSearchTool, HostedMCPTool, - HostedVectorStoreContent, - HostedWebSearchTool, TextContent, - ToolProtocol, get_logger, use_chat_middleware, use_function_invocation, ) -from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidRequestError +from agent_framework.exceptions import ServiceInitializationError from agent_framework.observability import use_instrumentation from agent_framework.openai._responses_client import OpenAIBaseResponsesClient from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ( - AgentObject, - AgentReference, - AgentVersionObject, - ApproximateLocation, - CodeInterpreterTool, - CodeInterpreterToolAuto, - FileSearchTool, - FunctionTool, MCPTool, PromptAgentDefinition, PromptAgentDefinitionText, - ResponseTextFormatConfigurationJsonObject, - ResponseTextFormatConfigurationJsonSchema, - ResponseTextFormatConfigurationText, - Tool, - WebSearchPreviewTool, ) from azure.core.credentials_async import AsyncTokenCredential from azure.core.exceptions import ResourceNotFoundError -from pydantic import BaseModel, ValidationError +from pydantic import ValidationError -from ._shared import AzureAISettings +from ._shared import AzureAISettings, create_text_format_config if sys.version_info >= (3, 11): from typing import Self # pragma: no cover @@ -347,7 +325,7 @@ async def _get_agent_reference_or_create( else chat_options.additional_properties.get("response_format") ) if response_format: - args["text"] = PromptAgentDefinitionText(format=_create_text_format_config(response_format)) + args["text"] = PromptAgentDefinitionText(format=create_text_format_config(response_format)) # Combine instructions from messages and options combined_instructions = [ @@ -514,459 +492,3 @@ def _prepare_mcp_tool(tool: HostedMCPTool) -> MCPTool: # type: ignore[override] mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}} return mcp - - -async def get_agent( - project_client: AIProjectClient | None = None, - name: str | None = None, - agent_reference: AgentReference | None = None, - agent_object: AgentObject | None = None, - tools: ToolProtocol - | Callable[..., Any] - | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] - | None = None, - project_endpoint: str | None = None, - credential: AsyncTokenCredential | None = None, - env_file_path: str | None = None, - env_file_encoding: str | None = None, -) -> ChatAgent: - """Retrieves an existing Azure AI agent and converts it into a ChatAgent instance. - - Args: - project_client: An existing AIProjectClient to use. If not provided, one will be created. - name: Optional name of the agent to retrieve (if reference is not provided). - agent_reference: Optional reference containing the agent's name and specific version. - agent_object: Optional pre-fetched agent object. - tools: A collection of tools to be made available to the agent. - project_endpoint: The Azure AI Project endpoint URL. - Can also be set via environment variable AZURE_AI_PROJECT_ENDPOINT. - Ignored when a project_client is passed. - credential: Azure async credential to use for authentication. - env_file_path: Path to environment file for loading settings. - env_file_encoding: Encoding of the environment file. - - Returns: - ChatAgent: An initialized `ChatAgent` configured with the retrieved agent's settings and tools. - """ - # If no project_client is provided, create one - if project_client is None: - project_client = _get_project_client( - project_endpoint=project_endpoint, - credential=credential, - env_file_path=env_file_path, - env_file_encoding=env_file_encoding, - ) - - existing_agent: AgentVersionObject - - if agent_reference and agent_reference.version: - existing_agent = await project_client.agents.get_version( - agent_name=agent_reference.name, agent_version=agent_reference.version - ) - else: - if agent_name := (agent_reference.name if agent_reference else name): - agent_object = await project_client.agents.get(agent_name=agent_name) - - if not agent_object: - raise ValueError("Either name or agent_reference or agent_object must be provided to get an agent.") - - existing_agent = agent_object.versions.latest - - if not isinstance(existing_agent.definition, PromptAgentDefinition): - raise ValueError("Agent definition must be PromptAgentDefinition to get a ChatAgent.") - - # Normalize and validate function tools. - normalized_tools = ChatOptions(tools=tools).tools or [] - tool_names = {tool.name for tool in normalized_tools if isinstance(tool, AIFunction)} - - # If function tools exist in agent definition but were not provided to this method, - # we need to raise an error, as it won't be possible to invoke the function. - missing_tools = [ - tool.name - for tool in (existing_agent.definition.tools or []) - if isinstance(tool, FunctionTool) and tool.name not in tool_names - ] - - if missing_tools: - raise ValueError( - f"The following prompt agent definition required tools were not provided: {', '.join(missing_tools)}" - ) - - return _get_agent_from_version_object(project_client, existing_agent) - - -async def create_agent( - name: str, - model: str, - instructions: str | None = None, - description: str | None = None, - temperature: float | None = None, - top_p: float | None = None, - response_format: type[BaseModel] | None = None, - tools: ToolProtocol - | Callable[..., Any] - | MutableMapping[str, Any] - | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] - | None = None, - project_client: AIProjectClient | None = None, - project_endpoint: str | None = None, - credential: AsyncTokenCredential | None = None, - env_file_path: str | None = None, - env_file_encoding: str | None = None, -) -> ChatAgent: - # If no project_client is provided, create one - if project_client is None: - project_client = _get_project_client( - project_endpoint=project_endpoint, - credential=credential, - env_file_path=env_file_path, - env_file_encoding=env_file_encoding, - ) - - args: dict[str, Any] = {"model": model} - - if instructions: - args["instructions"] = instructions - if temperature: - args["temperature"] = temperature - if top_p: - args["top_p"] = top_p - if response_format: - args["text"] = PromptAgentDefinitionText(format=_create_text_format_config(response_format)) - if tools: - normalized_tools = ChatOptions(tools=tools).tools or [] - args["tools"] = _to_azure_ai_tools(normalized_tools) - - created_agent = await project_client.agents.create_version( - agent_name=name, - definition=PromptAgentDefinition(**args), - description=description, - ) - - return _get_agent_from_version_object(project_client, created_agent) - - -def _get_agent_from_version_object(project_client: AIProjectClient, version_object: AgentVersionObject) -> ChatAgent: - if not isinstance(version_object.definition, PromptAgentDefinition): - raise ValueError("Agent definition must be PromptAgentDefinition to get a ChatAgent.") - - client = AzureAIClient( - project_client=project_client, - agent_name=version_object.name, - agent_version=version_object.version, - agent_description=version_object.description, - ) - - return ChatAgent( - chat_client=client, - id=version_object.id, - name=version_object.name, - description=version_object.description, - instructions=version_object.definition.instructions, - model_id=version_object.definition.model, - temperature=version_object.definition.temperature, - top_p=version_object.definition.top_p, - tools=_from_azure_ai_tools(version_object.definition.tools), - ) - - -def _get_project_client( - project_endpoint: str | None = None, - credential: AsyncTokenCredential | None = None, - env_file_path: str | None = None, - env_file_encoding: str | None = None, -) -> AIProjectClient: - try: - azure_ai_settings = AzureAISettings( - project_endpoint=project_endpoint, - env_file_path=env_file_path, - env_file_encoding=env_file_encoding, - ) - except ValidationError as ex: - raise ServiceInitializationError("Failed to create Azure AI settings.", ex) from ex - - if not azure_ai_settings.project_endpoint: - raise ServiceInitializationError( - "Azure AI project endpoint is required. Set via 'project_endpoint' parameter " - "or 'AZURE_AI_PROJECT_ENDPOINT' environment variable." - ) - - # Use provided credential - if not credential: - raise ServiceInitializationError("Azure credential is required when project_client is not provided.") - - return AIProjectClient( - endpoint=azure_ai_settings.project_endpoint, - credential=credential, - user_agent=AGENT_FRAMEWORK_USER_AGENT, - ) - - -def _from_azure_ai_tools(tools: Sequence[Tool | dict[str, Any]] | None) -> list[ToolProtocol | dict[str, Any]]: - """Parses and converts a sequence of Azure AI tools into Agent Framework compatible tools. - - Args: - tools: A sequence of tool objects or dictionaries - defining the tools to be parsed. Can be None. - - Returns: - list[ToolProtocol | dict[str, Any]]: A list of converted tools compatible with the - Agent Framework. - """ - agent_tools: list[ToolProtocol | dict[str, Any]] = [] - if not tools: - return agent_tools - for tool in tools: - # Handle raw dictionary tools - tool_dict = tool if isinstance(tool, dict) else dict(tool) - tool_type = tool_dict.get("type") - - if tool_type == "mcp": - mcp_tool = cast(MCPTool, tool_dict) - approval_mode: Literal["always_require", "never_require"] | dict[str, set[str]] | None = None - if require_approval := mcp_tool.get("require_approval"): - if require_approval == "always": - approval_mode = "always_require" - elif require_approval == "never": - approval_mode = "never_require" - elif isinstance(require_approval, dict): - approval_mode = {} - if "always" in require_approval: - approval_mode["always_require_approval"] = set(require_approval["always"].get("tool_names", [])) # type: ignore - if "never" in require_approval: - approval_mode["never_require_approval"] = set(require_approval["never"].get("tool_names", [])) # type: ignore - - agent_tools.append( - HostedMCPTool( - name=mcp_tool.get("server_label", "").replace("_", " "), - url=mcp_tool.get("server_url", ""), - description=mcp_tool.get("server_description"), - headers=mcp_tool.get("headers"), - allowed_tools=mcp_tool.get("allowed_tools"), - approval_mode=approval_mode, # type: ignore - ) - ) - elif tool_type == "code_interpreter": - ci_tool = cast(CodeInterpreterTool, tool_dict) - container = ci_tool.get("container", {}) - ci_inputs: list[Contents] = [] - if "file_ids" in container: - for file_id in container["file_ids"]: - ci_inputs.append(HostedFileContent(file_id=file_id)) - - agent_tools.append(HostedCodeInterpreterTool(inputs=ci_inputs if ci_inputs else None)) # type: ignore - elif tool_type == "file_search": - fs_tool = cast(FileSearchTool, tool_dict) - fs_inputs: list[Contents] = [] - if "vector_store_ids" in fs_tool: - for vs_id in fs_tool["vector_store_ids"]: - fs_inputs.append(HostedVectorStoreContent(vector_store_id=vs_id)) - - agent_tools.append( - HostedFileSearchTool( - inputs=fs_inputs if fs_inputs else None, # type: ignore - max_results=fs_tool.get("max_num_results"), - ) - ) - elif tool_type == "web_search_preview": - ws_tool = cast(WebSearchPreviewTool, tool_dict) - additional_properties: dict[str, Any] = {} - if user_location := ws_tool.get("user_location"): - additional_properties["user_location"] = { - "city": user_location.get("city"), - "country": user_location.get("country"), - "region": user_location.get("region"), - "timezone": user_location.get("timezone"), - } - - agent_tools.append(HostedWebSearchTool(additional_properties=additional_properties)) - else: - agent_tools.append(tool_dict) - return agent_tools - - -def _to_azure_ai_tools( - tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None, -) -> list[Tool | dict[str, Any]]: - """Converts Agent Framework tools into Azure AI compatible tools. - - Args: - tools: A sequence of Agent Framework tool objects or dictionaries - defining the tools to be converted. Can be None. - - Returns: - list[Tool | dict[str, Any]]: A list of converted tools compatible with Azure AI. - """ - azure_tools: list[Tool | dict[str, Any]] = [] - if not tools: - return azure_tools - - for tool in tools: - if isinstance(tool, ToolProtocol): - match tool: - case HostedMCPTool(): - azure_tools.append(_prepare_mcp_tool_for_azure_ai(tool)) - case HostedCodeInterpreterTool(): - file_ids: list[str] = [] - if tool.inputs: - for tool_input in tool.inputs: - if isinstance(tool_input, HostedFileContent): - file_ids.append(tool_input.file_id) - container = CodeInterpreterToolAuto(file_ids=file_ids if file_ids else None) - ci_tool: CodeInterpreterTool = CodeInterpreterTool(container=container) - azure_tools.append(ci_tool) - case AIFunction(): - params = tool.parameters() - params["additionalProperties"] = False - azure_tools.append( - FunctionTool( - name=tool.name, - parameters=params, - strict=False, - description=tool.description, - ) - ) - case HostedFileSearchTool(): - if not tool.inputs: - raise ValueError("HostedFileSearchTool requires inputs to be specified.") - vector_store_ids: list[str] = [ - inp.vector_store_id for inp in tool.inputs if isinstance(inp, HostedVectorStoreContent) - ] - if not vector_store_ids: - raise ValueError( - "HostedFileSearchTool requires inputs to be of type `HostedVectorStoreContent`." - ) - fs_tool: FileSearchTool = FileSearchTool(vector_store_ids=vector_store_ids) - if tool.max_results: - fs_tool["max_num_results"] = tool.max_results - azure_tools.append(fs_tool) - case HostedWebSearchTool(): - ws_tool: WebSearchPreviewTool = WebSearchPreviewTool() - if tool.additional_properties: - location: dict[str, str] | None = ( - tool.additional_properties.get("user_location", None) - if tool.additional_properties - else None - ) - if location: - ws_tool.user_location = ApproximateLocation( - city=location.get("city"), - country=location.get("country"), - region=location.get("region"), - timezone=location.get("timezone"), - ) - azure_tools.append(ws_tool) - case _: - logger.debug("Unsupported tool passed (type: %s)", type(tool)) - else: - # Handle raw dictionary tools - tool_dict = tool if isinstance(tool, dict) else dict(tool) - azure_tools.append(tool_dict) - - return azure_tools - - -def _prepare_mcp_tool_for_azure_ai(tool: HostedMCPTool) -> MCPTool: - """Convert HostedMCPTool to Azure AI MCPTool format. - - Args: - tool: The HostedMCPTool to convert. - - Returns: - MCPTool: The converted Azure AI MCPTool. - """ - mcp: MCPTool = MCPTool(server_label=tool.name.replace(" ", "_"), server_url=str(tool.url)) - - if tool.description: - mcp["server_description"] = tool.description - - if tool.headers: - mcp["headers"] = tool.headers - - if tool.allowed_tools: - mcp["allowed_tools"] = list(tool.allowed_tools) - - if tool.approval_mode: - match tool.approval_mode: - case str(): - mcp["require_approval"] = "always" if tool.approval_mode == "always_require" else "never" - case _: - if always_require_approvals := tool.approval_mode.get("always_require_approval"): - mcp["require_approval"] = {"always": {"tool_names": list(always_require_approvals)}} - if never_require_approvals := tool.approval_mode.get("never_require_approval"): - mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}} - - return mcp - - -def _create_text_format_config( - response_format: Any, -) -> ( - ResponseTextFormatConfigurationJsonSchema - | ResponseTextFormatConfigurationJsonObject - | ResponseTextFormatConfigurationText -): - """Convert response_format into Azure text format configuration.""" - if isinstance(response_format, type) and issubclass(response_format, BaseModel): - return ResponseTextFormatConfigurationJsonSchema( - name=response_format.__name__, - schema=response_format.model_json_schema(), - ) - - if isinstance(response_format, Mapping): - format_config = _convert_response_format(response_format) - format_type = format_config.get("type") - if format_type == "json_schema": - config_kwargs: dict[str, Any] = { - "name": format_config.get("name") or "response", - "schema": format_config["schema"], - } - if "strict" in format_config: - config_kwargs["strict"] = format_config["strict"] - if "description" in format_config: - config_kwargs["description"] = format_config["description"] - return ResponseTextFormatConfigurationJsonSchema(**config_kwargs) - if format_type == "json_object": - return ResponseTextFormatConfigurationJsonObject() - if format_type == "text": - return ResponseTextFormatConfigurationText() - - raise ServiceInvalidRequestError("response_format must be a Pydantic model or mapping.") - - -def _convert_response_format(response_format: Mapping[str, Any]) -> dict[str, Any]: - """Convert Chat style response_format into Responses text format config.""" - if "format" in response_format and isinstance(response_format["format"], Mapping): - return dict(cast("Mapping[str, Any]", response_format["format"])) - - format_type = response_format.get("type") - if format_type == "json_schema": - schema_section = response_format.get("json_schema", response_format) - if not isinstance(schema_section, Mapping): - raise ServiceInvalidRequestError("json_schema response_format must be a mapping.") - schema_section_typed = cast("Mapping[str, Any]", schema_section) - schema: Any = schema_section_typed.get("schema") - if schema is None: - raise ServiceInvalidRequestError("json_schema response_format requires a schema.") - name: str = str( - schema_section_typed.get("name") - or schema_section_typed.get("title") - or (cast("Mapping[str, Any]", schema).get("title") if isinstance(schema, Mapping) else None) - or "response" - ) - format_config: dict[str, Any] = { - "type": "json_schema", - "name": name, - "schema": schema, - } - if "strict" in schema_section: - format_config["strict"] = schema_section["strict"] - if "description" in schema_section and schema_section["description"] is not None: - format_config["description"] = schema_section["description"] - return format_config - - if format_type in {"json_object", "text"}: - return {"type": format_type} - - raise ServiceInvalidRequestError("Unsupported response_format provided for Azure AI client.") diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_provider.py new file mode 100644 index 0000000000..bc3dd749ad --- /dev/null +++ b/python/packages/azure-ai/agent_framework_azure_ai/_provider.py @@ -0,0 +1,405 @@ +# Copyright (c) Microsoft. All rights reserved. + +import sys +from collections.abc import Callable, MutableMapping, Sequence +from typing import Any + +from agent_framework import ( + AGENT_FRAMEWORK_USER_AGENT, + AIFunction, + ChatAgent, + ChatOptions, + ToolProtocol, + get_logger, +) +from agent_framework.exceptions import ServiceInitializationError +from azure.ai.projects.aio import AIProjectClient +from azure.ai.projects.models import ( + AgentDetails, + AgentReference, + AgentVersionDetails, + FunctionTool, + PromptAgentDefinition, + PromptAgentDefinitionText, +) +from azure.core.credentials_async import AsyncTokenCredential +from pydantic import BaseModel, ValidationError + +from ._client import AzureAIClient +from ._shared import AzureAISettings, create_text_format_config, from_azure_ai_tools, to_azure_ai_tools + +if sys.version_info >= (3, 11): + from typing import Self # pragma: no cover +else: + from typing_extensions import Self # pragma: no cover + + +logger = get_logger("agent_framework.azure") + + +class AzureAIProjectAgentProvider: + """Provider for Azure AI Agent Service (Responses API). + + This provider allows you to create, retrieve, and manage Azure AI agents + using the AIProjectClient from the Azure AI Projects SDK. + + Examples: + Using with explicit AIProjectClient: + + .. code-block:: python + + from agent_framework.azure import AzureAIProjectAgentProvider + from azure.ai.projects.aio import AIProjectClient + from azure.identity.aio import DefaultAzureCredential + + async with AIProjectClient(endpoint, credential) as client: + provider = AzureAIProjectAgentProvider(client) + agent = await provider.create_agent( + name="MyAgent", + model="gpt-4", + instructions="You are a helpful assistant.", + ) + response = await agent.run("Hello!") + + Using with credential and endpoint (auto-creates client): + + .. code-block:: python + + from agent_framework.azure import AzureAIProjectAgentProvider + from azure.identity.aio import DefaultAzureCredential + + async with AzureAIProjectAgentProvider(credential=credential) as provider: + agent = await provider.create_agent( + name="MyAgent", + model="gpt-4", + instructions="You are a helpful assistant.", + ) + response = await agent.run("Hello!") + """ + + def __init__( + self, + project_client: AIProjectClient | None = None, + *, + project_endpoint: str | None = None, + model: str | None = None, + credential: AsyncTokenCredential | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ) -> None: + """Initialize an Azure AI Project Agent Provider. + + Args: + project_client: An existing AIProjectClient to use. If not provided, one will be created. + project_endpoint: The Azure AI Project endpoint URL. + Can also be set via environment variable AZURE_AI_PROJECT_ENDPOINT. + Ignored when a project_client is passed. + model: The default model deployment name to use for agent creation. + Can also be set via environment variable AZURE_AI_MODEL_DEPLOYMENT_NAME. + credential: Azure async credential to use for authentication. + Required when project_client is not provided. + env_file_path: Path to environment file for loading settings. + env_file_encoding: Encoding of the environment file. + + Raises: + ServiceInitializationError: If required parameters are missing or invalid. + """ + try: + self._settings = AzureAISettings( + project_endpoint=project_endpoint, + model_deployment_name=model, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create Azure AI settings.", ex) from ex + + # Track whether we should close client connection + self._should_close_client = False + + if project_client is None: + if not self._settings.project_endpoint: + raise ServiceInitializationError( + "Azure AI project endpoint is required. Set via 'project_endpoint' parameter " + "or 'AZURE_AI_PROJECT_ENDPOINT' environment variable." + ) + + if not credential: + raise ServiceInitializationError("Azure credential is required when project_client is not provided.") + + project_client = AIProjectClient( + endpoint=self._settings.project_endpoint, + credential=credential, + user_agent=AGENT_FRAMEWORK_USER_AGENT, + ) + self._should_close_client = True + + self._project_client = project_client + self._credential = credential + + async def create_agent( + self, + name: str, + model: str | None = None, + instructions: str | None = None, + description: str | None = None, + temperature: float | None = None, + top_p: float | None = None, + response_format: type[BaseModel] | None = None, + tools: ToolProtocol + | Callable[..., Any] + | MutableMapping[str, Any] + | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | None = None, + ) -> ChatAgent: + """Create a new agent on the Azure AI service and return a local ChatAgent wrapper. + + Args: + name: The name of the agent to create. + model: The model deployment name to use. Falls back to AZURE_AI_MODEL_DEPLOYMENT_NAME + environment variable if not provided. + instructions: Instructions for the agent. + description: A description of the agent. + temperature: The sampling temperature to use. + top_p: The nucleus sampling probability to use. + response_format: The format of the response (Pydantic model for structured output). + tools: Tools to make available to the agent. + + Returns: + ChatAgent: A ChatAgent instance configured with the created agent. + + Raises: + ServiceInitializationError: If required parameters are missing. + """ + # Resolve model from parameter or environment variable + resolved_model = model or self._settings.model_deployment_name + if not resolved_model: + raise ServiceInitializationError( + "Model deployment name is required. Provide 'model' parameter " + "or set 'AZURE_AI_MODEL_DEPLOYMENT_NAME' environment variable." + ) + + args: dict[str, Any] = {"model": resolved_model} + + if instructions: + args["instructions"] = instructions + if temperature is not None: + args["temperature"] = temperature + if top_p is not None: + args["top_p"] = top_p + if response_format: + args["text"] = PromptAgentDefinitionText(format=create_text_format_config(response_format)) + if tools: + normalized_tools = ChatOptions(tools=tools).tools or [] + args["tools"] = to_azure_ai_tools(normalized_tools) + + created_agent = await self._project_client.agents.create_version( + agent_name=name, + definition=PromptAgentDefinition(**args), + description=description, + ) + + # Pass the user-provided tools for function invocation + normalized_tools = ChatOptions(tools=tools).tools if tools else None + return self._create_chat_agent_from_details(created_agent, normalized_tools) + + async def get_agent( + self, + *, + name: str | None = None, + reference: AgentReference | None = None, + details: AgentDetails | None = None, + tools: ToolProtocol + | Callable[..., Any] + | MutableMapping[str, Any] + | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | None = None, + ) -> ChatAgent: + """Retrieve an existing agent from the Azure AI service and return a local ChatAgent wrapper. + + You must provide one of: name, reference, or details. + + Args: + name: The name of the agent to retrieve (fetches latest version). + reference: Reference containing the agent's name and optionally a specific version. + details: A pre-fetched AgentDetails object (uses latest version from it). + tools: Tools to make available to the agent. Required if the agent has function tools. + + Returns: + ChatAgent: A ChatAgent instance configured with the retrieved agent. + + Raises: + ValueError: If no identifier is provided or required tools are missing. + """ + existing_agent: AgentVersionDetails + + if reference and reference.version: + # Fetch specific version + existing_agent = await self._project_client.agents.get_version( + agent_name=reference.name, agent_version=reference.version + ) + else: + # Get agent details if not provided + if agent_name := (reference.name if reference else name): + details = await self._project_client.agents.get(agent_name=agent_name) + + if not details: + raise ValueError("Either name, reference, or details must be provided to get an agent.") + + existing_agent = details.versions.latest + + if not isinstance(existing_agent.definition, PromptAgentDefinition): + raise ValueError("Agent definition must be PromptAgentDefinition to get a ChatAgent.") + + # Validate that required function tools are provided + self._validate_function_tools(existing_agent.definition.tools, tools) + + # Pass user-provided tools for function invocation + normalized_tools = ChatOptions(tools=tools).tools if tools else None + return self._create_chat_agent_from_details(existing_agent, normalized_tools) + + def as_agent( + self, + details: AgentVersionDetails, + tools: ToolProtocol + | Callable[..., Any] + | MutableMapping[str, Any] + | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | None = None, + ) -> ChatAgent: + """Wrap an SDK agent version object into a ChatAgent without making HTTP calls. + + Use this when you already have an AgentVersionDetails from a previous API call. + + Args: + details: The AgentVersionDetails to wrap. + tools: Tools to make available to the agent. Required if the agent has function tools. + + Returns: + ChatAgent: A ChatAgent instance configured with the agent version. + + Raises: + ValueError: If the agent definition is not a PromptAgentDefinition or required tools are missing. + """ + if not isinstance(details.definition, PromptAgentDefinition): + raise ValueError("Agent definition must be PromptAgentDefinition to create a ChatAgent.") + + # Validate that required function tools are provided + self._validate_function_tools(details.definition.tools, tools) + + # Pass user-provided tools for function invocation + normalized_tools = ChatOptions(tools=tools).tools if tools else None + return self._create_chat_agent_from_details(details, normalized_tools) + + def _create_chat_agent_from_details( + self, + details: AgentVersionDetails, + provided_tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None = None, + ) -> ChatAgent: + """Create a ChatAgent from an AgentVersionDetails. + + Args: + details: The AgentVersionDetails containing the agent definition. + provided_tools: User-provided tools (including function implementations). + These are merged with hosted tools from the definition. + """ + if not isinstance(details.definition, PromptAgentDefinition): + raise ValueError("Agent definition must be PromptAgentDefinition to get a ChatAgent.") + + client = AzureAIClient( + project_client=self._project_client, + agent_name=details.name, + agent_version=details.version, + agent_description=details.description, + ) + + # Merge tools: hosted tools from definition + user-provided function tools + # from_azure_ai_tools converts hosted tools (MCP, code interpreter, file search, web search) + # but function tools need the actual implementations from provided_tools + merged_tools = self._merge_tools(details.definition.tools, provided_tools) + + return ChatAgent( + chat_client=client, + id=details.id, + name=details.name, + description=details.description, + instructions=details.definition.instructions, + model_id=details.definition.model, + temperature=details.definition.temperature, + top_p=details.definition.top_p, + tools=merged_tools, + ) + + def _merge_tools( + self, + definition_tools: Sequence[Any] | None, + provided_tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None, + ) -> list[ToolProtocol | dict[str, Any]]: + """Merge hosted tools from definition with user-provided function tools. + + Args: + definition_tools: Tools from the agent definition (Azure AI format). + provided_tools: User-provided tools (Agent Framework format), including function implementations. + + Returns: + Combined list of tools for the ChatAgent. + """ + merged: list[ToolProtocol | dict[str, Any]] = [] + + # Convert hosted tools from definition (MCP, code interpreter, file search, web search) + # Function tools from the definition are skipped - we use user-provided implementations instead + hosted_tools = from_azure_ai_tools(definition_tools) + for tool in hosted_tools: + # Skip function tool dicts - they don't have implementations + if isinstance(tool, dict) and tool.get("type") == "function": + continue + merged.append(tool) + + # Add user-provided function tools (these have the actual implementations) + if provided_tools: + for tool in provided_tools: + if isinstance(tool, AIFunction): + merged.append(tool) + + return merged + + def _validate_function_tools( + self, + agent_tools: Sequence[Any] | None, + provided_tools: ToolProtocol + | Callable[..., Any] + | MutableMapping[str, Any] + | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | None, + ) -> None: + """Validate that required function tools are provided.""" + # Normalize and validate function tools + normalized_tools = ChatOptions(tools=provided_tools).tools or [] + tool_names = {tool.name for tool in normalized_tools if isinstance(tool, AIFunction)} + + # If function tools exist in agent definition but were not provided, + # we need to raise an error, as it won't be possible to invoke the function. + missing_tools = [ + tool.name for tool in (agent_tools or []) if isinstance(tool, FunctionTool) and tool.name not in tool_names + ] + + if missing_tools: + raise ValueError( + f"The following prompt agent definition required tools were not provided: {', '.join(missing_tools)}" + ) + + async def __aenter__(self) -> Self: + """Async context manager entry.""" + return self + + async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> None: + """Async context manager exit.""" + await self.close() + + async def close(self) -> None: + """Close the provider and release resources. + + Only closes the underlying AIProjectClient if it was created by this provider. + """ + if self._should_close_client: + await self._project_client.close() 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 a120e9f92e..727482f287 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_shared.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_shared.py @@ -1,8 +1,38 @@ # Copyright (c) Microsoft. All rights reserved. -from typing import ClassVar +from collections.abc import Mapping, MutableMapping, Sequence +from typing import Any, ClassVar, Literal, cast +from agent_framework import ( + AIFunction, + Contents, + HostedCodeInterpreterTool, + HostedFileContent, + HostedFileSearchTool, + HostedMCPTool, + HostedVectorStoreContent, + HostedWebSearchTool, + ToolProtocol, + get_logger, +) from agent_framework._pydantic import AFBaseSettings +from agent_framework.exceptions import ServiceInvalidRequestError +from azure.ai.projects.models import ( + ApproximateLocation, + CodeInterpreterTool, + CodeInterpreterToolAuto, + FileSearchTool, + FunctionTool, + MCPTool, + ResponseTextFormatConfigurationJsonObject, + ResponseTextFormatConfigurationJsonSchema, + ResponseTextFormatConfigurationText, + Tool, + WebSearchPreviewTool, +) +from pydantic import BaseModel + +logger = get_logger("agent_framework.azure") class AzureAISettings(AFBaseSettings): @@ -44,3 +74,272 @@ class AzureAISettings(AFBaseSettings): project_endpoint: str | None = None model_deployment_name: str | None = None + + +def from_azure_ai_tools(tools: Sequence[Tool | dict[str, Any]] | None) -> list[ToolProtocol | dict[str, Any]]: + """Parses and converts a sequence of Azure AI tools into Agent Framework compatible tools. + + Args: + tools: A sequence of tool objects or dictionaries + defining the tools to be parsed. Can be None. + + Returns: + list[ToolProtocol | dict[str, Any]]: A list of converted tools compatible with the + Agent Framework. + """ + agent_tools: list[ToolProtocol | dict[str, Any]] = [] + if not tools: + return agent_tools + for tool in tools: + # Handle raw dictionary tools + tool_dict = tool if isinstance(tool, dict) else dict(tool) + tool_type = tool_dict.get("type") + + if tool_type == "mcp": + mcp_tool = cast(MCPTool, tool_dict) + approval_mode: Literal["always_require", "never_require"] | dict[str, set[str]] | None = None + if require_approval := mcp_tool.get("require_approval"): + if require_approval == "always": + approval_mode = "always_require" + elif require_approval == "never": + approval_mode = "never_require" + elif isinstance(require_approval, dict): + approval_mode = {} + if "always" in require_approval: + approval_mode["always_require_approval"] = set(require_approval["always"].get("tool_names", [])) # type: ignore + if "never" in require_approval: + approval_mode["never_require_approval"] = set(require_approval["never"].get("tool_names", [])) # type: ignore + + agent_tools.append( + HostedMCPTool( + name=mcp_tool.get("server_label", "").replace("_", " "), + url=mcp_tool.get("server_url", ""), + description=mcp_tool.get("server_description"), + headers=mcp_tool.get("headers"), + allowed_tools=mcp_tool.get("allowed_tools"), + approval_mode=approval_mode, # type: ignore + ) + ) + elif tool_type == "code_interpreter": + ci_tool = cast(CodeInterpreterTool, tool_dict) + container = ci_tool.get("container", {}) + ci_inputs: list[Contents] = [] + if "file_ids" in container: + for file_id in container["file_ids"]: + ci_inputs.append(HostedFileContent(file_id=file_id)) + + agent_tools.append(HostedCodeInterpreterTool(inputs=ci_inputs if ci_inputs else None)) # type: ignore + elif tool_type == "file_search": + fs_tool = cast(FileSearchTool, tool_dict) + fs_inputs: list[Contents] = [] + if "vector_store_ids" in fs_tool: + for vs_id in fs_tool["vector_store_ids"]: + fs_inputs.append(HostedVectorStoreContent(vector_store_id=vs_id)) + + agent_tools.append( + HostedFileSearchTool( + inputs=fs_inputs if fs_inputs else None, # type: ignore + max_results=fs_tool.get("max_num_results"), + ) + ) + elif tool_type == "web_search_preview": + ws_tool = cast(WebSearchPreviewTool, tool_dict) + additional_properties: dict[str, Any] = {} + if user_location := ws_tool.get("user_location"): + additional_properties["user_location"] = { + "city": user_location.get("city"), + "country": user_location.get("country"), + "region": user_location.get("region"), + "timezone": user_location.get("timezone"), + } + + agent_tools.append(HostedWebSearchTool(additional_properties=additional_properties)) + else: + agent_tools.append(tool_dict) + return agent_tools + + +def to_azure_ai_tools( + tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None, +) -> list[Tool | dict[str, Any]]: + """Converts Agent Framework tools into Azure AI compatible tools. + + Args: + tools: A sequence of Agent Framework tool objects or dictionaries + defining the tools to be converted. Can be None. + + Returns: + list[Tool | dict[str, Any]]: A list of converted tools compatible with Azure AI. + """ + azure_tools: list[Tool | dict[str, Any]] = [] + if not tools: + return azure_tools + + for tool in tools: + if isinstance(tool, ToolProtocol): + match tool: + case HostedMCPTool(): + azure_tools.append(_prepare_mcp_tool_for_azure_ai(tool)) + case HostedCodeInterpreterTool(): + file_ids: list[str] = [] + if tool.inputs: + for tool_input in tool.inputs: + if isinstance(tool_input, HostedFileContent): + file_ids.append(tool_input.file_id) + container = CodeInterpreterToolAuto(file_ids=file_ids if file_ids else None) + ci_tool: CodeInterpreterTool = CodeInterpreterTool(container=container) + azure_tools.append(ci_tool) + case AIFunction(): + params = tool.parameters() + params["additionalProperties"] = False + azure_tools.append( + FunctionTool( + name=tool.name, + parameters=params, + strict=False, + description=tool.description, + ) + ) + case HostedFileSearchTool(): + if not tool.inputs: + raise ValueError("HostedFileSearchTool requires inputs to be specified.") + vector_store_ids: list[str] = [ + inp.vector_store_id for inp in tool.inputs if isinstance(inp, HostedVectorStoreContent) + ] + if not vector_store_ids: + raise ValueError( + "HostedFileSearchTool requires inputs to be of type `HostedVectorStoreContent`." + ) + fs_tool: FileSearchTool = FileSearchTool(vector_store_ids=vector_store_ids) + if tool.max_results: + fs_tool["max_num_results"] = tool.max_results + azure_tools.append(fs_tool) + case HostedWebSearchTool(): + ws_tool: WebSearchPreviewTool = WebSearchPreviewTool() + if tool.additional_properties: + location: dict[str, str] | None = ( + tool.additional_properties.get("user_location", None) + if tool.additional_properties + else None + ) + if location: + ws_tool.user_location = ApproximateLocation( + city=location.get("city"), + country=location.get("country"), + region=location.get("region"), + timezone=location.get("timezone"), + ) + azure_tools.append(ws_tool) + case _: + logger.debug("Unsupported tool passed (type: %s)", type(tool)) + else: + # Handle raw dictionary tools + tool_dict = tool if isinstance(tool, dict) else dict(tool) + azure_tools.append(tool_dict) + + return azure_tools + + +def _prepare_mcp_tool_for_azure_ai(tool: HostedMCPTool) -> MCPTool: + """Convert HostedMCPTool to Azure AI MCPTool format. + + Args: + tool: The HostedMCPTool to convert. + + Returns: + MCPTool: The converted Azure AI MCPTool. + """ + mcp: MCPTool = MCPTool(server_label=tool.name.replace(" ", "_"), server_url=str(tool.url)) + + if tool.description: + mcp["server_description"] = tool.description + + if tool.headers: + mcp["headers"] = tool.headers + + if tool.allowed_tools: + mcp["allowed_tools"] = list(tool.allowed_tools) + + if tool.approval_mode: + match tool.approval_mode: + case str(): + mcp["require_approval"] = "always" if tool.approval_mode == "always_require" else "never" + case _: + if always_require_approvals := tool.approval_mode.get("always_require_approval"): + mcp["require_approval"] = {"always": {"tool_names": list(always_require_approvals)}} + if never_require_approvals := tool.approval_mode.get("never_require_approval"): + mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}} + + return mcp + + +def create_text_format_config( + response_format: Any, +) -> ( + ResponseTextFormatConfigurationJsonSchema + | ResponseTextFormatConfigurationJsonObject + | ResponseTextFormatConfigurationText +): + """Convert response_format into Azure text format configuration.""" + if isinstance(response_format, type) and issubclass(response_format, BaseModel): + return ResponseTextFormatConfigurationJsonSchema( + name=response_format.__name__, + schema=response_format.model_json_schema(), + ) + + if isinstance(response_format, Mapping): + format_config = _convert_response_format(response_format) + format_type = format_config.get("type") + if format_type == "json_schema": + config_kwargs: dict[str, Any] = { + "name": format_config.get("name") or "response", + "schema": format_config["schema"], + } + if "strict" in format_config: + config_kwargs["strict"] = format_config["strict"] + if "description" in format_config: + config_kwargs["description"] = format_config["description"] + return ResponseTextFormatConfigurationJsonSchema(**config_kwargs) + if format_type == "json_object": + return ResponseTextFormatConfigurationJsonObject() + if format_type == "text": + return ResponseTextFormatConfigurationText() + + raise ServiceInvalidRequestError("response_format must be a Pydantic model or mapping.") + + +def _convert_response_format(response_format: Mapping[str, Any]) -> dict[str, Any]: + """Convert Chat style response_format into Responses text format config.""" + if "format" in response_format and isinstance(response_format["format"], Mapping): + return dict(cast("Mapping[str, Any]", response_format["format"])) + + format_type = response_format.get("type") + if format_type == "json_schema": + schema_section = response_format.get("json_schema", response_format) + if not isinstance(schema_section, Mapping): + raise ServiceInvalidRequestError("json_schema response_format must be a mapping.") + schema_section_typed = cast("Mapping[str, Any]", schema_section) + schema: Any = schema_section_typed.get("schema") + if schema is None: + raise ServiceInvalidRequestError("json_schema response_format requires a schema.") + name: str = str( + schema_section_typed.get("name") + or schema_section_typed.get("title") + or (cast("Mapping[str, Any]", schema).get("title") if isinstance(schema, Mapping) else None) + or "response" + ) + format_config: dict[str, Any] = { + "type": "json_schema", + "name": name, + "schema": schema, + } + if "strict" in schema_section: + format_config["strict"] = schema_section["strict"] + if "description" in schema_section and schema_section["description"] is not None: + format_config["description"] = schema_section["description"] + return format_config + + if format_type in {"json_object", "text"}: + return {"type": format_type} + + raise ServiceInvalidRequestError("Unsupported response_format provided for Azure AI client.") diff --git a/python/packages/azure-ai/tests/test_azure_ai_client.py b/python/packages/azure-ai/tests/test_azure_ai_client.py index ee9803a62b..4e1ad0eb77 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_client.py @@ -26,15 +26,11 @@ from agent_framework.exceptions import ServiceInitializationError from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ( - AgentReference, - AgentVersionObject, ApproximateLocation, CodeInterpreterTool, CodeInterpreterToolAuto, FileSearchTool, - FunctionTool, MCPTool, - PromptAgentDefinition, ResponseTextFormatConfigurationJsonSchema, WebSearchPreviewTool, ) @@ -44,7 +40,7 @@ from pydantic import BaseModel, ConfigDict, Field, ValidationError from agent_framework_azure_ai import AzureAIClient, AzureAISettings -from agent_framework_azure_ai._client import _from_azure_ai_tools, get_agent # type: ignore +from agent_framework_azure_ai._shared import from_azure_ai_tools skip_if_azure_ai_integration_tests_disabled = pytest.mark.skipif( os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true" @@ -1036,78 +1032,11 @@ async def test_azure_ai_chat_client_agent_with_tools() -> None: assert any(word in response.text.lower() for word in ["sunny", "25"]) -async def test_get_agent_parameter_handling(mock_project_client: MagicMock) -> None: - """Test get_agent parameter handling.""" - mock_project_client.agents = AsyncMock() - - # Test with agent_reference - agent_reference = AgentReference(name="test-agent", version="1.0") - mock_agent_version = MagicMock(spec=AgentVersionObject) - mock_agent_version.name = "test-agent" - mock_agent_version.version = "1.0" - mock_agent_version.description = "Test Agent" - mock_agent_version.definition = PromptAgentDefinition(model="test-model") - mock_agent_version.definition.model = "gpt-4" - mock_agent_version.definition.instructions = "Test instructions" - mock_agent_version.definition.tools = [] - - mock_project_client.agents.get_version.return_value = mock_agent_version - - agent = await get_agent(project_client=mock_project_client, agent_reference=agent_reference) - - assert agent.name == "test-agent" - mock_project_client.agents.get_version.assert_called_with(agent_name="test-agent", agent_version="1.0") - - # Test with agent_name - mock_agent_object = MagicMock() - mock_agent_object.versions = MagicMock() - mock_agent_object.versions.latest = mock_agent_version - mock_project_client.agents.get.return_value = mock_agent_object - - agent = await get_agent(project_client=mock_project_client, name="test-agent") - - assert agent.name == "test-agent" - mock_project_client.agents.get.assert_called_with(agent_name="test-agent") - - # Test with agent_object - agent = await get_agent(project_client=mock_project_client, agent_object=mock_agent_object) - - assert agent.name == "test-agent" - - -async def test_get_agent_missing_parameters(mock_project_client: MagicMock) -> None: - """Test get_agent missing parameters.""" - - with pytest.raises(ValueError, match="Either name or agent_reference or agent_object must be provided"): - await get_agent(project_client=mock_project_client) - - -async def test_get_agent_missing_tools(mock_project_client: MagicMock) -> None: - """Test get_agent missing tools.""" - mock_project_client.agents = AsyncMock() - - mock_agent_version = MagicMock(spec=AgentVersionObject) - mock_agent_version.name = "test-agent" - mock_agent_version.definition = MagicMock(spec=PromptAgentDefinition) - mock_agent_version.definition.tools = [ - FunctionTool(name="test_tool", parameters=[], strict=True, description="Test tool") - ] - - mock_agent_object = MagicMock() - mock_agent_object.versions = MagicMock() - mock_agent_object.versions.latest = mock_agent_version - - with pytest.raises( - ValueError, match="The following prompt agent definition required tools were not provided: test_tool" - ): - await get_agent(project_client=mock_project_client, agent_object=mock_agent_object) - - def test_parse_tools() -> None: """Test _parse_tools.""" # Test MCP tool mcp_tool = MCPTool(server_label="test_server", server_url="http://localhost:8080") - parsed_tools = _from_azure_ai_tools([mcp_tool]) + parsed_tools = from_azure_ai_tools([mcp_tool]) assert len(parsed_tools) == 1 assert isinstance(parsed_tools[0], HostedMCPTool) assert parsed_tools[0].name == "test server" @@ -1115,7 +1044,7 @@ def test_parse_tools() -> None: # Test Code Interpreter tool ci_tool = CodeInterpreterTool(container=CodeInterpreterToolAuto(file_ids=["file-1"])) - parsed_tools = _from_azure_ai_tools([ci_tool]) + parsed_tools = from_azure_ai_tools([ci_tool]) assert len(parsed_tools) == 1 assert isinstance(parsed_tools[0], HostedCodeInterpreterTool) assert parsed_tools[0].inputs is not None @@ -1127,7 +1056,7 @@ def test_parse_tools() -> None: # Test File Search tool fs_tool = FileSearchTool(vector_store_ids=["vs-1"], max_num_results=5) - parsed_tools = _from_azure_ai_tools([fs_tool]) + parsed_tools = from_azure_ai_tools([fs_tool]) assert len(parsed_tools) == 1 assert isinstance(parsed_tools[0], HostedFileSearchTool) assert parsed_tools[0].inputs is not None @@ -1142,7 +1071,7 @@ def test_parse_tools() -> None: ws_tool = WebSearchPreviewTool( user_location=ApproximateLocation(city="Seattle", country="US", region="WA", timezone="PST") ) - parsed_tools = _from_azure_ai_tools([ws_tool]) + parsed_tools = from_azure_ai_tools([ws_tool]) assert len(parsed_tools) == 1 assert isinstance(parsed_tools[0], HostedWebSearchTool) assert parsed_tools[0].additional_properties @@ -1155,35 +1084,6 @@ def test_parse_tools() -> None: assert user_location["timezone"] == "PST" -async def test_get_agent_success(mock_project_client: MagicMock) -> None: - """Test get_agent success path.""" - mock_project_client.agents = AsyncMock() - - mock_agent_version = MagicMock(spec=AgentVersionObject) - mock_agent_version.id = "agent-id" - mock_agent_version.name = "test-agent" - mock_agent_version.description = "Test Agent" - mock_agent_version.version = "1.0" - mock_agent_version.definition = MagicMock(spec=PromptAgentDefinition) - mock_agent_version.definition.model = "gpt-4" - mock_agent_version.definition.instructions = "Test instructions" - mock_agent_version.definition.temperature = 0.7 - mock_agent_version.definition.top_p = 0.9 - - mock_project_client.agents.get_version.return_value = mock_agent_version - - agent_reference = AgentReference(name="test-agent", version="1.0") - agent = await get_agent(project_client=mock_project_client, agent_reference=agent_reference) - - assert agent.id == "agent-id" - assert agent.name == "test-agent" - assert agent.description == "Test Agent" - assert agent.chat_options.instructions == "Test instructions" - assert agent.chat_options.model_id == "gpt-4" - assert agent.chat_options.temperature == 0.7 - assert agent.chat_options.top_p == 0.9 - - class ReleaseBrief(BaseModel): """Structured output model for release brief.""" diff --git a/python/packages/azure-ai/tests/test_provider.py b/python/packages/azure-ai/tests/test_provider.py new file mode 100644 index 0000000000..834221e353 --- /dev/null +++ b/python/packages/azure-ai/tests/test_provider.py @@ -0,0 +1,410 @@ +# Copyright (c) Microsoft. All rights reserved. + +import os +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from agent_framework import ChatAgent +from agent_framework.exceptions import ServiceInitializationError +from azure.ai.projects.aio import AIProjectClient +from azure.ai.projects.models import ( + AgentReference, + AgentVersionDetails, + FunctionTool, + PromptAgentDefinition, +) +from azure.identity.aio import AzureCliCredential + +from agent_framework_azure_ai import AzureAIProjectAgentProvider + +skip_if_azure_ai_integration_tests_disabled = pytest.mark.skipif( + os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true" + or os.getenv("AZURE_AI_PROJECT_ENDPOINT", "") in ("", "https://test-project.cognitiveservices.azure.com/") + or os.getenv("AZURE_AI_MODEL_DEPLOYMENT_NAME", "") == "", + reason=( + "No real AZURE_AI_PROJECT_ENDPOINT or AZURE_AI_MODEL_DEPLOYMENT_NAME provided; skipping integration tests." + if os.getenv("RUN_INTEGRATION_TESTS", "false").lower() == "true" + else "Integration tests are disabled." + ), +) + + +@pytest.fixture +def mock_project_client() -> MagicMock: + """Fixture that provides a mock AIProjectClient.""" + mock_client = MagicMock() + + # Mock agents property + mock_client.agents = MagicMock() + mock_client.agents.create_version = AsyncMock() + + # Mock conversations property + mock_client.conversations = MagicMock() + mock_client.conversations.create = AsyncMock() + + # Mock telemetry property + mock_client.telemetry = MagicMock() + mock_client.telemetry.get_application_insights_connection_string = AsyncMock() + + # Mock get_openai_client method + mock_client.get_openai_client = AsyncMock() + + # Mock close method + mock_client.close = AsyncMock() + + return mock_client + + +@pytest.fixture +def mock_azure_credential() -> MagicMock: + """Fixture that provides a mock Azure credential.""" + return MagicMock() + + +@pytest.fixture +def azure_ai_unit_test_env(monkeypatch: pytest.MonkeyPatch) -> dict[str, str]: + """Fixture that sets up Azure AI environment variables for unit testing.""" + env_vars = { + "AZURE_AI_PROJECT_ENDPOINT": "https://test-project.cognitiveservices.azure.com/", + "AZURE_AI_MODEL_DEPLOYMENT_NAME": "test-model-deployment", + } + for key, value in env_vars.items(): + monkeypatch.setenv(key, value) + return env_vars + + +def test_provider_init_with_project_client(mock_project_client: MagicMock) -> None: + """Test AzureAIProjectAgentProvider initialization with existing project_client.""" + provider = AzureAIProjectAgentProvider(project_client=mock_project_client) + + assert provider._project_client is mock_project_client # type: ignore + assert not provider._should_close_client # type: ignore + + +def test_provider_init_with_credential_and_endpoint( + azure_ai_unit_test_env: dict[str, str], + mock_azure_credential: MagicMock, +) -> None: + """Test AzureAIProjectAgentProvider initialization with credential and endpoint.""" + with patch("agent_framework_azure_ai._provider.AIProjectClient") as mock_ai_project_client: + mock_client = MagicMock() + mock_ai_project_client.return_value = mock_client + + provider = AzureAIProjectAgentProvider( + project_endpoint=azure_ai_unit_test_env["AZURE_AI_PROJECT_ENDPOINT"], + credential=mock_azure_credential, + ) + + assert provider._project_client is mock_client # type: ignore + assert provider._should_close_client # type: ignore + + # Verify AIProjectClient was called with correct parameters + mock_ai_project_client.assert_called_once() + + +def test_provider_init_missing_endpoint() -> None: + """Test AzureAIProjectAgentProvider initialization when endpoint is missing.""" + with patch("agent_framework_azure_ai._provider.AzureAISettings") as mock_settings: + mock_settings.return_value.project_endpoint = None + mock_settings.return_value.model_deployment_name = "test-model" + + with pytest.raises(ServiceInitializationError, match="Azure AI project endpoint is required"): + AzureAIProjectAgentProvider(credential=MagicMock()) + + +def test_provider_init_missing_credential(azure_ai_unit_test_env: dict[str, str]) -> None: + """Test AzureAIProjectAgentProvider initialization when credential is missing.""" + with pytest.raises( + ServiceInitializationError, match="Azure credential is required when project_client is not provided" + ): + AzureAIProjectAgentProvider( + project_endpoint=azure_ai_unit_test_env["AZURE_AI_PROJECT_ENDPOINT"], + ) + + +async def test_provider_create_agent( + mock_project_client: MagicMock, + azure_ai_unit_test_env: dict[str, str], +) -> None: + """Test AzureAIProjectAgentProvider.create_agent method.""" + with patch("agent_framework_azure_ai._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 = "Test Agent" + mock_agent_version.definition = MagicMock(spec=PromptAgentDefinition) + mock_agent_version.definition.model = "gpt-4" + mock_agent_version.definition.instructions = "Test instructions" + mock_agent_version.definition.temperature = 0.7 + mock_agent_version.definition.top_p = 0.9 + mock_agent_version.definition.tools = [] + + mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent_version) + + agent = await provider.create_agent( + name="test-agent", + model="gpt-4", + instructions="Test instructions", + description="Test Agent", + temperature=0.7, + top_p=0.9, + ) + + assert isinstance(agent, ChatAgent) + assert agent.name == "test-agent" + mock_project_client.agents.create_version.assert_called_once() + + +async def test_provider_create_agent_with_env_model( + mock_project_client: MagicMock, + azure_ai_unit_test_env: dict[str, str], +) -> None: + """Test AzureAIProjectAgentProvider.create_agent uses model from env var.""" + with patch("agent_framework_azure_ai._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 = azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"] + 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) + + # Call without model parameter - should use env var + agent = await provider.create_agent(name="test-agent") + + assert isinstance(agent, ChatAgent) + # Verify the model from env var was used + call_args = mock_project_client.agents.create_version.call_args + assert call_args[1]["definition"].model == azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"] + + +async def test_provider_create_agent_missing_model(mock_project_client: MagicMock) -> None: + """Test AzureAIProjectAgentProvider.create_agent raises when model is missing.""" + with patch("agent_framework_azure_ai._provider.AzureAISettings") as mock_settings: + mock_settings.return_value.project_endpoint = "https://test.com" + mock_settings.return_value.model_deployment_name = None + + provider = AzureAIProjectAgentProvider(project_client=mock_project_client) + + with pytest.raises(ServiceInitializationError, match="Model deployment name is required"): + await provider.create_agent(name="test-agent") + + +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) + + # Mock agent 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 = "Test Agent" + mock_agent_version.definition = MagicMock(spec=PromptAgentDefinition) + mock_agent_version.definition.model = "gpt-4" + mock_agent_version.definition.instructions = "Test instructions" + mock_agent_version.definition.temperature = None + mock_agent_version.definition.top_p = None + mock_agent_version.definition.tools = [] + + mock_agent_object = MagicMock() + mock_agent_object.versions.latest = mock_agent_version + + mock_project_client.agents = AsyncMock() + mock_project_client.agents.get.return_value = mock_agent_object + + agent = await provider.get_agent(name="test-agent") + + assert isinstance(agent, ChatAgent) + assert agent.name == "test-agent" + mock_project_client.agents.get.assert_called_with(agent_name="test-agent") + + +async def test_provider_get_agent_with_reference(mock_project_client: MagicMock) -> None: + """Test AzureAIProjectAgentProvider.get_agent with reference parameter.""" + provider = AzureAIProjectAgentProvider(project_client=mock_project_client) + + # Mock agent 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 = "Test Agent" + mock_agent_version.definition = MagicMock(spec=PromptAgentDefinition) + mock_agent_version.definition.model = "gpt-4" + mock_agent_version.definition.instructions = "Test instructions" + mock_agent_version.definition.temperature = None + mock_agent_version.definition.top_p = None + mock_agent_version.definition.tools = [] + + mock_project_client.agents = AsyncMock() + mock_project_client.agents.get_version.return_value = mock_agent_version + + agent_reference = AgentReference(name="test-agent", version="1.0") + agent = await provider.get_agent(reference=agent_reference) + + assert isinstance(agent, ChatAgent) + assert agent.name == "test-agent" + mock_project_client.agents.get_version.assert_called_with(agent_name="test-agent", agent_version="1.0") + + +async def test_provider_get_agent_missing_parameters(mock_project_client: MagicMock) -> None: + """Test AzureAIProjectAgentProvider.get_agent raises when no identifier provided.""" + provider = AzureAIProjectAgentProvider(project_client=mock_project_client) + + with pytest.raises(ValueError, match="Either name, reference, or details must be provided"): + await provider.get_agent() + + +async def test_provider_get_agent_missing_function_tools(mock_project_client: MagicMock) -> None: + """Test AzureAIProjectAgentProvider.get_agent raises when required tools are missing.""" + provider = AzureAIProjectAgentProvider(project_client=mock_project_client) + + # Mock agent with function tools + 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.tools = [ + FunctionTool(name="test_tool", parameters=[], strict=True, description="Test tool") + ] + + mock_agent_object = MagicMock() + mock_agent_object.versions.latest = mock_agent_version + + mock_project_client.agents = AsyncMock() + mock_project_client.agents.get.return_value = mock_agent_object + + with pytest.raises( + ValueError, match="The following prompt agent definition required tools were not provided: test_tool" + ): + await provider.get_agent(name="test-agent") + + +def test_provider_as_agent(mock_project_client: MagicMock) -> None: + """Test AzureAIProjectAgentProvider.as_agent method.""" + provider = AzureAIProjectAgentProvider(project_client=mock_project_client) + + # Create mock agent version + 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 = "Test Agent" + mock_agent_version.definition = MagicMock(spec=PromptAgentDefinition) + mock_agent_version.definition.model = "gpt-4" + mock_agent_version.definition.instructions = "Test instructions" + mock_agent_version.definition.temperature = 0.7 + mock_agent_version.definition.top_p = 0.9 + mock_agent_version.definition.tools = [] + + agent = provider.as_agent(mock_agent_version) + + assert isinstance(agent, ChatAgent) + assert agent.name == "test-agent" + assert agent.description == "Test Agent" + + +async def test_provider_context_manager(mock_project_client: MagicMock) -> None: + """Test AzureAIProjectAgentProvider async context manager.""" + with patch("agent_framework_azure_ai._provider.AIProjectClient") as mock_ai_project_client: + mock_client = MagicMock() + mock_client.close = AsyncMock() + mock_ai_project_client.return_value = mock_client + + with patch("agent_framework_azure_ai._provider.AzureAISettings") as mock_settings: + mock_settings.return_value.project_endpoint = "https://test.com" + mock_settings.return_value.model_deployment_name = "test-model" + + async with AzureAIProjectAgentProvider(credential=MagicMock()) as provider: + assert provider._project_client is mock_client # type: ignore + + # Should call close after exiting context + mock_client.close.assert_called_once() + + +async def test_provider_context_manager_with_provided_client(mock_project_client: MagicMock) -> None: + """Test AzureAIProjectAgentProvider context manager doesn't close provided client.""" + mock_project_client.close = AsyncMock() + + async with AzureAIProjectAgentProvider(project_client=mock_project_client) as provider: + assert provider._project_client is mock_project_client # type: ignore + + # Should NOT call close when client was provided + mock_project_client.close.assert_not_called() + + +async def test_provider_close_method(mock_project_client: MagicMock) -> None: + """Test AzureAIProjectAgentProvider.close method.""" + with patch("agent_framework_azure_ai._provider.AIProjectClient") as mock_ai_project_client: + mock_client = MagicMock() + mock_client.close = AsyncMock() + mock_ai_project_client.return_value = mock_client + + with patch("agent_framework_azure_ai._provider.AzureAISettings") as mock_settings: + mock_settings.return_value.project_endpoint = "https://test.com" + mock_settings.return_value.model_deployment_name = "test-model" + + provider = AzureAIProjectAgentProvider(credential=MagicMock()) + await provider.close() + + mock_client.close.assert_called_once() + + +@pytest.mark.flaky +@skip_if_azure_ai_integration_tests_disabled +async def test_provider_create_and_get_agent_integration() -> None: + """Integration test for provider create_agent and get_agent.""" + endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"] + model = os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"] + + async with ( + AzureCliCredential() as credential, + AIProjectClient(endpoint=endpoint, credential=credential) as project_client, + ): + provider = AzureAIProjectAgentProvider(project_client=project_client) + + try: + # Create agent + agent = await provider.create_agent( + name="ProviderTestAgent", + model=model, + instructions="You are a helpful assistant. Always respond with 'Hello from provider!'", + ) + + assert isinstance(agent, ChatAgent) + assert agent.name == "ProviderTestAgent" + + # Run the agent + response = await agent.run("Hi!") + assert response.text is not None + assert len(response.text) > 0 + + # Get the same agent + retrieved_agent = await provider.get_agent(name="ProviderTestAgent") + assert retrieved_agent.name == "ProviderTestAgent" + + finally: + # Cleanup + await project_client.agents.delete(agent_name="ProviderTestAgent") diff --git a/python/packages/core/agent_framework/azure/__init__.py b/python/packages/core/agent_framework/azure/__init__.py index 21d3d0f3d9..5c3cfbca4f 100644 --- a/python/packages/core/agent_framework/azure/__init__.py +++ b/python/packages/core/agent_framework/azure/__init__.py @@ -9,7 +9,7 @@ "AgentResponseCallbackProtocol": ("agent_framework_azurefunctions", "agent-framework-azurefunctions"), "AzureAIAgentClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), "AzureAIClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), - "get_agent": ("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"), "AzureAISearchSettings": ("agent_framework_azure_ai_search", "agent-framework-azure-ai-search"), "AzureAISettings": ("agent_framework_azure_ai", "agent-framework-azure-ai"), diff --git a/python/packages/core/agent_framework/azure/__init__.pyi b/python/packages/core/agent_framework/azure/__init__.pyi index df74e86b68..07f909cae3 100644 --- a/python/packages/core/agent_framework/azure/__init__.pyi +++ b/python/packages/core/agent_framework/azure/__init__.pyi @@ -1,6 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. -from agent_framework_azure_ai import AzureAIAgentClient, AzureAIClient, AzureAISettings, get_agent +from agent_framework_azure_ai import AzureAIAgentClient, AzureAIClient, AzureAIProjectAgentProvider, AzureAISettings from agent_framework_azure_ai_search import AzureAISearchContextProvider, AzureAISearchSettings from agent_framework_azurefunctions import ( AgentCallbackContext, @@ -21,6 +21,7 @@ __all__ = [ "AgentResponseCallbackProtocol", "AzureAIAgentClient", "AzureAIClient", + "AzureAIProjectAgentProvider", "AzureAISearchContextProvider", "AzureAISearchSettings", "AzureAISettings", @@ -29,6 +30,5 @@ __all__ = [ "AzureOpenAIResponsesClient", "AzureOpenAISettings", "DurableAIAgent", - "get_agent", "get_entra_auth_token", ] diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_basic.py b/python/samples/getting_started/agents/azure_ai/azure_ai_basic.py index 86aa603892..6cf5144bb7 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_basic.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_basic.py @@ -4,14 +4,14 @@ from random import randint from typing import Annotated -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential from pydantic import Field """ Azure AI Agent Basic Example -This sample demonstrates basic usage of AzureAIClient. +This sample demonstrates basic usage of AzureAIProjectAgentProvider. Shows both streaming and non-streaming responses with function tools. """ @@ -28,17 +28,18 @@ async def non_streaming_example() -> None: """Example of non-streaming response (get the complete result at once).""" print("=== Non-streaming Response Example ===") - # Since no Agent ID is provided, the agent will be automatically created. # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="BasicWeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather, - ) as agent, - ): + ) + query = "What's the weather like in Seattle?" print(f"User: {query}") result = await agent.run(query) @@ -49,17 +50,18 @@ async def streaming_example() -> None: """Example of streaming response (get results as they are generated).""" print("=== Streaming Response Example ===") - # Since no Agent ID is provided, the agent will be automatically created. # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="BasicWeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather, - ) as agent, - ): + ) + query = "What's the weather like in Tokyo?" print(f"User: {query}") print("Agent: ", end="", flush=True) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_use_latest_version.py b/python/samples/getting_started/agents/azure_ai/azure_ai_use_latest_version.py index 1a2a152821..025e78813e 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_use_latest_version.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_use_latest_version.py @@ -4,7 +4,7 @@ from random import randint from typing import Annotated -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential from pydantic import Field @@ -13,7 +13,7 @@ This sample demonstrates how to reuse the latest version of an existing agent instead of creating a new agent version on each instantiation. The first call creates a new agent, -while subsequent calls with `use_latest_version=True` reuse the latest agent version. +while subsequent calls with `get_agent()` reuse the latest agent version. """ @@ -28,39 +28,36 @@ def get_weather( async def main() -> None: # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. - async with AzureCliCredential() as credential: - async with ( - AzureAIClient( - credential=credential, - ).create_agent( - name="MyWeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) as agent, - ): - # First query will create a new agent - query = "What's the weather like in Seattle?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") + async with ( + AzureCliCredential() as credential, + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + # First call creates a new agent + agent = await provider.create_agent( + name="MyWeatherAgent", + instructions="You are a helpful weather agent.", + tools=get_weather, + ) - # Create a new agent instance - async with ( - AzureAIClient( - credential=credential, - # This parameter will allow to re-use latest agent version - # instead of creating a new one - use_latest_version=True, - ).create_agent( - name="MyWeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) as agent, - ): - query = "What's the weather like in Tokyo?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result}\n") + query = "What's the weather like in Seattle?" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result}\n") + + # Second call retrieves the existing agent (latest version) instead of creating a new one + # This is useful when you want to reuse an agent that was created earlier + agent2 = await provider.get_agent( + name="MyWeatherAgent", + tools=get_weather, # Tools must be provided for function tools + ) + + query = "What's the weather like in Tokyo?" + print(f"User: {query}") + result = await agent2.run(query) + print(f"Agent: {result}\n") + + print(f"First agent ID with version: {agent.id}") + print(f"Second agent ID with version: {agent2.id}") if __name__ == "__main__": diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_agent_to_agent.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_agent_to_agent.py index 93bd445e28..f234535699 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_agent_to_agent.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_agent_to_agent.py @@ -2,13 +2,13 @@ import asyncio import os -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ Azure AI Agent with Agent-to-Agent (A2A) Example -This sample demonstrates usage of AzureAIClient with Agent-to-Agent (A2A) capabilities +This sample demonstrates usage of AzureAIProjectAgentProvider with Agent-to-Agent (A2A) capabilities to enable communication with other agents using the A2A protocol. Prerequisites: @@ -21,7 +21,9 @@ async def main() -> None: async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="MyA2AAgent", instructions="""You are a helpful assistant that can communicate with other agents. Use the A2A tool when you need to interact with other agents to complete tasks @@ -30,8 +32,8 @@ async def main() -> None: "type": "a2a_preview", "project_connection_id": os.environ["A2A_PROJECT_CONNECTION_ID"], }, - ) as agent, - ): + ) + query = "What can the secondary agent do?" print(f"User: {query}") result = await agent.run(query) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_azure_ai_search.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_azure_ai_search.py index 057c2b5ff7..c4ee686d87 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_azure_ai_search.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_azure_ai_search.py @@ -2,13 +2,13 @@ import asyncio import os -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ Azure AI Agent with Azure AI Search Example -This sample demonstrates usage of AzureAIClient with Azure AI Search +This sample demonstrates usage of AzureAIProjectAgentProvider with Azure AI Search to search through indexed data and answer user questions about it. Prerequisites: @@ -21,7 +21,9 @@ async def main() -> None: async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="MySearchAgent", instructions="""You are a helpful assistant. You must always provide citations for answers using the tool and render them as: `[message_idx:search_idx†source]`.""", @@ -38,8 +40,8 @@ async def main() -> None: ] }, }, - ) as agent, - ): + ) + query = "Tell me about insurance options" print(f"User: {query}") result = await agent.run(query) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_bing_custom_search.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_bing_custom_search.py index 682e2fc38e..2a2db762f4 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_bing_custom_search.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_bing_custom_search.py @@ -2,13 +2,13 @@ import asyncio import os -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ Azure AI Agent with Bing Custom Search Example -This sample demonstrates usage of AzureAIClient with Bing Custom Search +This sample demonstrates usage of AzureAIProjectAgentProvider with Bing Custom Search to search custom search instances and provide responses with relevant results. Prerequisites: @@ -21,7 +21,9 @@ async def main() -> None: async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="MyCustomSearchAgent", instructions="""You are a helpful agent that can use Bing Custom Search tools to assist users. Use the available Bing Custom Search tools to answer questions and perform tasks.""", @@ -36,8 +38,8 @@ async def main() -> None: ] }, }, - ) as agent, - ): + ) + query = "Tell me more about foundry agent service" print(f"User: {query}") result = await agent.run(query) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_bing_grounding.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_bing_grounding.py index 810962ab24..92c00dddc9 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_bing_grounding.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_bing_grounding.py @@ -2,13 +2,13 @@ import asyncio import os -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ Azure AI Agent with Bing Grounding Example -This sample demonstrates usage of AzureAIClient with Bing Grounding +This sample demonstrates usage of AzureAIProjectAgentProvider with Bing Grounding to search the web for current information and provide grounded responses. Prerequisites: @@ -27,7 +27,9 @@ async def main() -> None: async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="MyBingGroundingAgent", instructions="""You are a helpful assistant that can search the web for current information. Use the Bing search tool to find up-to-date information and provide accurate, well-sourced answers. @@ -42,8 +44,8 @@ async def main() -> None: ] }, }, - ) as agent, - ): + ) + query = "What is today's date and weather in Seattle?" print(f"User: {query}") result = await agent.run(query) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_browser_automation.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_browser_automation.py index 72ee2cd5b0..21a180530c 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_browser_automation.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_browser_automation.py @@ -2,13 +2,13 @@ import asyncio import os -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ Azure AI Agent with Browser Automation Example -This sample demonstrates usage of AzureAIClient with Browser Automation +This sample demonstrates usage of AzureAIProjectAgentProvider with Browser Automation to perform automated web browsing tasks and provide responses based on web interactions. Prerequisites: @@ -21,7 +21,9 @@ async def main() -> None: async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="MyBrowserAutomationAgent", instructions="""You are an Agent helping with browser automation tasks. You can answer questions, provide information, and assist with various tasks @@ -34,8 +36,8 @@ async def main() -> None: } }, }, - ) as agent, - ): + ) + query = """Your goal is to report the percent of Microsoft year-to-date stock price change. To do that, go to the website finance.yahoo.com. At the top of the page, you will find a search bar. diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter.py index 2622e273e8..ad43e21e9c 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter.py @@ -3,7 +3,7 @@ import asyncio from agent_framework import ChatResponse, HostedCodeInterpreterTool -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential from openai.types.responses.response import Response as OpenAIResponse from openai.types.responses.response_code_interpreter_tool_call import ResponseCodeInterpreterToolCall @@ -11,22 +11,24 @@ """ Azure AI Agent Code Interpreter Example -This sample demonstrates using HostedCodeInterpreterTool with AzureAIClient +This sample demonstrates using HostedCodeInterpreterTool with AzureAIProjectAgentProvider for Python code execution and mathematical problem solving. """ async def main() -> None: - """Example showing how to use the HostedCodeInterpreterTool with AzureAIClient.""" + """Example showing how to use the HostedCodeInterpreterTool with AzureAIProjectAgentProvider.""" async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="MyCodeInterpreterAgent", instructions="You are a helpful assistant that can write and execute Python code to solve problems.", tools=HostedCodeInterpreterTool(), - ) as agent, - ): + ) + query = "Use code to get the factorial of 100?" print(f"User: {query}") result = await agent.run(query) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_generation.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_generation.py index 76758d1b61..fee3b9df45 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_generation.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_code_interpreter_file_generation.py @@ -3,19 +3,19 @@ import asyncio from agent_framework import ( + AgentRunResponseUpdate, CitationAnnotation, HostedCodeInterpreterTool, HostedFileContent, TextContent, ) -from agent_framework._agents import AgentRunResponseUpdate -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ Azure AI V2 Code Interpreter File Generation Sample -This sample demonstrates how the V2 AzureAIClient handles file annotations +This sample demonstrates how the AzureAIProjectAgentProvider handles file annotations when code interpreter generates text files. It shows both non-streaming and streaming approaches to verify file ID extraction. """ @@ -32,12 +32,14 @@ async def test_non_streaming() -> None: async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="V2CodeInterpreterFileAgent", instructions="You are a helpful assistant that can write and execute Python code to create files.", tools=HostedCodeInterpreterTool(), - ) as agent, - ): + ) + print(f"User: {QUERY}\n") result = await agent.run(QUERY) @@ -66,12 +68,14 @@ async def test_streaming() -> None: async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="V2CodeInterpreterFileAgentStreaming", instructions="You are a helpful assistant that can write and execute Python code to create files.", tools=HostedCodeInterpreterTool(), - ) as agent, - ): + ) + print(f"User: {QUERY}\n") annotations_found: list[str] = [] text_chunks: list[str] = [] @@ -102,7 +106,7 @@ async def test_streaming() -> None: async def main() -> None: - print("AzureAIClient Code Interpreter File Generation Test\n") + print("AzureAIProjectAgentProvider Code Interpreter File Generation Test\n") await test_non_streaming() await test_streaming() diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py index f636d202f6..c403e9c822 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py @@ -4,7 +4,7 @@ import os from agent_framework import ChatAgent -from agent_framework.azure import get_agent +from agent_framework.azure import AzureAIProjectAgentProvider from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import PromptAgentDefinition from azure.identity.aio import AzureCliCredential @@ -12,20 +12,20 @@ """ Azure AI Agent with Existing Agent Example -This sample demonstrates working with pre-existing Azure AI Agents by using get_agent method, +This sample demonstrates working with pre-existing Azure AI Agents by using provider.get_agent() method, showing agent reuse patterns for production scenarios. """ -async def using_get_agent_method() -> None: - print("=== Get existing Azure AI agent with get_agent method ===") +async def using_provider_get_agent() -> None: + print("=== Get existing Azure AI agent with provider.get_agent() ===") # Create the client async with ( AzureCliCredential() as credential, AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, ): - # Create remote agent + # Create remote agent using SDK directly azure_ai_agent = await project_client.agents.create_version( agent_name="MyNewTestAgent", description="Agent for testing purposes.", @@ -37,8 +37,9 @@ async def using_get_agent_method() -> None: ) try: - # Get newly created agent as ChatAgent by using get_agent method - agent: ChatAgent = await get_agent(project_client=project_client, agent_name=azure_ai_agent.name) + # Get newly created agent as ChatAgent by using provider.get_agent() + provider = AzureAIProjectAgentProvider(project_client=project_client) + agent: ChatAgent = await provider.get_agent(name=azure_ai_agent.name) # Verify agent properties print(f"Agent ID: {agent.id}") @@ -60,7 +61,7 @@ async def using_get_agent_method() -> None: async def main() -> None: - await using_get_agent_method() + await using_provider_get_agent() if __name__ == "__main__": diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py index 43019d050c..099c5ad5aa 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py @@ -4,7 +4,7 @@ from random import randint from typing import Annotated -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.ai.projects.aio import AIProjectClient from azure.identity.aio import AzureCliCredential from pydantic import Field @@ -12,7 +12,7 @@ """ Azure AI Agent Existing Conversation Example -This sample demonstrates usage of AzureAIClient with existing conversation created on service side. +This sample demonstrates usage of AzureAIProjectAgentProvider with existing conversation created on service side. """ @@ -24,9 +24,9 @@ def get_weather( return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C." -async def example_with_client() -> None: - """Example shows how to specify existing conversation ID when initializing Azure AI Client.""" - print("=== Azure AI Agent With Existing Conversation and Client ===") +async def example_with_conversation_id() -> None: + """Example shows how to use existing conversation ID with the provider.""" + print("=== Azure AI Agent With Existing Conversation ===") async with ( AzureCliCredential() as credential, AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, @@ -37,24 +37,23 @@ async def example_with_client() -> None: conversation_id = conversation.id print(f"Conversation ID: {conversation_id}") - async with AzureAIClient( - project_client=project_client, - # Specify conversation ID on client level - conversation_id=conversation_id, - ).create_agent( + provider = AzureAIProjectAgentProvider(project_client=project_client) + agent = await provider.create_agent( name="BasicAgent", instructions="You are a helpful agent.", tools=get_weather, - ) as agent: - query = "What's the weather like in Seattle?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result.text}\n") + ) - query = "What was my last question?" - print(f"User: {query}") - result = await agent.run(query) - print(f"Agent: {result.text}\n") + # Pass conversation_id at run level + query = "What's the weather like in Seattle?" + print(f"User: {query}") + result = await agent.run(query, conversation_id=conversation_id) + print(f"Agent: {result.text}\n") + + query = "What was my last question?" + print(f"User: {query}") + result = await agent.run(query, conversation_id=conversation_id) + print(f"Agent: {result.text}\n") async def example_with_thread() -> None: @@ -63,12 +62,14 @@ async def example_with_thread() -> None: async with ( AzureCliCredential() as credential, AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, - AzureAIClient(project_client=project_client).create_agent( + ): + provider = AzureAIProjectAgentProvider(project_client=project_client) + agent = await provider.create_agent( name="BasicAgent", instructions="You are a helpful agent.", tools=get_weather, - ) as agent, - ): + ) + # Create a conversation using OpenAI client openai_client = project_client.get_openai_client() conversation = await openai_client.conversations.create() @@ -90,7 +91,7 @@ async def example_with_thread() -> None: async def main() -> None: - await example_with_client() + await example_with_conversation_id() await example_with_thread() diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py index d5860a64f2..a3e3e24fe1 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py @@ -5,8 +5,7 @@ from random import randint from typing import Annotated -from agent_framework import ChatAgent -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential from pydantic import Field @@ -27,22 +26,22 @@ def get_weather( async def main() -> None: - # Since no Agent ID is provided, the agent will be automatically created. # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. async with ( AzureCliCredential() as credential, - ChatAgent( - chat_client=AzureAIClient( - project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], - model_deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], - credential=credential, - agent_name="WeatherAgent", - ), + AzureAIProjectAgentProvider( + project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + credential=credential, + ) as provider, + ): + agent = await provider.create_agent( + name="WeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather, - ) as agent, - ): + ) + query = "What's the weather like in New York?" print(f"User: {query}") result = await agent.run(query) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_file_search.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_file_search.py index de8c3b22b1..9558546093 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_file_search.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_file_search.py @@ -4,8 +4,8 @@ import os from pathlib import Path -from agent_framework import ChatAgent, HostedFileSearchTool, HostedVectorStoreContent -from agent_framework.azure import AzureAIClient +from agent_framework import HostedFileSearchTool, HostedVectorStoreContent +from agent_framework.azure import AzureAIProjectAgentProvider from azure.ai.agents.aio import AgentsClient from azure.ai.agents.models import FileInfo, VectorStore from azure.identity.aio import AzureCliCredential @@ -32,7 +32,7 @@ async def main() -> None: async with ( AzureCliCredential() as credential, AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client, - AzureAIClient(credential=credential) as client, + AzureAIProjectAgentProvider(credential=credential) as provider, ): try: # 1. Upload file and create vector store @@ -48,22 +48,21 @@ async def main() -> None: # 2. Create file search tool with uploaded resources file_search_tool = HostedFileSearchTool(inputs=[HostedVectorStoreContent(vector_store_id=vector_store.id)]) - # 3. Create an agent with file search capabilities - # The tool_resources are automatically extracted from HostedFileSearchTool - async with ChatAgent( - chat_client=client, + # 3. Create an agent with file search capabilities using the provider + agent = await provider.create_agent( name="EmployeeSearchAgent", instructions=( "You are a helpful assistant that can search through uploaded employee files " "to answer questions about employees." ), tools=file_search_tool, - ) as agent: - # 4. Simulate conversation with the agent - for user_input in USER_INPUTS: - print(f"# User: '{user_input}'") - response = await agent.run(user_input) - print(f"# Agent: {response.text}") + ) + + # 4. Simulate conversation with the agent + for user_input in USER_INPUTS: + print(f"# User: '{user_input}'") + response = await agent.run(user_input) + print(f"# Agent: {response.text}") finally: # 5. Cleanup: Delete the vector store and file in case of earlier failure to prevent orphaned resources. if vector_store: diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py index dd72108c05..02809f3f4c 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py @@ -4,7 +4,7 @@ from typing import Any from agent_framework import AgentProtocol, AgentRunResponse, AgentThread, ChatMessage, HostedMCPTool -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ @@ -59,12 +59,13 @@ async def handle_approvals_with_thread(query: str, agent: "AgentProtocol", threa async def run_hosted_mcp_without_approval() -> None: """Example showing MCP Tools without approval.""" - # Since no Agent ID is provided, the agent will be automatically created. # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="MyLearnDocsAgent", instructions="You are a helpful assistant that can help with Microsoft documentation questions.", tools=HostedMCPTool( @@ -72,8 +73,8 @@ async def run_hosted_mcp_without_approval() -> None: url="https://learn.microsoft.com/api/mcp", approval_mode="never_require", ), - ) as agent, - ): + ) + query = "How to create an Azure storage account using az cli?" print(f"User: {query}") result = await handle_approvals_without_thread(query, agent) @@ -84,12 +85,13 @@ async def run_hosted_mcp_with_approval_and_thread() -> None: """Example showing MCP Tools with approvals using a thread.""" print("=== MCP with approvals and with thread ===") - # Since no Agent ID is provided, the agent will be automatically created. # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="MyApiSpecsAgent", instructions="You are a helpful agent that can use MCP tools to assist users.", tools=HostedMCPTool( @@ -97,8 +99,8 @@ async def run_hosted_mcp_with_approval_and_thread() -> None: url="https://gitmcp.io/Azure/azure-rest-api-specs", approval_mode="always_require", ), - ) as agent, - ): + ) + thread = agent.get_new_thread() query = "Please summarize the Azure REST API specifications Readme" print(f"User: {query}") diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_image_generation.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_image_generation.py index 8274c43ab0..8eb1a3a738 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_image_generation.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_image_generation.py @@ -4,13 +4,13 @@ import aiofiles from agent_framework import DataContent, HostedImageGenerationTool -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ Azure AI Agent with Image Generation Example -This sample demonstrates basic usage of AzureAIClient to create an agent +This sample demonstrates basic usage of AzureAIProjectAgentProvider to create an agent that can generate images based on user requirements. Pre-requisites: @@ -20,12 +20,13 @@ async def main() -> None: - # Since no Agent ID is provided, the agent will be automatically created. # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="ImageGenAgent", instructions="Generate images based on user requirements.", tools=[ @@ -37,8 +38,8 @@ async def main() -> None: } ) ], - ) as agent, - ): + ) + query = "Generate an image of Microsoft logo." print(f"User: {query}") result = await agent.run( diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_local_mcp.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_local_mcp.py index 5f97116707..91b6228b71 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_local_mcp.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_local_mcp.py @@ -3,7 +3,7 @@ import asyncio from agent_framework import MCPStreamableHTTPTool -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ @@ -19,20 +19,22 @@ async def main() -> None: - """Example showing use of Local MCP Tool with AzureAIClient.""" + """Example showing use of Local MCP Tool with AzureAIProjectAgentProvider.""" print("=== Azure AI Agent with Local MCP Tools Example ===\n") async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="DocsAgent", instructions="You are a helpful assistant that can help with Microsoft documentation questions.", tools=MCPStreamableHTTPTool( name="Microsoft Learn MCP", url="https://learn.microsoft.com/api/mcp", ), - ) as agent, - ): + ) + # First query first_query = "How to create an Azure storage account using az cli?" print(f"User: {first_query}") diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_memory_search.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_memory_search.py index 2996840489..72b9ea1a01 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_memory_search.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_memory_search.py @@ -3,7 +3,7 @@ import os import uuid -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import MemoryStoreDefaultDefinition, MemoryStoreDefaultOptions from azure.identity.aio import AzureCliCredential @@ -11,7 +11,7 @@ """ Azure AI Agent with Memory Search Example -This sample demonstrates usage of AzureAIClient with memory search capabilities +This sample demonstrates usage of AzureAIProjectAgentProvider with memory search capabilities to retrieve relevant past user messages and maintain conversation context across sessions. It shows explicit memory store creation using Azure AI Projects client and agent creation using the Agent Framework. @@ -46,18 +46,20 @@ async def main() -> None: ) print(f"Created memory store: {memory_store.name} ({memory_store.id}): {memory_store.description}") - # Then, create the agent using Agent Framework - async with AzureAIClient(credential=credential).create_agent( - name="MyMemoryAgent", - instructions="""You are a helpful assistant that remembers past conversations. - Use the memory search tool to recall relevant information from previous interactions.""", - tools={ - "type": "memory_search", - "memory_store_name": memory_store.name, - "scope": "user_123", - "update_delay": 1, # Wait 1 second before updating memories (use higher value in production) - }, - ) as agent: + # Then, create the agent using Agent Framework provider + async with AzureAIProjectAgentProvider(credential=credential) as provider: + agent = await provider.create_agent( + name="MyMemoryAgent", + instructions="""You are a helpful assistant that remembers past conversations. + Use the memory search tool to recall relevant information from previous interactions.""", + tools={ + "type": "memory_search", + "memory_store_name": memory_store.name, + "scope": "user_123", + "update_delay": 1, # Wait 1 second before updating memories (use higher value in production) + }, + ) + # First interaction - establish some preferences print("=== First conversation ===") query1 = "I prefer dark roast coffee" diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_microsoft_fabric.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_microsoft_fabric.py index e19837f99b..0f3b39d192 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_microsoft_fabric.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_microsoft_fabric.py @@ -2,13 +2,13 @@ import asyncio import os -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ Azure AI Agent with Microsoft Fabric Example -This sample demonstrates usage of AzureAIClient with Microsoft Fabric +This sample demonstrates usage of AzureAIProjectAgentProvider with Microsoft Fabric to query Fabric data sources and provide responses based on data analysis. Prerequisites: @@ -21,7 +21,9 @@ async def main() -> None: async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="MyFabricAgent", instructions="You are a helpful assistant.", tools={ @@ -34,8 +36,8 @@ async def main() -> None: ] }, }, - ) as agent, - ): + ) + query = "Tell me about sales records" print(f"User: {query}") result = await agent.run(query) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_openapi.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_openapi.py index 8824106656..17a6d78f91 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_openapi.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_openapi.py @@ -4,13 +4,13 @@ from pathlib import Path import aiofiles -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ Azure AI Agent with OpenAPI Tool Example -This sample demonstrates usage of AzureAIClient with OpenAPI tools +This sample demonstrates usage of AzureAIProjectAgentProvider with OpenAPI tools to call external APIs defined by OpenAPI specifications. Prerequisites: @@ -29,7 +29,9 @@ async def main() -> None: async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="MyOpenAPIAgent", instructions="""You are a helpful assistant that can use country APIs to provide information. Use the available OpenAPI tools to answer questions about countries, currencies, and demographics.""", @@ -42,8 +44,8 @@ async def main() -> None: "auth": {"type": "anonymous"}, }, }, - ) as agent, - ): + ) + query = "What is the name and population of the country that uses currency with abbreviation THB?" print(f"User: {query}") result = await agent.run(query) 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 dfb4ce6a21..7b2550f52c 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 @@ -2,14 +2,14 @@ import asyncio -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential from pydantic import BaseModel, ConfigDict """ Azure AI Agent Response Format Example -This sample demonstrates basic usage of AzureAIClient with response format, +This sample demonstrates basic usage of AzureAIProjectAgentProvider with response format, also known as structured outputs. """ @@ -24,24 +24,23 @@ class ReleaseBrief(BaseModel): async def main() -> None: """Example of using response_format property.""" - # Since no Agent ID is provided, the agent will be automatically created. # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="ProductMarketerAgent", instructions="Return launch briefs as structured JSON.", - ) as agent, - ): - query = "Draft a launch brief for the Contoso Note app." - print(f"User: {query}") - result = await agent.run( - query, # Specify type to use as response response_format=ReleaseBrief, ) + query = "Draft a launch brief for the Contoso Note app." + print(f"User: {query}") + result = await agent.run(query) + if isinstance(result.value, ReleaseBrief): release_brief = result.value print("Agent:") 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 17bf359afe..0dafaa06ea 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 @@ -2,13 +2,13 @@ import asyncio -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ Azure AI Agent Response Format Example with Runtime JSON Schema -This sample demonstrates basic usage of AzureAIClient with response format, +This sample demonstrates basic usage of AzureAIProjectAgentProvider with response format, also known as structured outputs. """ @@ -31,17 +31,18 @@ async def main() -> None: """Example of using response_format property.""" - # Since no Agent ID is provided, the agent will be automatically created. # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( - name="ProductMarketerAgent", - instructions="Return launch briefs as structured JSON.", - ) as agent, + AzureAIProjectAgentProvider(credential=credential) as provider, ): - query = "Draft a launch brief for the Contoso Note app." + agent = await provider.create_agent( + name="ProductMarketerAgent", + instructions="Return sample weather digest as structured JSON.", + ) + + query = "Draft a sample weather digest." print(f"User: {query}") result = await agent.run( query, diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_sharepoint.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_sharepoint.py index a58de50e84..cd7765741e 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_sharepoint.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_sharepoint.py @@ -2,13 +2,13 @@ import asyncio import os -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ Azure AI Agent with SharePoint Example -This sample demonstrates usage of AzureAIClient with SharePoint +This sample demonstrates usage of AzureAIProjectAgentProvider with SharePoint to search through SharePoint content and answer user questions about it. Prerequisites: @@ -21,7 +21,9 @@ async def main() -> None: async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="MySharePointAgent", instructions="""You are a helpful agent that can use SharePoint tools to assist users. Use the available SharePoint tools to answer questions and perform tasks.""", @@ -35,8 +37,8 @@ async def main() -> None: ] }, }, - ) as agent, - ): + ) + query = "What is Contoso whistleblower policy?" print(f"User: {query}") result = await agent.run(query) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_thread.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_thread.py index fe8c7f5370..f4e69e02ca 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_thread.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_thread.py @@ -4,7 +4,7 @@ from random import randint from typing import Annotated -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential from pydantic import Field @@ -30,12 +30,14 @@ async def example_with_automatic_thread_creation() -> None: async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="BasicWeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather, - ) as agent, - ): + ) + # First conversation - no thread provided, will be created automatically query1 = "What's the weather like in Seattle?" print(f"User: {query1}") @@ -59,12 +61,14 @@ async def example_with_thread_persistence_in_memory() -> None: async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="BasicWeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather, - ) as agent, - ): + ) + # Create a new thread that will be reused thread = agent.get_new_thread() @@ -100,12 +104,14 @@ async def example_with_existing_thread_id() -> None: async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="BasicWeatherAgent", instructions="You are a helpful weather agent.", tools=get_weather, - ) as agent, - ): + ) + # Start a conversation and get the thread ID thread = agent.get_new_thread() @@ -121,21 +127,21 @@ async def example_with_existing_thread_id() -> None: if existing_thread_id: print("\n--- Continuing with the same thread ID in a new agent instance ---") - async with ( - AzureAIClient(credential=credential).create_agent( - name="BasicWeatherAgent", - instructions="You are a helpful weather agent.", - tools=get_weather, - ) as agent, - ): - # Create a thread with the existing ID - thread = agent.get_new_thread(service_thread_id=existing_thread_id) - - query2 = "What was the last city I asked about?" - print(f"User: {query2}") - result2 = await agent.run(query2, thread=thread) - print(f"Agent: {result2.text}") - print("Note: The agent continues the conversation from the previous thread by using thread ID.\n") + # Create a new agent instance from the same provider + agent2 = await provider.create_agent( + name="BasicWeatherAgent", + instructions="You are a helpful weather agent.", + tools=get_weather, + ) + + # Create a thread with the existing ID + thread = agent2.get_new_thread(service_thread_id=existing_thread_id) + + query2 = "What was the last city I asked about?" + print(f"User: {query2}") + result2 = await agent2.run(query2, thread=thread) + print(f"Agent: {result2.text}") + print("Note: The agent continues the conversation from the previous thread by using thread ID.\n") async def main() -> None: diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_web_search.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_web_search.py index ef788e4f5e..9ecb416f8d 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_web_search.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_web_search.py @@ -3,13 +3,13 @@ import asyncio from agent_framework import HostedWebSearchTool -from agent_framework.azure import AzureAIClient +from agent_framework.azure import AzureAIProjectAgentProvider from azure.identity.aio import AzureCliCredential """ Azure AI Agent With Web Search -This sample demonstrates basic usage of AzureAIClient to create an agent +This sample demonstrates basic usage of AzureAIProjectAgentProvider to create an agent that can perform web searches using the HostedWebSearchTool. Pre-requisites: @@ -19,17 +19,18 @@ async def main() -> None: - # Since no Agent ID is provided, the agent will be automatically created. # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. async with ( AzureCliCredential() as credential, - AzureAIClient(credential=credential).create_agent( + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + agent = await provider.create_agent( name="WebsearchAgent", instructions="You are a helpful assistant that can search the web", tools=[HostedWebSearchTool()], - ) as agent, - ): + ) + query = "What's the weather today in Seattle?" print(f"User: {query}") result = await agent.run(query) From e55a10d4e55737f2d228563e35157696c5f4b66b Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:49:15 -0800 Subject: [PATCH 09/16] Updated response format handling --- .../agent_framework_azure_ai/_provider.py | 19 +++++++++++-- .../azure_ai_with_runtime_json_schema.py | 28 ++++++++----------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_provider.py index bc3dd749ad..f19289032e 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_provider.py @@ -145,7 +145,7 @@ async def create_agent( description: str | None = None, temperature: float | None = None, top_p: float | None = None, - response_format: type[BaseModel] | None = None, + response_format: type[BaseModel] | MutableMapping[str, Any] | None = None, tools: ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] @@ -162,7 +162,8 @@ async def create_agent( description: A description of the agent. temperature: The sampling temperature to use. top_p: The nucleus sampling probability to use. - response_format: The format of the response (Pydantic model for structured output). + response_format: The format of the response. Can be a Pydantic model for structured + output, or a dict with JSON schema configuration. tools: Tools to make available to the agent. Returns: @@ -201,7 +202,16 @@ async def create_agent( # Pass the user-provided tools for function invocation normalized_tools = ChatOptions(tools=tools).tools if tools else None - return self._create_chat_agent_from_details(created_agent, normalized_tools) + + # Only pass Pydantic models to ChatAgent for response parsing + # Dict schemas are used by Azure AI for formatting, but can't be used for local parsing + pydantic_response_format = ( + response_format if isinstance(response_format, type) and issubclass(response_format, BaseModel) else None + ) + + return self._create_chat_agent_from_details( + created_agent, normalized_tools, response_format=pydantic_response_format + ) async def get_agent( self, @@ -295,6 +305,7 @@ def _create_chat_agent_from_details( self, details: AgentVersionDetails, provided_tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None = None, + response_format: type[BaseModel] | None = None, ) -> ChatAgent: """Create a ChatAgent from an AgentVersionDetails. @@ -302,6 +313,7 @@ def _create_chat_agent_from_details( details: The AgentVersionDetails containing the agent definition. provided_tools: User-provided tools (including function implementations). These are merged with hosted tools from the definition. + response_format: The Pydantic model type for structured output parsing. """ if not isinstance(details.definition, PromptAgentDefinition): raise ValueError("Agent definition must be PromptAgentDefinition to get a ChatAgent.") @@ -328,6 +340,7 @@ def _create_chat_agent_from_details( temperature=details.definition.temperature, top_p=details.definition.top_p, tools=merged_tools, + response_format=response_format, ) def _merge_tools( 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 0dafaa06ea..aa1132298c 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 @@ -29,7 +29,7 @@ async def main() -> None: - """Example of using response_format property.""" + """Example of using response_format property with a runtime JSON schema.""" # For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred # authentication option. @@ -37,27 +37,23 @@ async def main() -> None: AzureCliCredential() as credential, AzureAIProjectAgentProvider(credential=credential) as provider, ): + # Pass response_format at agent creation time using dict schema format agent = await provider.create_agent( - name="ProductMarketerAgent", + 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, + }, + }, ) query = "Draft a sample weather digest." print(f"User: {query}") - result = await agent.run( - query, - # Specify type to use as response - additional_chat_options={ - "response_format": { - "type": "json_schema", - "json_schema": { - "name": runtime_schema["title"], - "strict": True, - "schema": runtime_schema, - }, - }, - }, - ) + result = await agent.run(query) print(result.text) From 0accc46ba9d3e6e10b54a52170747cfcf34da477 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:03:34 -0800 Subject: [PATCH 10/16] Added provider example --- .../getting_started/agents/azure_ai/README.md | 3 +- .../azure_ai/azure_ai_provider_methods.py | 306 ++++++++++++++++++ 2 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py diff --git a/python/samples/getting_started/agents/azure_ai/README.md b/python/samples/getting_started/agents/azure_ai/README.md index 8ed95ad091..f093217b51 100644 --- a/python/samples/getting_started/agents/azure_ai/README.md +++ b/python/samples/getting_started/agents/azure_ai/README.md @@ -6,7 +6,8 @@ This folder contains examples demonstrating different ways to create and use age | File | Description | |------|-------------| -| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `AzureAIClient`. Demonstrates both streaming and non-streaming responses with function tools. Shows automatic agent creation and basic weather functionality. | +| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `AzureAIProjectAgentProvider`. Demonstrates both streaming and non-streaming responses with function tools. Shows automatic agent creation and basic weather functionality. | +| [`azure_ai_provider_methods.py`](azure_ai_provider_methods.py) | Comprehensive guide to `AzureAIProjectAgentProvider` methods: `create_agent()` for creating new agents, `get_agent()` for retrieving existing agents (by name, reference, or details), and `as_agent()` for wrapping SDK objects without HTTP calls. | | [`azure_ai_use_latest_version.py`](azure_ai_use_latest_version.py) | Demonstrates how to reuse the latest version of an existing agent instead of creating a new agent version on each instantiation using the `use_latest_version=True` parameter. | | [`azure_ai_with_agent_to_agent.py`](azure_ai_with_agent_to_agent.py) | Shows how to use Agent-to-Agent (A2A) capabilities with Azure AI agents to enable communication with other agents using the A2A protocol. Requires an A2A connection configured in your Azure AI project. | | [`azure_ai_with_azure_ai_search.py`](azure_ai_with_azure_ai_search.py) | Shows how to use Azure AI Search with Azure AI agents to search through indexed data and answer user questions with proper citations. Requires an Azure AI Search connection and index configured in your Azure AI project. | diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py b/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py new file mode 100644 index 0000000000..53fd694849 --- /dev/null +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py @@ -0,0 +1,306 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os +from random import randint +from typing import Annotated + +from agent_framework import ChatAgent +from agent_framework.azure import AzureAIProjectAgentProvider +from azure.ai.projects.aio import AIProjectClient +from azure.ai.projects.models import AgentReference, PromptAgentDefinition +from azure.identity.aio import AzureCliCredential +from pydantic import Field + +""" +Azure AI Project Agent Provider Methods Example + +This sample demonstrates the three main methods of AzureAIProjectAgentProvider: +1. create_agent() - Create a new agent on the Azure AI service +2. get_agent() - Retrieve an existing agent from the service +3. as_agent() - Wrap an SDK agent version object without making HTTP calls + +It also shows how to use a single provider instance to spawn multiple agents +with different configurations, which is efficient for multi-agent scenarios. + +Each method returns a ChatAgent that can be used for conversations. +""" + + +def get_weather( + location: Annotated[str, Field(description="The location to get the weather for.")], +) -> str: + """Get the weather for a given location.""" + conditions = ["sunny", "cloudy", "rainy", "stormy"] + return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}C." + + +async def create_agent_example() -> None: + """Example of using provider.create_agent() to create a new agent. + + This method creates a new agent version on the Azure AI service and returns + a ChatAgent. Use this when you want to create a fresh agent with + specific configuration. + """ + print("=== provider.create_agent() Example ===") + + async with ( + AzureCliCredential() as credential, + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + # Create a new agent with custom configuration + agent: ChatAgent = await provider.create_agent( + name="WeatherAssistant", + instructions="You are a helpful weather assistant. Always be concise.", + description="An agent that provides weather information.", + tools=get_weather, + temperature=0.7, + ) + + print(f"Created agent: {agent.name}") + print(f"Agent ID: {agent.id}") + + query = "What's the weather in Paris?" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result}\n") + + +async def get_agent_by_name_example() -> None: + """Example of using provider.get_agent(name=...) to retrieve an agent by name. + + This method fetches the latest version of an existing agent from the service. + Use this when you know the agent name and want to use the most recent version. + """ + print("=== provider.get_agent(name=...) Example ===") + + async with ( + AzureCliCredential() as credential, + AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, + ): + # First, create an agent using the SDK directly + created_agent = await project_client.agents.create_version( + agent_name="TestAgentByName", + description="Test agent for get_agent by name example.", + definition=PromptAgentDefinition( + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + instructions="You are a helpful assistant. End each response with '- Your Assistant'.", + ), + ) + + try: + # Get the agent using the provider by name (fetches latest version) + provider = AzureAIProjectAgentProvider(project_client=project_client) + agent: ChatAgent = await provider.get_agent(name=created_agent.name) + + print(f"Retrieved agent: {agent.name}") + print(f"Agent instructions: {agent.chat_options.instructions}") + + query = "Hello!" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result}\n") + finally: + # Clean up the agent + await project_client.agents.delete_version( + agent_name=created_agent.name, agent_version=created_agent.version + ) + + +async def get_agent_by_reference_example() -> None: + """Example of using provider.get_agent(reference=...) to retrieve a specific agent version. + + This method fetches a specific version of an agent using an AgentReference. + Use this when you need to use a particular version of an agent. + """ + print("=== provider.get_agent(reference=...) Example ===") + + async with ( + AzureCliCredential() as credential, + AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, + ): + # First, create an agent using the SDK directly + created_agent = await project_client.agents.create_version( + agent_name="TestAgentByReference", + description="Test agent for get_agent by reference example.", + definition=PromptAgentDefinition( + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + instructions="You are a helpful assistant. Always respond in uppercase.", + ), + ) + + try: + # Get the agent using an AgentReference with specific version + provider = AzureAIProjectAgentProvider(project_client=project_client) + reference = AgentReference(name=created_agent.name, version=created_agent.version) + agent: ChatAgent = await provider.get_agent(reference=reference) + + print(f"Retrieved agent: {agent.name} (version via reference)") + print(f"Agent instructions: {agent.chat_options.instructions}") + + query = "Say hello" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result}\n") + finally: + # Clean up the agent + await project_client.agents.delete_version( + agent_name=created_agent.name, agent_version=created_agent.version + ) + + +async def get_agent_by_details_example() -> None: + """Example of using provider.get_agent(details=...) with pre-fetched AgentDetails. + + This method uses pre-fetched AgentDetails to get the latest version. + Use this when you already have AgentDetails from a previous API call. + """ + print("=== provider.get_agent(details=...) Example ===") + + async with ( + AzureCliCredential() as credential, + AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, + ): + # First, create an agent using the SDK directly + created_agent = await project_client.agents.create_version( + agent_name="TestAgentByDetails", + description="Test agent for get_agent by details example.", + definition=PromptAgentDefinition( + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + instructions="You are a helpful assistant. Always include an emoji in your response.", + ), + ) + + try: + # Fetch AgentDetails separately (simulating a previous API call) + agent_details = await project_client.agents.get(agent_name=created_agent.name) + + # Get the agent using the pre-fetched details + provider = AzureAIProjectAgentProvider(project_client=project_client) + agent: ChatAgent = await provider.get_agent(details=agent_details) + + print(f"Retrieved agent: {agent.name} (from pre-fetched details)") + print(f"Agent instructions: {agent.chat_options.instructions}") + + query = "How are you today?" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result}\n") + finally: + # Clean up the agent + await project_client.agents.delete_version( + agent_name=created_agent.name, agent_version=created_agent.version + ) + + +async def multiple_agents_example() -> None: + """Example of using a single provider to spawn multiple agents. + + A single provider instance can create multiple agents with different + configurations. + """ + print("=== Multiple Agents from Single Provider Example ===") + + async with ( + AzureCliCredential() as credential, + AzureAIProjectAgentProvider(credential=credential) as provider, + ): + # Create multiple specialized agents from the same provider + weather_agent = await provider.create_agent( + name="WeatherExpert", + instructions="You are a weather expert. Provide brief weather information.", + tools=get_weather, + ) + + translator_agent = await provider.create_agent( + name="Translator", + instructions="You are a translator. Translate any text to French. Only output the translation.", + ) + + poet_agent = await provider.create_agent( + name="Poet", + instructions="You are a poet. Respond to everything with a short haiku.", + ) + + print(f"Created agents: {weather_agent.name}, {translator_agent.name}, {poet_agent.name}\n") + + # Use each agent for its specialty + weather_query = "What's the weather in London?" + print(f"User to WeatherExpert: {weather_query}") + weather_result = await weather_agent.run(weather_query) + print(f"WeatherExpert: {weather_result}\n") + + translate_query = "Hello, how are you today?" + print(f"User to Translator: {translate_query}") + translate_result = await translator_agent.run(translate_query) + print(f"Translator: {translate_result}\n") + + poet_query = "Tell me about the morning sun" + print(f"User to Poet: {poet_query}") + poet_result = await poet_agent.run(poet_query) + print(f"Poet: {poet_result}\n") + + +async def as_agent_example() -> None: + """Example of using provider.as_agent() to wrap an SDK object without HTTP calls. + + This method wraps an existing AgentVersionDetails into a ChatAgent without + making additional HTTP calls. Use this when you already have the full + AgentVersionDetails from a previous SDK operation. + """ + print("=== provider.as_agent() Example ===") + + async with ( + AzureCliCredential() as credential, + AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client, + ): + # Create an agent using the SDK directly - this returns AgentVersionDetails + agent_version_details = await project_client.agents.create_version( + agent_name="TestAgentAsAgent", + description="Test agent for as_agent example.", + definition=PromptAgentDefinition( + model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], + instructions="You are a helpful assistant. Keep responses under 20 words.", + ), + ) + + try: + # Wrap the SDK object directly without any HTTP calls + provider = AzureAIProjectAgentProvider(project_client=project_client) + agent: ChatAgent = provider.as_agent(details=agent_version_details) + + print(f"Wrapped agent: {agent.name} (no HTTP call needed)") + print(f"Agent version: {agent_version_details.version}") + print(f"Agent instructions: {agent.chat_options.instructions}") + + query = "What can you do?" + print(f"User: {query}") + result = await agent.run(query) + print(f"Agent: {result}\n") + finally: + # Clean up the agent + await project_client.agents.delete_version( + agent_name=agent_version_details.name, agent_version=agent_version_details.version + ) + + +async def main() -> None: + print("=== Azure AI Project Agent Provider Methods Example ===\n") + + # create_agent() - Create a new agent + await create_agent_example() + + # Multiple agents from single provider + await multiple_agents_example() + + # get_agent() - Retrieve existing agents (three ways) + await get_agent_by_name_example() + await get_agent_by_reference_example() + await get_agent_by_details_example() + + # as_agent() - Wrap SDK objects without HTTP calls + await as_agent_example() + + +if __name__ == "__main__": + asyncio.run(main()) From 470a51b5ccc4503bf447f5c3e6729ce5755ebb18 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 12 Jan 2026 17:18:52 -0800 Subject: [PATCH 11/16] Fixed errors --- .../agent_framework_azure_ai/_provider.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_provider.py index f19289032e..b9e9de9130 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_provider.py @@ -201,7 +201,7 @@ async def create_agent( ) # Pass the user-provided tools for function invocation - normalized_tools = ChatOptions(tools=tools).tools if tools else None + chat_agent_tools = ChatOptions(tools=tools).tools if tools else None # Only pass Pydantic models to ChatAgent for response parsing # Dict schemas are used by Azure AI for formatting, but can't be used for local parsing @@ -210,7 +210,7 @@ async def create_agent( ) return self._create_chat_agent_from_details( - created_agent, normalized_tools, response_format=pydantic_response_format + created_agent, chat_agent_tools, response_format=pydantic_response_format ) async def get_agent( @@ -362,17 +362,17 @@ def _merge_tools( # Convert hosted tools from definition (MCP, code interpreter, file search, web search) # Function tools from the definition are skipped - we use user-provided implementations instead hosted_tools = from_azure_ai_tools(definition_tools) - for tool in hosted_tools: + for hosted_tool in hosted_tools: # Skip function tool dicts - they don't have implementations - if isinstance(tool, dict) and tool.get("type") == "function": + if isinstance(hosted_tool, dict) and hosted_tool.get("type") == "function": continue - merged.append(tool) + merged.append(hosted_tool) # Add user-provided function tools (these have the actual implementations) if provided_tools: - for tool in provided_tools: - if isinstance(tool, AIFunction): - merged.append(tool) + for provided_tool in provided_tools: + if isinstance(provided_tool, AIFunction): + merged.append(provided_tool) # type: ignore[reportUnknownArgumentType] return merged From ddb9e1a03f1a444bd31379e6ae6e88f6010577f0 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 12 Jan 2026 17:44:24 -0800 Subject: [PATCH 12/16] Update python/samples/getting_started/agents/azure_ai/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- python/samples/getting_started/agents/azure_ai/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/samples/getting_started/agents/azure_ai/README.md b/python/samples/getting_started/agents/azure_ai/README.md index f093217b51..1bac0f4cc6 100644 --- a/python/samples/getting_started/agents/azure_ai/README.md +++ b/python/samples/getting_started/agents/azure_ai/README.md @@ -8,7 +8,7 @@ This folder contains examples demonstrating different ways to create and use age |------|-------------| | [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `AzureAIProjectAgentProvider`. Demonstrates both streaming and non-streaming responses with function tools. Shows automatic agent creation and basic weather functionality. | | [`azure_ai_provider_methods.py`](azure_ai_provider_methods.py) | Comprehensive guide to `AzureAIProjectAgentProvider` methods: `create_agent()` for creating new agents, `get_agent()` for retrieving existing agents (by name, reference, or details), and `as_agent()` for wrapping SDK objects without HTTP calls. | -| [`azure_ai_use_latest_version.py`](azure_ai_use_latest_version.py) | Demonstrates how to reuse the latest version of an existing agent instead of creating a new agent version on each instantiation using the `use_latest_version=True` parameter. | +| [`azure_ai_use_latest_version.py`](azure_ai_use_latest_version.py) | Demonstrates how to reuse the latest version of an existing agent instead of creating a new agent version on each instantiation by using `provider.get_agent()` to retrieve the latest version. | | [`azure_ai_with_agent_to_agent.py`](azure_ai_with_agent_to_agent.py) | Shows how to use Agent-to-Agent (A2A) capabilities with Azure AI agents to enable communication with other agents using the A2A protocol. Requires an A2A connection configured in your Azure AI project. | | [`azure_ai_with_azure_ai_search.py`](azure_ai_with_azure_ai_search.py) | Shows how to use Azure AI Search with Azure AI agents to search through indexed data and answer user questions with proper citations. Requires an Azure AI Search connection and index configured in your Azure AI project. | | [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to search the web for current information and provide grounded responses with citations. Requires a Bing connection configured in your Azure AI project. | From b852ac9b902de752c500921c0fcd774529aecbc4 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Mon, 12 Jan 2026 23:27:32 -0800 Subject: [PATCH 13/16] Small fix --- .../agent_framework_azure_ai/_provider.py | 19 +++++++------------ .../packages/azure-ai/tests/test_provider.py | 2 +- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_provider.py index b9e9de9130..93051ee2ac 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_provider.py @@ -15,7 +15,6 @@ from agent_framework.exceptions import ServiceInitializationError from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ( - AgentDetails, AgentReference, AgentVersionDetails, FunctionTool, @@ -218,7 +217,6 @@ async def get_agent( *, name: str | None = None, reference: AgentReference | None = None, - details: AgentDetails | None = None, tools: ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] @@ -227,12 +225,12 @@ async def get_agent( ) -> ChatAgent: """Retrieve an existing agent from the Azure AI service and return a local ChatAgent wrapper. - You must provide one of: name, reference, or details. + You must provide either name or reference. Use `as_agent()` if you already have + AgentVersionDetails and want to avoid an async call. Args: name: The name of the agent to retrieve (fetches latest version). reference: Reference containing the agent's name and optionally a specific version. - details: A pre-fetched AgentDetails object (uses latest version from it). tools: Tools to make available to the agent. Required if the agent has function tools. Returns: @@ -248,15 +246,12 @@ async def get_agent( existing_agent = await self._project_client.agents.get_version( agent_name=reference.name, agent_version=reference.version ) - else: - # Get agent details if not provided - if agent_name := (reference.name if reference else name): - details = await self._project_client.agents.get(agent_name=agent_name) - - if not details: - raise ValueError("Either name, reference, or details must be provided to get an agent.") - + elif agent_name := (reference.name if reference else name): + # Fetch latest version + details = await self._project_client.agents.get(agent_name=agent_name) existing_agent = details.versions.latest + else: + raise ValueError("Either name or reference must be provided to get an agent.") if not isinstance(existing_agent.definition, PromptAgentDefinition): raise ValueError("Agent definition must be PromptAgentDefinition to get a ChatAgent.") diff --git a/python/packages/azure-ai/tests/test_provider.py b/python/packages/azure-ai/tests/test_provider.py index 834221e353..ad79c6f5f6 100644 --- a/python/packages/azure-ai/tests/test_provider.py +++ b/python/packages/azure-ai/tests/test_provider.py @@ -271,7 +271,7 @@ async def test_provider_get_agent_missing_parameters(mock_project_client: MagicM """Test AzureAIProjectAgentProvider.get_agent raises when no identifier provided.""" provider = AzureAIProjectAgentProvider(project_client=mock_project_client) - with pytest.raises(ValueError, match="Either name, reference, or details must be provided"): + with pytest.raises(ValueError, match="Either name or reference must be provided"): await provider.get_agent() From b4b7b7593dba7580767402ba401c3afcdd771210 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:42:02 -0800 Subject: [PATCH 14/16] Updates from merge --- .../azure-ai/agent_framework_azure_ai/_shared.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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 727482f287..ecab7da9c4 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_shared.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_shared.py @@ -274,7 +274,7 @@ def _prepare_mcp_tool_for_azure_ai(tool: HostedMCPTool) -> MCPTool: def create_text_format_config( - response_format: Any, + response_format: type[BaseModel] | Mapping[str, Any], ) -> ( ResponseTextFormatConfigurationJsonSchema | ResponseTextFormatConfigurationJsonObject @@ -282,18 +282,25 @@ def create_text_format_config( ): """Convert response_format into Azure text format configuration.""" if isinstance(response_format, type) and issubclass(response_format, BaseModel): + schema = response_format.model_json_schema() + # Ensure additionalProperties is explicitly false to satisfy Azure validation + if isinstance(schema, dict): + schema.setdefault("additionalProperties", False) return ResponseTextFormatConfigurationJsonSchema( name=response_format.__name__, - schema=response_format.model_json_schema(), + schema=schema, ) if isinstance(response_format, Mapping): format_config = _convert_response_format(response_format) format_type = format_config.get("type") if format_type == "json_schema": + # Ensure schema includes additionalProperties=False to satisfy Azure validation + schema = dict(format_config.get("schema", {})) # type: ignore[assignment] + schema.setdefault("additionalProperties", False) config_kwargs: dict[str, Any] = { "name": format_config.get("name") or "response", - "schema": format_config["schema"], + "schema": schema, } if "strict" in format_config: config_kwargs["strict"] = format_config["strict"] From 1a2b3f25b22c5c009a8431b1c498287e7fa28812 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:55:19 -0800 Subject: [PATCH 15/16] Resolved comments --- .../agent_framework_azure_ai/_provider.py | 102 +++++++++++++----- .../packages/core/agent_framework/_types.py | 75 ++++++++++--- .../azure_ai/azure_ai_provider_methods.py | 26 ++--- .../azure_ai/azure_ai_with_existing_agent.py | 4 +- 4 files changed, 148 insertions(+), 59 deletions(-) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_provider.py index 93051ee2ac..a8f93ae20b 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_provider.py @@ -2,15 +2,17 @@ import sys from collections.abc import Callable, MutableMapping, Sequence -from typing import Any +from typing import Any, Generic, TypedDict from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, AIFunction, ChatAgent, - ChatOptions, + ContextProvider, + Middleware, ToolProtocol, get_logger, + normalize_tools, ) from agent_framework.exceptions import ServiceInitializationError from azure.ai.projects.aio import AIProjectClient @@ -27,16 +29,25 @@ from ._client import AzureAIClient from ._shared import AzureAISettings, create_text_format_config, from_azure_ai_tools, to_azure_ai_tools -if sys.version_info >= (3, 11): - from typing import Self # pragma: no cover +if sys.version_info >= (3, 13): + from typing import Self, TypeVar # pragma: no cover else: - from typing_extensions import Self # pragma: no cover + from typing_extensions import Self, TypeVar # pragma: no cover logger = get_logger("agent_framework.azure") -class AzureAIProjectAgentProvider: +# Type variable for options - allows typed ChatAgent[TOptions] returns +TOptions_co = TypeVar( + "TOptions_co", + bound=TypedDict, # type: ignore[valid-type] + default=TypedDict, # type: ignore[valid-type] + covariant=True, +) + + +class AzureAIProjectAgentProvider(Generic[TOptions_co]): """Provider for Azure AI Agent Service (Responses API). This provider allows you to create, retrieve, and manage Azure AI agents @@ -150,7 +161,10 @@ async def create_agent( | MutableMapping[str, Any] | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] | None = None, - ) -> ChatAgent: + default_options: TOptions_co | None = None, + middleware: Sequence[Middleware] | None = None, + context_provider: ContextProvider | None = None, + ) -> "ChatAgent[TOptions_co]": """Create a new agent on the Azure AI service and return a local ChatAgent wrapper. Args: @@ -164,6 +178,10 @@ async def create_agent( response_format: The format of the response. Can be a Pydantic model for structured output, or a dict with JSON schema configuration. tools: Tools to make available to the agent. + default_options: A TypedDict containing default chat options for the agent. + These options are applied to every run unless overridden. + middleware: List of middleware to intercept agent and function invocations. + context_provider: Context provider to include during agent invocation. Returns: ChatAgent: A ChatAgent instance configured with the created agent. @@ -189,8 +207,10 @@ async def create_agent( args["top_p"] = top_p if response_format: args["text"] = PromptAgentDefinitionText(format=create_text_format_config(response_format)) - if tools: - normalized_tools = ChatOptions(tools=tools).tools or [] + + # Normalize tools once and reuse for both Azure AI API and ChatAgent + normalized_tools = normalize_tools(tools) + if normalized_tools: args["tools"] = to_azure_ai_tools(normalized_tools) created_agent = await self._project_client.agents.create_version( @@ -199,9 +219,6 @@ async def create_agent( description=description, ) - # Pass the user-provided tools for function invocation - chat_agent_tools = ChatOptions(tools=tools).tools if tools else None - # Only pass Pydantic models to ChatAgent for response parsing # Dict schemas are used by Azure AI for formatting, but can't be used for local parsing pydantic_response_format = ( @@ -209,7 +226,12 @@ async def create_agent( ) return self._create_chat_agent_from_details( - created_agent, chat_agent_tools, response_format=pydantic_response_format + created_agent, + normalized_tools, + response_format=pydantic_response_format, + default_options=default_options, + middleware=middleware, + context_provider=context_provider, ) async def get_agent( @@ -222,7 +244,10 @@ async def get_agent( | MutableMapping[str, Any] | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] | None = None, - ) -> ChatAgent: + default_options: TOptions_co | None = None, + middleware: Sequence[Middleware] | None = None, + context_provider: ContextProvider | None = None, + ) -> "ChatAgent[TOptions_co]": """Retrieve an existing agent from the Azure AI service and return a local ChatAgent wrapper. You must provide either name or reference. Use `as_agent()` if you already have @@ -232,6 +257,10 @@ async def get_agent( name: The name of the agent to retrieve (fetches latest version). reference: Reference containing the agent's name and optionally a specific version. tools: Tools to make available to the agent. Required if the agent has function tools. + default_options: A TypedDict containing default chat options for the agent. + These options are applied to every run unless overridden. + middleware: List of middleware to intercept agent and function invocations. + context_provider: Context provider to include during agent invocation. Returns: ChatAgent: A ChatAgent instance configured with the retrieved agent. @@ -259,9 +288,13 @@ async def get_agent( # Validate that required function tools are provided self._validate_function_tools(existing_agent.definition.tools, tools) - # Pass user-provided tools for function invocation - normalized_tools = ChatOptions(tools=tools).tools if tools else None - return self._create_chat_agent_from_details(existing_agent, normalized_tools) + return self._create_chat_agent_from_details( + existing_agent, + normalize_tools(tools), + default_options=default_options, + middleware=middleware, + context_provider=context_provider, + ) def as_agent( self, @@ -271,7 +304,10 @@ def as_agent( | MutableMapping[str, Any] | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] | None = None, - ) -> ChatAgent: + default_options: TOptions_co | None = None, + middleware: Sequence[Middleware] | None = None, + context_provider: ContextProvider | None = None, + ) -> "ChatAgent[TOptions_co]": """Wrap an SDK agent version object into a ChatAgent without making HTTP calls. Use this when you already have an AgentVersionDetails from a previous API call. @@ -279,6 +315,10 @@ def as_agent( Args: details: The AgentVersionDetails to wrap. tools: Tools to make available to the agent. Required if the agent has function tools. + default_options: A TypedDict containing default chat options for the agent. + These options are applied to every run unless overridden. + middleware: List of middleware to intercept agent and function invocations. + context_provider: Context provider to include during agent invocation. Returns: ChatAgent: A ChatAgent instance configured with the agent version. @@ -292,16 +332,23 @@ def as_agent( # Validate that required function tools are provided self._validate_function_tools(details.definition.tools, tools) - # Pass user-provided tools for function invocation - normalized_tools = ChatOptions(tools=tools).tools if tools else None - return self._create_chat_agent_from_details(details, normalized_tools) + return self._create_chat_agent_from_details( + details, + normalize_tools(tools), + default_options=default_options, + middleware=middleware, + context_provider=context_provider, + ) def _create_chat_agent_from_details( self, details: AgentVersionDetails, provided_tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None = None, response_format: type[BaseModel] | None = None, - ) -> ChatAgent: + default_options: TOptions_co | None = None, + middleware: Sequence[Middleware] | None = None, + context_provider: ContextProvider | None = None, + ) -> "ChatAgent[TOptions_co]": """Create a ChatAgent from an AgentVersionDetails. Args: @@ -309,6 +356,10 @@ def _create_chat_agent_from_details( provided_tools: User-provided tools (including function implementations). These are merged with hosted tools from the definition. response_format: The Pydantic model type for structured output parsing. + default_options: A TypedDict containing default chat options for the agent. + These options are applied to every run unless overridden. + middleware: List of middleware to intercept agent and function invocations. + context_provider: Context provider to include during agent invocation. """ if not isinstance(details.definition, PromptAgentDefinition): raise ValueError("Agent definition must be PromptAgentDefinition to get a ChatAgent.") @@ -325,7 +376,7 @@ def _create_chat_agent_from_details( # but function tools need the actual implementations from provided_tools merged_tools = self._merge_tools(details.definition.tools, provided_tools) - return ChatAgent( + return ChatAgent( # type: ignore[return-value] chat_client=client, id=details.id, name=details.name, @@ -336,6 +387,9 @@ def _create_chat_agent_from_details( top_p=details.definition.top_p, tools=merged_tools, response_format=response_format, + default_options=default_options, # type: ignore[arg-type] + middleware=middleware, + context_provider=context_provider, ) def _merge_tools( @@ -382,7 +436,7 @@ def _validate_function_tools( ) -> None: """Validate that required function tools are provided.""" # Normalize and validate function tools - normalized_tools = ChatOptions(tools=provided_tools).tools or [] + normalized_tools = normalize_tools(provided_tools) tool_names = {tool.name for tool in normalized_tools if isinstance(tool, AIFunction)} # If function tools exist in agent definition but were not provided, diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index 3df5280e0f..2dad305e46 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -66,6 +66,7 @@ "UsageContent", "UsageDetails", "merge_chat_options", + "normalize_tools", "prepare_function_call_results", "prepend_instructions_to_messages", "validate_chat_options", @@ -3490,6 +3491,60 @@ async def validate_chat_options(options: dict[str, Any]) -> dict[str, Any]: return result +def normalize_tools( + tools: ( + ToolProtocol + | Callable[..., Any] + | MutableMapping[str, Any] + | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | None + ), +) -> list[ToolProtocol | MutableMapping[str, Any]]: + """Normalize tools into a list. + + Converts callables to AIFunction objects and ensures all tools are either + ToolProtocol instances or MutableMappings. + + Args: + tools: Tools to normalize - can be a single tool, callable, or sequence. + + Returns: + Normalized list of tools. + + Examples: + .. code-block:: python + + from agent_framework import normalize_tools, ai_function + + + @ai_function + def my_tool(x: int) -> int: + return x * 2 + + + # Single tool + tools = normalize_tools(my_tool) + + # List of tools + tools = normalize_tools([my_tool, another_tool]) + """ + final_tools: list[ToolProtocol | MutableMapping[str, Any]] = [] + if not tools: + return final_tools + if not isinstance(tools, Sequence) or isinstance(tools, (str, MutableMapping)): + # Single tool (not a sequence, or is a mapping which shouldn't be treated as sequence) + if not isinstance(tools, (ToolProtocol, MutableMapping)): + return [ai_function(tools)] + return [tools] + for tool in tools: + if isinstance(tool, (ToolProtocol, MutableMapping)): + final_tools.append(tool) + else: + # Convert callable to AIFunction + final_tools.append(ai_function(tool)) + return final_tools + + async def validate_tools( tools: ( ToolProtocol @@ -3528,16 +3583,12 @@ def my_tool(x: int) -> int: # List of tools tools = await validate_tools([my_tool, another_tool]) """ - # Sequence of tools - convert callables and expand MCP tools + # Use normalize_tools for common sync logic (converts callables to AIFunction) + normalized = normalize_tools(tools) + + # Handle MCP tool expansion (async-only) final_tools: list[ToolProtocol | MutableMapping[str, Any]] = [] - if not tools: - return final_tools - if not isinstance(tools, Sequence) or isinstance(tools, (str, MutableMapping)): - # Single tool (not a sequence, or is a mapping which shouldn't be treated as sequence) - if not isinstance(tools, (ToolProtocol, MutableMapping)): - return [ai_function(tools)] - return [tools] - for tool in tools: + for tool in normalized: # Import MCPTool here to avoid circular imports from ._mcp import MCPTool @@ -3546,11 +3597,9 @@ def my_tool(x: int) -> int: if not tool.is_connected: await tool.connect() final_tools.extend(tool.functions) # type: ignore - elif isinstance(tool, (ToolProtocol, MutableMapping)): - final_tools.append(tool) else: - # Convert callable to AIFunction - final_tools.append(ai_function(tool)) + final_tools.append(tool) + return final_tools diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py b/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py index 53fd694849..6a5b49bc28 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py @@ -5,7 +5,6 @@ from random import randint from typing import Annotated -from agent_framework import ChatAgent from agent_framework.azure import AzureAIProjectAgentProvider from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import AgentReference, PromptAgentDefinition @@ -49,7 +48,7 @@ async def create_agent_example() -> None: AzureAIProjectAgentProvider(credential=credential) as provider, ): # Create a new agent with custom configuration - agent: ChatAgent = await provider.create_agent( + agent = await provider.create_agent( name="WeatherAssistant", instructions="You are a helpful weather assistant. Always be concise.", description="An agent that provides weather information.", @@ -91,10 +90,9 @@ async def get_agent_by_name_example() -> None: try: # Get the agent using the provider by name (fetches latest version) provider = AzureAIProjectAgentProvider(project_client=project_client) - agent: ChatAgent = await provider.get_agent(name=created_agent.name) + agent = await provider.get_agent(name=created_agent.name) print(f"Retrieved agent: {agent.name}") - print(f"Agent instructions: {agent.chat_options.instructions}") query = "Hello!" print(f"User: {query}") @@ -133,10 +131,9 @@ async def get_agent_by_reference_example() -> None: # Get the agent using an AgentReference with specific version provider = AzureAIProjectAgentProvider(project_client=project_client) reference = AgentReference(name=created_agent.name, version=created_agent.version) - agent: ChatAgent = await provider.get_agent(reference=reference) + agent = await provider.get_agent(reference=reference) print(f"Retrieved agent: {agent.name} (version via reference)") - print(f"Agent instructions: {agent.chat_options.instructions}") query = "Say hello" print(f"User: {query}") @@ -175,12 +172,11 @@ async def get_agent_by_details_example() -> None: # Fetch AgentDetails separately (simulating a previous API call) agent_details = await project_client.agents.get(agent_name=created_agent.name) - # Get the agent using the pre-fetched details + # Get the agent using the pre-fetched details (sync - no HTTP call) provider = AzureAIProjectAgentProvider(project_client=project_client) - agent: ChatAgent = await provider.get_agent(details=agent_details) + agent = provider.as_agent(agent_details.versions.latest) print(f"Retrieved agent: {agent.name} (from pre-fetched details)") - print(f"Agent instructions: {agent.chat_options.instructions}") query = "How are you today?" print(f"User: {query}") @@ -267,11 +263,10 @@ async def as_agent_example() -> None: try: # Wrap the SDK object directly without any HTTP calls provider = AzureAIProjectAgentProvider(project_client=project_client) - agent: ChatAgent = provider.as_agent(details=agent_version_details) + agent = provider.as_agent(agent_version_details) print(f"Wrapped agent: {agent.name} (no HTTP call needed)") print(f"Agent version: {agent_version_details.version}") - print(f"Agent instructions: {agent.chat_options.instructions}") query = "What can you do?" print(f"User: {query}") @@ -287,19 +282,12 @@ async def as_agent_example() -> None: async def main() -> None: print("=== Azure AI Project Agent Provider Methods Example ===\n") - # create_agent() - Create a new agent await create_agent_example() - - # Multiple agents from single provider - await multiple_agents_example() - - # get_agent() - Retrieve existing agents (three ways) await get_agent_by_name_example() await get_agent_by_reference_example() await get_agent_by_details_example() - - # as_agent() - Wrap SDK objects without HTTP calls await as_agent_example() + await multiple_agents_example() if __name__ == "__main__": diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py index c403e9c822..7341068f10 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_agent.py @@ -3,7 +3,6 @@ import asyncio import os -from agent_framework import ChatAgent from agent_framework.azure import AzureAIProjectAgentProvider from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import PromptAgentDefinition @@ -39,13 +38,12 @@ async def using_provider_get_agent() -> None: try: # Get newly created agent as ChatAgent by using provider.get_agent() provider = AzureAIProjectAgentProvider(project_client=project_client) - agent: ChatAgent = await provider.get_agent(name=azure_ai_agent.name) + agent = await provider.get_agent(name=azure_ai_agent.name) # Verify agent properties print(f"Agent ID: {agent.id}") print(f"Agent name: {agent.name}") print(f"Agent description: {agent.description}") - print(f"Agent instructions: {agent.chat_options.instructions}") query = "How are you?" print(f"User: {query}") From 5bdc5301a3c127abab48b84cd29089146dd23703 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Wed, 14 Jan 2026 09:19:53 -0800 Subject: [PATCH 16/16] Resolved comments --- .../agent_framework_azure_ai/_provider.py | 40 +++++++------------ .../packages/azure-ai/tests/test_provider.py | 2 - .../azure_ai/azure_ai_provider_methods.py | 1 - 3 files changed, 14 insertions(+), 29 deletions(-) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_provider.py b/python/packages/azure-ai/agent_framework_azure_ai/_provider.py index a8f93ae20b..5dc7e7624b 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_provider.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_provider.py @@ -2,7 +2,7 @@ import sys from collections.abc import Callable, MutableMapping, Sequence -from typing import Any, Generic, TypedDict +from typing import TYPE_CHECKING, Any, Generic, TypedDict from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, @@ -29,6 +29,9 @@ from ._client import AzureAIClient 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: @@ -39,10 +42,11 @@ # Type variable for options - allows typed ChatAgent[TOptions] returns +# Default matches AzureAIClient's default options type TOptions_co = TypeVar( "TOptions_co", bound=TypedDict, # type: ignore[valid-type] - default=TypedDict, # type: ignore[valid-type] + default="OpenAIResponsesOptions", covariant=True, ) @@ -145,7 +149,6 @@ def __init__( self._should_close_client = True self._project_client = project_client - self._credential = credential async def create_agent( self, @@ -153,8 +156,6 @@ async def create_agent( model: str | None = None, instructions: str | None = None, description: str | None = None, - temperature: float | None = None, - top_p: float | None = None, response_format: type[BaseModel] | MutableMapping[str, Any] | None = None, tools: ToolProtocol | Callable[..., Any] @@ -173,8 +174,6 @@ async def create_agent( environment variable if not provided. instructions: Instructions for the agent. description: A description of the agent. - temperature: The sampling temperature to use. - top_p: The nucleus sampling probability to use. response_format: The format of the response. Can be a Pydantic model for structured output, or a dict with JSON schema configuration. tools: Tools to make available to the agent. @@ -201,10 +200,6 @@ async def create_agent( if instructions: args["instructions"] = instructions - if temperature is not None: - args["temperature"] = temperature - if top_p is not None: - args["top_p"] = top_p if response_format: args["text"] = PromptAgentDefinitionText(format=create_text_format_config(response_format)) @@ -219,16 +214,10 @@ async def create_agent( description=description, ) - # Only pass Pydantic models to ChatAgent for response parsing - # Dict schemas are used by Azure AI for formatting, but can't be used for local parsing - pydantic_response_format = ( - response_format if isinstance(response_format, type) and issubclass(response_format, BaseModel) else None - ) - - return self._create_chat_agent_from_details( + return self._to_chat_agent_from_details( created_agent, normalized_tools, - response_format=pydantic_response_format, + response_format=response_format, default_options=default_options, middleware=middleware, context_provider=context_provider, @@ -288,7 +277,7 @@ async def get_agent( # Validate that required function tools are provided self._validate_function_tools(existing_agent.definition.tools, tools) - return self._create_chat_agent_from_details( + return self._to_chat_agent_from_details( existing_agent, normalize_tools(tools), default_options=default_options, @@ -332,7 +321,7 @@ def as_agent( # Validate that required function tools are provided self._validate_function_tools(details.definition.tools, tools) - return self._create_chat_agent_from_details( + return self._to_chat_agent_from_details( details, normalize_tools(tools), default_options=default_options, @@ -340,11 +329,11 @@ def as_agent( context_provider=context_provider, ) - def _create_chat_agent_from_details( + def _to_chat_agent_from_details( self, details: AgentVersionDetails, provided_tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None = None, - response_format: type[BaseModel] | None = None, + response_format: type[BaseModel] | MutableMapping[str, Any] | None = None, default_options: TOptions_co | None = None, middleware: Sequence[Middleware] | None = None, context_provider: ContextProvider | None = None, @@ -355,7 +344,8 @@ def _create_chat_agent_from_details( details: The AgentVersionDetails containing the agent definition. provided_tools: User-provided tools (including function implementations). These are merged with hosted tools from the definition. - response_format: The Pydantic model type for structured output parsing. + response_format: The response format. Can be a Pydantic model for structured + output parsing, or a dict with JSON schema for service-side formatting. default_options: A TypedDict containing default chat options for the agent. These options are applied to every run unless overridden. middleware: List of middleware to intercept agent and function invocations. @@ -383,8 +373,6 @@ def _create_chat_agent_from_details( description=details.description, instructions=details.definition.instructions, model_id=details.definition.model, - temperature=details.definition.temperature, - top_p=details.definition.top_p, tools=merged_tools, response_format=response_format, default_options=default_options, # type: ignore[arg-type] diff --git a/python/packages/azure-ai/tests/test_provider.py b/python/packages/azure-ai/tests/test_provider.py index ad79c6f5f6..cff18f19d5 100644 --- a/python/packages/azure-ai/tests/test_provider.py +++ b/python/packages/azure-ai/tests/test_provider.py @@ -153,8 +153,6 @@ async def test_provider_create_agent( model="gpt-4", instructions="Test instructions", description="Test Agent", - temperature=0.7, - top_p=0.9, ) assert isinstance(agent, ChatAgent) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py b/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py index 6a5b49bc28..0bf413c5d9 100644 --- a/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py @@ -53,7 +53,6 @@ async def create_agent_example() -> None: instructions="You are a helpful weather assistant. Always be concise.", description="An agent that provides weather information.", tools=get_weather, - temperature=0.7, ) print(f"Created agent: {agent.name}")