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 36ed5d696b..f5d6fce235 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/__init__.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/__init__.py @@ -4,6 +4,7 @@ from ._chat_client import AzureAIAgentClient, AzureAIAgentOptions from ._client import AzureAIClient +from ._provider import AzureAIProjectAgentProvider from ._shared import AzureAISettings try: @@ -15,6 +16,7 @@ "AzureAIAgentClient", "AzureAIAgentOptions", "AzureAIClient", + "AzureAIProjectAgentProvider", "AzureAISettings", "__version__", ] diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py index 6395993cd7..c735cce049 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 Mapping, MutableSequence -from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypedDict, cast +from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypedDict, TypeVar, cast from agent_framework import ( AGENT_FRAMEWORK_USER_AGENT, @@ -13,7 +13,7 @@ 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 @@ -21,15 +21,12 @@ MCPTool, PromptAgentDefinition, PromptAgentDefinitionText, - ResponseTextFormatConfigurationJsonObject, - ResponseTextFormatConfigurationJsonSchema, - ResponseTextFormatConfigurationText, ) 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 TYPE_CHECKING: from agent_framework.openai import OpenAIResponsesOptions @@ -286,47 +283,6 @@ async def close(self) -> None: """Close the project_client.""" await self._close_client_if_needed() - def _create_text_format_config( - self, response_format: type[BaseModel] | Mapping[str, Any] - ) -> ( - ResponseTextFormatConfigurationJsonSchema - | ResponseTextFormatConfigurationJsonObject - | ResponseTextFormatConfigurationText - ): - """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=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": - # 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": 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], @@ -380,7 +336,7 @@ async def _get_agent_reference_or_create( # response_format is accessed from chat_options or additional_properties # since the base class excludes it from run_options if chat_options and (response_format := chat_options.get("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 = [ 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..a8f93ae20b --- /dev/null +++ b/python/packages/azure-ai/agent_framework_azure_ai/_provider.py @@ -0,0 +1,467 @@ +# Copyright (c) Microsoft. All rights reserved. + +import sys +from collections.abc import Callable, MutableMapping, Sequence +from typing import Any, Generic, TypedDict + +from agent_framework import ( + AGENT_FRAMEWORK_USER_AGENT, + AIFunction, + ChatAgent, + ContextProvider, + Middleware, + ToolProtocol, + get_logger, + normalize_tools, +) +from agent_framework.exceptions import ServiceInitializationError +from azure.ai.projects.aio import AIProjectClient +from azure.ai.projects.models import ( + 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, 13): + from typing import Self, TypeVar # pragma: no cover +else: + from typing_extensions import Self, TypeVar # pragma: no cover + + +logger = get_logger("agent_framework.azure") + + +# 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 + 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] | MutableMapping[str, Any] | None = None, + tools: ToolProtocol + | Callable[..., Any] + | MutableMapping[str, Any] + | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | None = None, + 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: + 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. 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. + + 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)) + + # 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( + agent_name=name, + definition=PromptAgentDefinition(**args), + 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( + created_agent, + normalized_tools, + response_format=pydantic_response_format, + default_options=default_options, + middleware=middleware, + context_provider=context_provider, + ) + + async def get_agent( + self, + *, + name: str | None = None, + reference: AgentReference | None = None, + tools: ToolProtocol + | Callable[..., Any] + | MutableMapping[str, Any] + | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | None = None, + 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 + 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. + 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. + + 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 + ) + 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.") + + # Validate that required function tools are provided + self._validate_function_tools(existing_agent.definition.tools, 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, + details: AgentVersionDetails, + tools: ToolProtocol + | Callable[..., Any] + | MutableMapping[str, Any] + | Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] + | None = None, + 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. + + 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. + + 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) + + 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, + 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: + 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. + 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.") + + 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( # type: ignore[return-value] + 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, + response_format=response_format, + default_options=default_options, # type: ignore[arg-type] + middleware=middleware, + context_provider=context_provider, + ) + + 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 hosted_tool in hosted_tools: + # Skip function tool dicts - they don't have implementations + if isinstance(hosted_tool, dict) and hosted_tool.get("type") == "function": + continue + merged.append(hosted_tool) + + # Add user-provided function tools (these have the actual implementations) + if provided_tools: + for provided_tool in provided_tools: + if isinstance(provided_tool, AIFunction): + merged.append(provided_tool) # type: ignore[reportUnknownArgumentType] + + 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 = 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, + # 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..ecab7da9c4 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,279 @@ 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: type[BaseModel] | Mapping[str, Any], +) -> ( + ResponseTextFormatConfigurationJsonSchema + | ResponseTextFormatConfigurationJsonObject + | ResponseTextFormatConfigurationText +): + """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=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": 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 57180715e1..ec7b1bad71 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_client.py @@ -17,7 +17,10 @@ ChatOptions, ChatResponse, HostedCodeInterpreterTool, + HostedFileContent, + HostedFileSearchTool, HostedMCPTool, + HostedVectorStoreContent, HostedWebSearchTool, Role, TextContent, @@ -25,7 +28,13 @@ from agent_framework.exceptions import ServiceInitializationError from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.models import ( + ApproximateLocation, + CodeInterpreterTool, + CodeInterpreterToolAuto, + FileSearchTool, + MCPTool, ResponseTextFormatConfigurationJsonSchema, + WebSearchPreviewTool, ) from azure.identity.aio import AzureCliCredential from openai.types.responses.parsed_response import ParsedResponse @@ -34,6 +43,7 @@ from pytest import fixture, param from agent_framework_azure_ai import AzureAIClient, AzureAISettings +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" @@ -962,6 +972,58 @@ def test_get_conversation_id_with_parsed_response_no_conversation() -> None: assert result == "resp_parsed_12345" +def test_from_azure_ai_tools() -> None: + """Test from_azure_ai_tools.""" + # Test MCP tool + mcp_tool = MCPTool(server_label="test_server", server_url="http://localhost:8080") + 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" + 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 = 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 + 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 = 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 + 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 = from_azure_ai_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" + + # region Integration Tests @@ -993,6 +1055,7 @@ async def client() -> AsyncGenerator[AzureAIClient, None]: agent_name=agent_name, ) try: + assert client.function_invocation_configuration client.function_invocation_configuration.max_iterations = 1 yield client finally: 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..ad79c6f5f6 --- /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 or reference 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/_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/packages/core/agent_framework/azure/__init__.py b/python/packages/core/agent_framework/azure/__init__.py index 74d1ec81f4..f8419580c1 100644 --- a/python/packages/core/agent_framework/azure/__init__.py +++ b/python/packages/core/agent_framework/azure/__init__.py @@ -10,6 +10,7 @@ "AzureAIAgentClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), "AzureAIAgentOptions": ("agent_framework_azure_ai", "agent-framework-azure-ai"), "AzureAIClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"), + "AzureAIProjectAgentProvider": ("agent_framework_azure_ai", "agent-framework-azure-ai"), "AzureAISearchContextProvider": ("agent_framework_azure_ai_search", "agent-framework-azure-ai-search"), "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..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 +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", diff --git a/python/samples/getting_started/agents/azure_ai/README.md b/python/samples/getting_started/agents/azure_ai/README.md index 8ed95ad091..1bac0f4cc6 100644 --- a/python/samples/getting_started/agents/azure_ai/README.md +++ b/python/samples/getting_started/agents/azure_ai/README.md @@ -6,8 +6,9 @@ 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_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_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 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. | 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_provider_methods.py b/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py new file mode 100644 index 0000000000..6a5b49bc28 --- /dev/null +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_provider_methods.py @@ -0,0 +1,294 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os +from random import randint +from typing import Annotated + +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 = 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 = await provider.get_agent(name=created_agent.name) + + print(f"Retrieved agent: {agent.name}") + + 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 = await provider.get_agent(reference=reference) + + print(f"Retrieved agent: {agent.name} (version via reference)") + + 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 (sync - no HTTP call) + provider = AzureAIProjectAgentProvider(project_client=project_client) + agent = provider.as_agent(agent_details.versions.latest) + + print(f"Retrieved agent: {agent.name} (from pre-fetched details)") + + 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 = provider.as_agent(agent_version_details) + + print(f"Wrapped agent: {agent.name} (no HTTP call needed)") + print(f"Agent version: {agent_version_details.version}") + + 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") + + await create_agent_example() + await get_agent_by_name_example() + await get_agent_by_reference_example() + await get_agent_by_details_example() + await as_agent_example() + await multiple_agents_example() + + +if __name__ == "__main__": + asyncio.run(main()) 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 b361bd9ac8..d1dce0b220 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: @@ -26,21 +26,23 @@ async def main() -> None: "type": "a2a_preview", "project_connection_id": os.environ["A2A_PROJECT_CONNECTION_ID"], } - + # If the connection is missing a target, we need to set the A2A endpoint URL if os.environ.get("A2A_ENDPOINT"): a2a_tool["base_url"] = os.environ["A2A_ENDPOINT"] - + 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 or gather information from specialized agents.""", tools=a2a_tool, - ) 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 7486b19ec7..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,8 +3,7 @@ import asyncio import os -from agent_framework import ChatAgent -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 PromptAgentDefinition from azure.identity.aio import AzureCliCredential @@ -12,19 +11,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 provider.get_agent() method, +showing agent reuse patterns for production scenarios. """ -async def main() -> None: +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 using SDK directly 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 +35,22 @@ 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: - 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 provider.get_agent() + provider = AzureAIProjectAgentProvider(project_client=project_client) + 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}") + + 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 +58,9 @@ async def main() -> None: ) +async def main() -> None: + await using_provider_get_agent() + + if __name__ == "__main__": asyncio.run(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 63e6155b0d..21166911ac 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 b2865da64b..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 @@ -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. """ @@ -29,35 +29,32 @@ async def main() -> None: - """Example of using response_format property.""" + """Example of using response_format property with a runtime JSON schema.""" - # 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." - print(f"User: {query}") - result = await agent.run( - query, - # Specify type to use as response - options={ - "response_format": { - "type": "json_schema", - "json_schema": { - "name": runtime_schema["title"], - "strict": True, - "schema": runtime_schema, - }, + # Pass response_format at agent creation time using dict schema format + agent = await provider.create_agent( + name="WeatherDigestAgent", + instructions="Return sample weather digest as structured JSON.", + response_format={ + "type": "json_schema", + "json_schema": { + "name": runtime_schema["title"], + "strict": True, + "schema": runtime_schema, }, }, ) + query = "Draft a sample weather digest." + print(f"User: {query}") + result = await agent.run(query) + print(result.text) 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)