diff --git a/python/samples/02-agents/providers/foundry/README.md b/python/samples/02-agents/providers/foundry/README.md index f4253d98861..812e7532374 100644 --- a/python/samples/02-agents/providers/foundry/README.md +++ b/python/samples/02-agents/providers/foundry/README.md @@ -27,7 +27,7 @@ This folder contains Microsoft Foundry and Foundry Local samples for Agent Frame | [`foundry_chat_client_with_hosted_mcp.py`](foundry_chat_client_with_hosted_mcp.py) | Foundry Chat Client with hosted MCP | | [`foundry_chat_client_with_local_mcp.py`](foundry_chat_client_with_local_mcp.py) | Foundry Chat Client with local MCP | | [`foundry_chat_client_with_session.py`](foundry_chat_client_with_session.py) | Foundry Chat Client with session management | -| [`foundry_chat_client_with_toolbox.py`](foundry_chat_client_with_toolbox.py) | Foundry Chat Client connected to a toolbox via its MCP endpoint using `MCPStreamableHTTPTool` | +| [`foundry_chat_client_with_toolbox.py`](foundry_chat_client_with_toolbox.py) | Discover and invoke Foundry toolbox tools on demand with `tool_search` and `call_tool` over MCP | | [`foundry_chat_client_with_toolbox_skills.py`](foundry_chat_client_with_toolbox_skills.py) | Foundry Chat Client that discovers MCP-based skills from a Foundry Toolbox endpoint via `MCPSkillsSource` (uses an Azure AD bearer token and the toolbox preview header) | ## FoundryLocalClient Samples diff --git a/python/samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox.py b/python/samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox.py index 7d15cfb49ca..fbad7b3c0ab 100644 --- a/python/samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox.py +++ b/python/samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox.py @@ -24,27 +24,35 @@ Prerequisites: - A Microsoft Foundry project with a toolbox configured - FOUNDRY_PROJECT_ENDPOINT and FOUNDRY_MODEL environment variables set -- FOUNDRY_TOOLBOX_ENDPOINT: the toolbox's MCP endpoint URL, e.g. - ``https://.services.ai.azure.com/api/projects//toolsets//mcp?api-version=v1`` - Azure CLI authentication (``az login``) +- azure-ai-projects >= 2.4.0 for toolbox tool search + +The helper publishes a version with tool search enabled and connects to that exact +version. The model initially sees tool_search and call_tool, then discovers the +underlying MCP tools as needed. Each run creates a version; existing versions are +not deleted. Remove ToolSearchToolboxTool() to compare the full initial tool list. """ -# Must match the ```` segment of FOUNDRY_TOOLBOX_ENDPOINT. TOOLBOX_NAME = "research_toolbox" def create_sample_toolbox(name: str) -> str: - """Create (or replace) a toolbox version in the Foundry project. + """Create a toolbox version with tool search in the Foundry project. Toolboxes are normally configured in the Foundry portal or a deployment script, not the application itself. This helper exists so the sample can - be run end-to-end without first setting a toolbox up by hand — delete any - existing toolbox under ``name``, then create a fresh version containing a - single MCP tool. Returns the created version identifier. + be run end-to-end without first setting a toolbox up by hand. Returns the + created version identifier without deleting existing versions. """ from azure.ai.projects import AIProjectClient - from azure.ai.projects.models import MCPTool, Tool - from azure.core.exceptions import ResourceNotFoundError + + try: + from azure.ai.projects.models import MCPToolboxTool, ToolboxTool, ToolSearchToolboxTool + except ImportError as exc: + raise ImportError( + "This sample requires azure-ai-projects>=2.4.0 for toolbox tool search. " + "Upgrade with: pip install --upgrade 'azure-ai-projects>=2.4.0'" + ) from exc with ( AzureCliCredential() as credential, @@ -54,23 +62,18 @@ def create_sample_toolbox(name: str) -> str: if toolboxes is None: toolboxes = cast(Any, project_client.beta).toolboxes - try: - toolboxes.delete(name) - print(f"Toolbox `{name}` deleted") - except ResourceNotFoundError: - pass - - tools: list[Tool] = [ - MCPTool( + tools: list[ToolboxTool] = [ + MCPToolboxTool( server_label="api_specs", server_url="https://gitmcp.io/Azure/azure-rest-api-specs", require_approval="never", - ) + ), + ToolSearchToolboxTool(), ] created = toolboxes.create_version( name=name, - description="Toolbox version with MCP require_approval set to 'never'.", + description="Toolbox version with MCP tools discovered through tool search.", tools=tools, ) print(f"Created toolbox {created.name}@{created.version} ({len(created.tools)} tool(s))") @@ -92,13 +95,14 @@ def provide(_kwargs: dict[str, Any]) -> dict[str, str]: async def main() -> None: credential = DefaultAzureCredential() - # Comment out if the toolbox already exists in your Foundry project. - create_sample_toolbox(TOOLBOX_NAME) + # Connect to the new version so the tool-search configuration is unambiguous. + version = create_sample_toolbox(TOOLBOX_NAME) + project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"].rstrip("/") toolbox_tool = MCPStreamableHTTPTool( name="foundry_toolbox", description="Tools exposed by the configured Foundry toolbox", - url=os.environ["FOUNDRY_TOOLBOX_ENDPOINT"], + url=f"{project_endpoint}/toolboxes/{TOOLBOX_NAME}/versions/{version}/mcp?api-version=v1", header_provider=make_toolbox_header_provider(credential), load_prompts=False, ) @@ -112,7 +116,7 @@ async def main() -> None: instructions="You are a helpful assistant. Use the available toolbox tools to answer the user.", tools=toolbox_tool, ) as agent: - query = "What tools do you have access to?" + query = "Find the REST API documentation for Azure Container Apps session pools." print(f"User: {query}") result = await agent.run(query) print(f"Assistant: {result}") diff --git a/python/tests/samples/agents/test_foundry_toolbox_sample.py b/python/tests/samples/agents/test_foundry_toolbox_sample.py new file mode 100644 index 00000000000..c22cd7da4f7 --- /dev/null +++ b/python/tests/samples/agents/test_foundry_toolbox_sample.py @@ -0,0 +1,76 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Tests for the azure-ai-projects version guard in the Foundry toolbox sample.""" + +import importlib.util +import sys +from pathlib import Path +from types import ModuleType + +import pytest + +_SAMPLE_PATH = ( + Path(__file__).parents[3] + / "samples" + / "02-agents" + / "providers" + / "foundry" + / "foundry_chat_client_with_toolbox.py" +) + + +def _load_sample_module() -> ModuleType: + spec = importlib.util.spec_from_file_location("foundry_chat_client_with_toolbox", _SAMPLE_PATH) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +sample_module = _load_sample_module() + + +class _StopBeforeNetwork(Exception): + pass + + +def _install_models_surface(monkeypatch: pytest.MonkeyPatch, exported: tuple[str, ...]) -> None: + """Replace azure.ai.projects.models with a module exposing only the given toolbox names.""" + models = ModuleType("azure.ai.projects.models") + for name in exported: + setattr(models, name, type(name, (), {})) + monkeypatch.setitem(sys.modules, "azure.ai.projects.models", models) + + +@pytest.mark.parametrize( + "exported", + [ + pytest.param((), id="azure-ai-projects-2.2"), + pytest.param(("MCPToolboxTool", "ToolboxTool"), id="azure-ai-projects-2.3"), + ], +) +def test_create_sample_toolbox_reports_upgrade_for_older_sdk( + monkeypatch: pytest.MonkeyPatch, exported: tuple[str, ...] +) -> None: + _install_models_surface(monkeypatch, exported) + + def fail_if_called() -> None: + raise AssertionError("The version check must run before any credential is created.") + + monkeypatch.setattr(sample_module, "AzureCliCredential", fail_if_called) + + with pytest.raises(ImportError, match=r"azure-ai-projects>=2\.4\.0"): + sample_module.create_sample_toolbox("toolbox") + + +def test_create_sample_toolbox_accepts_tool_search_sdk(monkeypatch: pytest.MonkeyPatch) -> None: + _install_models_surface(monkeypatch, ("MCPToolboxTool", "ToolboxTool", "ToolSearchToolboxTool")) + + def stop_before_network() -> None: + raise _StopBeforeNetwork + + monkeypatch.setattr(sample_module, "AzureCliCredential", stop_before_network) + + with pytest.raises(_StopBeforeNetwork): + sample_module.create_sample_toolbox("toolbox")