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 c34d67e5b5..0713a39bec 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -199,7 +199,7 @@ async def _get_agent_reference_or_create( # Try to use latest version if requested and agent exists if self.use_latest_version: try: - existing_agent = await self.project_client.agents.retrieve(agent_name) + existing_agent = await self.project_client.agents.get(agent_name) self.agent_name = existing_agent.name self.agent_version = existing_agent.versions.latest.version return {"name": self.agent_name, "version": self.agent_version, "type": "agent_reference"} @@ -314,7 +314,6 @@ def get_mcp_tool(self, tool: HostedMCPTool) -> MutableMapping[str, Any]: if tool.allowed_tools: mcp["allowed_tools"] = list(tool.allowed_tools) - # TODO (dmytrostruk): Check "always" approval mode if tool.approval_mode: match tool.approval_mode: case str(): diff --git a/python/packages/azure-ai/pyproject.toml b/python/packages/azure-ai/pyproject.toml index 143f13fda2..156c0e06c1 100644 --- a/python/packages/azure-ai/pyproject.toml +++ b/python/packages/azure-ai/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] dependencies = [ "agent-framework-core", - "azure-ai-projects >= 2.0.0a20251105001", + "azure-ai-projects >= 2.0.0a20251110001", "azure-ai-agents == 1.2.0b5", "aiohttp", ] 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 ec69cdb96b..a6a0ba9085 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_client.py @@ -375,13 +375,13 @@ async def test_azure_ai_client_use_latest_version_existing_agent( mock_existing_agent = MagicMock() mock_existing_agent.name = "existing-agent" mock_existing_agent.versions.latest.version = "2.5" - mock_project_client.agents.retrieve = AsyncMock(return_value=mock_existing_agent) + mock_project_client.agents.get = AsyncMock(return_value=mock_existing_agent) run_options = {"model": "test-model"} agent_ref = await client._get_agent_reference_or_create(run_options, None) # type: ignore # Verify existing agent was retrieved and used - mock_project_client.agents.retrieve.assert_called_once_with("existing-agent") + mock_project_client.agents.get.assert_called_once_with("existing-agent") mock_project_client.agents.create_version.assert_not_called() assert agent_ref == {"name": "existing-agent", "version": "2.5", "type": "agent_reference"} @@ -398,7 +398,7 @@ async def test_azure_ai_client_use_latest_version_agent_not_found( client = create_test_azure_ai_client(mock_project_client, agent_name="non-existing-agent", use_latest_version=True) # Mock ResourceNotFoundError when trying to retrieve agent - mock_project_client.agents.retrieve = AsyncMock(side_effect=ResourceNotFoundError("Agent not found")) + mock_project_client.agents.get = AsyncMock(side_effect=ResourceNotFoundError("Agent not found")) # Mock agent creation response for fallback mock_created_agent = MagicMock() @@ -410,7 +410,7 @@ async def test_azure_ai_client_use_latest_version_agent_not_found( agent_ref = await client._get_agent_reference_or_create(run_options, None) # type: ignore # Verify retrieval was attempted and creation was used as fallback - mock_project_client.agents.retrieve.assert_called_once_with("non-existing-agent") + mock_project_client.agents.get.assert_called_once_with("non-existing-agent") mock_project_client.agents.create_version.assert_called_once() assert agent_ref == {"name": "non-existing-agent", "version": "1.0", "type": "agent_reference"} @@ -434,7 +434,7 @@ async def test_azure_ai_client_use_latest_version_false( agent_ref = await client._get_agent_reference_or_create(run_options, None) # type: ignore # Verify retrieval was not attempted and creation was used directly - mock_project_client.agents.retrieve.assert_not_called() + mock_project_client.agents.get.assert_not_called() mock_project_client.agents.create_version.assert_called_once() assert agent_ref == {"name": "test-agent", "version": "1.0", "type": "agent_reference"} @@ -451,7 +451,7 @@ async def test_azure_ai_client_use_latest_version_with_existing_agent_version( agent_ref = await client._get_agent_reference_or_create({}, None) # type: ignore # Verify neither retrieval nor creation was attempted since version is already set - mock_project_client.agents.retrieve.assert_not_called() + mock_project_client.agents.get.assert_not_called() mock_project_client.agents.create_version.assert_not_called() assert agent_ref == {"name": "test-agent", "version": "3.0", "type": "agent_reference"} diff --git a/python/samples/getting_started/agents/azure_ai/README.md b/python/samples/getting_started/agents/azure_ai/README.md index 0a8b01d682..0334ca4342 100644 --- a/python/samples/getting_started/agents/azure_ai/README.md +++ b/python/samples/getting_started/agents/azure_ai/README.md @@ -10,8 +10,8 @@ This folder contains examples demonstrating different ways to create and use age | [`azure_ai_use_latest_version.py`](azure_ai_use_latest_version.py) | Demonstrates how to reuse the latest version of an existing agent instead of creating a new agent version on each instantiation using the `use_latest_version=True` parameter. | | [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use the `HostedCodeInterpreterTool` with Azure AI agents to write and execute Python code for mathematical problem solving and data analysis. | | [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with a pre-existing agent by providing the agent name and version to the Azure AI client. Demonstrates agent reuse patterns for production scenarios. | -| [`azure_ai_with_existing_conversation.py`](azure_ai_with_existing_conversation.py) | Shows how to work with a pre-existing conversation by providing the conversation ID to continue existing chat sessions. | | [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured `AzureAIClient` settings, including project endpoint, model deployment, and credentials rather than relying on environment variable defaults. | +| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Shows how to use the `HostedFileSearchTool` with Azure AI agents to upload files, create vector stores, and enable agents to search through uploaded documents to answer user questions. | | [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate hosted Model Context Protocol (MCP) tools with Azure AI Agent. | | [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Shows how to use structured outputs (response format) with Azure AI agents using Pydantic models to enforce specific response schemas. | | [`azure_ai_with_thread.py`](azure_ai_with_thread.py) | Demonstrates thread management with Azure AI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. | 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 697a5c3894..a2ea4aafe3 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 @@ -35,11 +35,20 @@ async def main() -> None: isinstance(result.raw_representation, ChatResponse) and isinstance(result.raw_representation.raw_representation, OpenAIResponse) and len(result.raw_representation.raw_representation.output) > 0 - and isinstance(result.raw_representation.raw_representation.output[0], ResponseCodeInterpreterToolCall) ): - generated_code = result.raw_representation.raw_representation.output[0].code - - print(f"Generated code:\n{generated_code}") + # Find the first ResponseCodeInterpreterToolCall item + code_interpreter_item = next( + ( + item + for item in result.raw_representation.raw_representation.output + if isinstance(item, ResponseCodeInterpreterToolCall) + ), + None, + ) + + if code_interpreter_item is not None: + generated_code = code_interpreter_item.code + print(f"Generated code:\n{generated_code}") if __name__ == "__main__": 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 new file mode 100644 index 0000000000..28c47de5ca --- /dev/null +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_file_search.py @@ -0,0 +1,76 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import os +from pathlib import Path + +from agent_framework import ChatAgent, HostedFileSearchTool, HostedVectorStoreContent +from agent_framework.azure import AzureAIClient +from azure.ai.agents.aio import AgentsClient +from azure.ai.agents.models import FileInfo, VectorStore +from azure.identity.aio import AzureCliCredential + +""" +The following sample demonstrates how to create a simple, Azure AI agent that +uses a file search tool to answer user questions. +""" + + +# Simulate a conversation with the agent +USER_INPUTS = [ + "Who is the youngest employee?", + "Who works in sales?", + "I have a customer request, who can help me?", +] + + +async def main() -> None: + """Main function demonstrating Azure AI agent with file search capabilities.""" + file: FileInfo | None = None + vector_store: VectorStore | None = None + + async with ( + AzureCliCredential() as credential, + AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client, + AzureAIClient(async_credential=credential) as client, + ): + try: + # 1. Upload file and create vector store + pdf_file_path = Path(__file__).parent.parent / "resources" / "employees.pdf" + print(f"Uploading file from: {pdf_file_path}") + + file = await agents_client.files.upload_and_poll(file_path=str(pdf_file_path), purpose="assistants") + print(f"Uploaded file, file ID: {file.id}") + + vector_store = await agents_client.vector_stores.create_and_poll(file_ids=[file.id], name="my_vectorstore") + print(f"Created vector store, vector store ID: {vector_store.id}") + + # 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, + 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}") + finally: + # 5. Cleanup: Delete the vector store and file in case of earlier failure to prevent orphaned resources. + if vector_store: + await agents_client.vector_stores.delete(vector_store.id) + if file: + await agents_client.files.delete(file.id) + + +if __name__ == "__main__": + asyncio.run(main()) 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 ab4a1d53ef..9bd94b1f03 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 @@ -1,8 +1,9 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio +from typing import Any -from agent_framework import HostedMCPTool +from agent_framework import AgentProtocol, AgentThread, ChatMessage, HostedMCPTool from agent_framework.azure import AzureAIClient from azure.identity.aio import AzureCliCredential @@ -13,33 +14,103 @@ """ -async def run_hosted_mcp() -> None: +async def handle_approvals_without_thread(query: str, agent: "AgentProtocol"): + """When we don't have a thread, we need to ensure we return with the input, approval request and approval.""" + + result = await agent.run(query, store=False) + while len(result.user_input_requests) > 0: + new_inputs: list[Any] = [query] + for user_input_needed in result.user_input_requests: + print( + f"User Input Request for function from {agent.name}: {user_input_needed.function_call.name}" + f" with arguments: {user_input_needed.function_call.arguments}" + ) + new_inputs.append(ChatMessage(role="assistant", contents=[user_input_needed])) + user_approval = input("Approve function call? (y/n): ") + new_inputs.append( + ChatMessage(role="user", contents=[user_input_needed.create_response(user_approval.lower() == "y")]) + ) + + result = await agent.run(new_inputs, store=False) + return result + + +async def handle_approvals_with_thread(query: str, agent: "AgentProtocol", thread: "AgentThread"): + """Here we let the thread deal with the previous responses, and we just rerun with the approval.""" + + result = await agent.run(query, thread=thread) + while len(result.user_input_requests) > 0: + new_input: list[Any] = [] + for user_input_needed in result.user_input_requests: + print( + f"User Input Request for function from {agent.name}: {user_input_needed.function_call.name}" + f" with arguments: {user_input_needed.function_call.arguments}" + ) + user_approval = input("Approve function call? (y/n): ") + new_input.append( + ChatMessage( + role="user", + contents=[user_input_needed.create_response(user_approval.lower() == "y")], + ) + ) + result = await agent.run(new_input, thread=thread) + return result + + +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(async_credential=credential).create_agent( - name="MyDocsAgent", + name="MyLearnDocsAgent", instructions="You are a helpful assistant that can help with Microsoft documentation questions.", tools=HostedMCPTool( name="Microsoft Learn MCP", url="https://learn.microsoft.com/api/mcp", - # "always_require" mode is not supported yet approval_mode="never_require", ), ) as agent, ): query = "How to create an Azure storage account using az cli?" print(f"User: {query}") - result = await agent.run(query) + result = await handle_approvals_without_thread(query, agent) + print(f"{agent.name}: {result}\n") + + +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(async_credential=credential).create_agent( + name="MyApiSpecsAgent", + instructions="You are a helpful agent that can use MCP tools to assist users.", + tools=HostedMCPTool( + name="api-specs", + 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}") + result = await handle_approvals_with_thread(query, agent, thread) print(f"{agent.name}: {result}\n") async def main() -> None: - print("=== Azure AI Agent with Hosted Mcp Tools Example ===\n") + print("=== Azure AI Agent with Hosted MCP Tools Example ===\n") - await run_hosted_mcp() + await run_hosted_mcp_without_approval() + await run_hosted_mcp_with_approval_and_thread() if __name__ == "__main__": diff --git a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_file_search.py b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_file_search.py index a1615d5cde..32ddd8e85c 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_file_search.py +++ b/python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_file_search.py @@ -71,7 +71,7 @@ async def main() -> None: # Ignore cleanup errors to avoid masking issues pass finally: - # 6. Cleanup: Delete the vector store and file in case of eariler failure to prevent orphaned resources. + # 6. Cleanup: Delete the vector store and file in case of earlier failure to prevent orphaned resources. # Refreshing the client is required since chat agent closes it client = AzureAIAgentClient(async_credential=AzureCliCredential()) diff --git a/python/uv.lock b/python/uv.lock index 0cf031d1c9..c389835576 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -252,7 +252,7 @@ requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, { name = "aiohttp" }, { name = "azure-ai-agents", specifier = "==1.2.0b5" }, - { name = "azure-ai-projects", specifier = ">=2.0.0a20251105001", index = "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple" }, + { name = "azure-ai-projects", specifier = ">=2.0.0a20251110001", index = "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple" }, ] [[package]] @@ -883,7 +883,7 @@ wheels = [ [[package]] name = "azure-ai-projects" -version = "2.0.0a20251105001" +version = "2.0.0a20251110001" source = { registry = "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple" } dependencies = [ { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -891,9 +891,9 @@ dependencies = [ { name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://pkgs.dev.azure.com/azure-sdk/29ec6040-b234-4e31-b139-33dc4287b756/_packaging/3572dbf9-b5ef-433b-9137-fc4d7768e7cc/pypi/download/azure-ai-projects/2a20251105001/azure_ai_projects-2.0.0a20251105001.tar.gz", hash = "sha256:045740a4f9154b2aae5ae76a074a243ea29b26086e00948539b6725249535430" } +sdist = { url = "https://pkgs.dev.azure.com/azure-sdk/29ec6040-b234-4e31-b139-33dc4287b756/_packaging/3572dbf9-b5ef-433b-9137-fc4d7768e7cc/pypi/download/azure-ai-projects/2a20251110001/azure_ai_projects-2.0.0a20251110001.tar.gz", hash = "sha256:d241d41507517ec9f438830f116e67539d7e762a9b9f9cff40f0245f1ff13f5b" } wheels = [ - { url = "https://pkgs.dev.azure.com/azure-sdk/29ec6040-b234-4e31-b139-33dc4287b756/_packaging/3572dbf9-b5ef-433b-9137-fc4d7768e7cc/pypi/download/azure-ai-projects/2a20251105001/azure_ai_projects-2.0.0a20251105001-py3-none-any.whl", hash = "sha256:4fd25f169137533723ea41d40d08a226f10df63e091829a8d4a30e46f968574b" }, + { url = "https://pkgs.dev.azure.com/azure-sdk/29ec6040-b234-4e31-b139-33dc4287b756/_packaging/3572dbf9-b5ef-433b-9137-fc4d7768e7cc/pypi/download/azure-ai-projects/2a20251110001/azure_ai_projects-2.0.0a20251110001-py3-none-any.whl", hash = "sha256:1d5e16affceb7beecfffeb2e77044b5358e8b6d3a8dfbe0a3caa34e0eaafd5a9" }, ] [[package]]