mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: feat(foundry): add experimental hosted tool factories on FoundryChatClient (#5958)
* feat(foundry): add experimental hosted tool factories on FoundryChatClient Adds eight new `@experimental` static factory methods on `FoundryChatClient` covering Foundry-hosted tools that previously had no helper: - get_azure_ai_search_tool - get_sharepoint_tool - get_fabric_tool - get_memory_search_tool - get_computer_use_tool - get_browser_automation_tool - get_bing_custom_search_tool - get_a2a_tool All factories are marked with the new `ExperimentalFeature.FOUNDRY_TOOLS` tag and resolve the underlying `azure-ai-projects` preview classes lazily through a `_require_sdk_class` helper so older SDK versions still import cleanly and fail with a clear `ImportError` only on use. Tests cover each factory's return type and field wiring, the experimental metadata, and the missing-SDK-class fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(foundry): address review comments on tool-factory tests * Skip preview-tool tests gracefully (`_skip_if_sdk_class_missing`) when the installed `azure-ai-projects` does not expose the required preview class, matching the lazy-import guard in production code so the test suite stays green on older SDK installs. * Add `filterwarnings("ignore::FutureWarning")` to each new tool-factory test (and the parametrized metadata test) so they remain stable under strict warning configurations \u2014 the global dedup in `_feature_stage._WARNED_FEATURES` makes `pytest.warns` brittle across ordered runs. * Use `monkeypatch.setattr(..., None, raising=False)` instead of `delattr` in the missing-SDK-class test so it works for modules that implement PEP 562 `__getattr__`. * Split the long `get_bing_custom_search_tool` return into two lines for readability. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(foundry): harden tool-factory kwargs against silent override * Reorder the dict-literal kwargs assembly in get_azure_ai_search_tool, get_memory_search_tool, and get_bing_custom_search_tool so explicit parameters always take precedence over **kwargs (matching the safe pattern already used in get_a2a_tool). This prevents a caller passing `project_connection_id`, `index_name`, `memory_store_name`, `scope`, or `instance_name` through `**kwargs` from silently overriding the explicit security-sensitive arguments. * Update the README experimental note to reflect once-per-feature-id dedup semantics of `_feature_stage._WARNED_FEATURES` rather than claiming a per-factory "first use" warning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(foundry): split FOUNDRY_TOOLS / FOUNDRY_PREVIEW_TOOLS, add bing-grounding - Add ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS to distinguish wrappers around preview Foundry SDK tool classes (Sharepoint/Fabric/Memory/ComputerUse/ BrowserAutomation/BingCustomSearch/A2A) from FOUNDRY_TOOLS, which is for GA-SDK wrappers that are simply new in agent-framework-foundry (AzureAISearch, BingGrounding). - Add get_bing_grounding_tool factory and a 'Choosing a web grounding tool' comparison block on get_web_search_tool / get_bing_grounding_tool / get_bing_custom_search_tool docstrings. - Drop the _require_sdk_class lazy resolver: every guarded class is available at azure-ai-projects>=2.1.0 (the package floor), so import them eagerly. Concrete return types replace 'Any'. - README: split the experimental factories into two tables, one per feature flag, with a note explaining the distinction. - Tests: split into FOUNDRY_TOOLS / FOUNDRY_PREVIEW_TOOLS factory cases; drop the obsolete missing-SDK-class ImportError test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
01a3c5be8a
commit
47f5c3397f
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any
|
||||
@@ -984,6 +985,25 @@ def test_get_web_search_tool_with_location() -> None:
|
||||
assert tool_obj is not None
|
||||
|
||||
|
||||
def test_get_web_search_tool_allowed_domains() -> None:
|
||||
"""allowed_domains is wrapped into the SDK filters field."""
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
tool_obj = RawFoundryChatClient.get_web_search_tool(allowed_domains=["example.com"])
|
||||
assert tool_obj.filters is not None
|
||||
assert tool_obj.filters.allowed_domains == ["example.com"]
|
||||
|
||||
|
||||
def test_get_web_search_tool_custom_search_configuration() -> None:
|
||||
"""custom_search_configuration is forwarded to the SDK without warning."""
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
tool_obj = RawFoundryChatClient.get_web_search_tool(
|
||||
custom_search_configuration={"connection_id": "c", "instance_name": "i"},
|
||||
)
|
||||
assert tool_obj.custom_search_configuration == {"connection_id": "c", "instance_name": "i"}
|
||||
|
||||
|
||||
def test_get_image_generation_tool() -> None:
|
||||
"""Test image generation tool creation."""
|
||||
|
||||
@@ -1012,6 +1032,223 @@ def test_get_mcp_tool_with_connection_id() -> None:
|
||||
assert tool_obj is not None
|
||||
|
||||
|
||||
def _skip_if_sdk_class_missing(name: str) -> Any:
|
||||
"""Return the SDK class or skip the test if older azure-ai-projects lacks it."""
|
||||
from azure.ai.projects import models as projects_models
|
||||
|
||||
cls = getattr(projects_models, name, None)
|
||||
if cls is None:
|
||||
pytest.skip(f"azure-ai-projects in this environment does not expose {name!r}.")
|
||||
return cls
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::FutureWarning")
|
||||
def test_get_azure_ai_search_tool() -> None:
|
||||
"""Azure AI Search tool factory builds the nested resource correctly."""
|
||||
azure_ai_search_tool_cls = _skip_if_sdk_class_missing("AzureAISearchTool")
|
||||
|
||||
tool_obj = FoundryChatClient.get_azure_ai_search_tool(
|
||||
index_connection_id="conn-1",
|
||||
index_name="my-index",
|
||||
query_type="vector_semantic_hybrid",
|
||||
top_k=5,
|
||||
filter="category eq 'docs'",
|
||||
)
|
||||
assert isinstance(tool_obj, azure_ai_search_tool_cls)
|
||||
indexes = tool_obj.azure_ai_search.indexes
|
||||
assert len(indexes) == 1
|
||||
index = indexes[0]
|
||||
assert index.project_connection_id == "conn-1"
|
||||
assert index.index_name == "my-index"
|
||||
assert index.query_type == "vector_semantic_hybrid"
|
||||
assert index.top_k == 5
|
||||
assert index.filter == "category eq 'docs'"
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::FutureWarning")
|
||||
def test_get_sharepoint_tool() -> None:
|
||||
"""SharePoint tool factory wires the connection through nested params."""
|
||||
sharepoint_tool_cls = _skip_if_sdk_class_missing("SharepointPreviewTool")
|
||||
|
||||
tool_obj = FoundryChatClient.get_sharepoint_tool(connection_id="sp-conn")
|
||||
assert isinstance(tool_obj, sharepoint_tool_cls)
|
||||
connections = tool_obj.sharepoint_grounding_preview.project_connections
|
||||
assert connections is not None
|
||||
assert len(connections) == 1
|
||||
assert connections[0].project_connection_id == "sp-conn"
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::FutureWarning")
|
||||
def test_get_fabric_tool() -> None:
|
||||
"""Fabric tool factory wires the connection through nested params."""
|
||||
fabric_tool_cls = _skip_if_sdk_class_missing("MicrosoftFabricPreviewTool")
|
||||
|
||||
tool_obj = FoundryChatClient.get_fabric_tool(connection_id="fab-conn")
|
||||
assert isinstance(tool_obj, fabric_tool_cls)
|
||||
connections = tool_obj.fabric_dataagent_preview.project_connections
|
||||
assert connections is not None
|
||||
assert len(connections) == 1
|
||||
assert connections[0].project_connection_id == "fab-conn"
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::FutureWarning")
|
||||
def test_get_memory_search_tool() -> None:
|
||||
"""Memory search tool factory passes core fields through."""
|
||||
memory_tool_cls = _skip_if_sdk_class_missing("MemorySearchPreviewTool")
|
||||
|
||||
tool_obj = FoundryChatClient.get_memory_search_tool(
|
||||
memory_store_name="store-1",
|
||||
scope="{{$userId}}",
|
||||
update_delay=600,
|
||||
)
|
||||
assert isinstance(tool_obj, memory_tool_cls)
|
||||
assert tool_obj.memory_store_name == "store-1"
|
||||
assert tool_obj.scope == "{{$userId}}"
|
||||
assert tool_obj.update_delay == 600
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::FutureWarning")
|
||||
def test_get_computer_use_tool() -> None:
|
||||
"""Computer use tool factory passes environment + display dimensions."""
|
||||
computer_use_cls = _skip_if_sdk_class_missing("ComputerUsePreviewTool")
|
||||
|
||||
tool_obj = FoundryChatClient.get_computer_use_tool(
|
||||
environment="browser",
|
||||
display_width=1920,
|
||||
display_height=1080,
|
||||
)
|
||||
assert isinstance(tool_obj, computer_use_cls)
|
||||
assert tool_obj.environment == "browser"
|
||||
assert tool_obj.display_width == 1920
|
||||
assert tool_obj.display_height == 1080
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::FutureWarning")
|
||||
def test_get_browser_automation_tool() -> None:
|
||||
"""Browser automation tool factory wraps the connection id in the params type."""
|
||||
browser_tool_cls = _skip_if_sdk_class_missing("BrowserAutomationPreviewTool")
|
||||
|
||||
tool_obj = FoundryChatClient.get_browser_automation_tool(connection_id="playwright-conn")
|
||||
assert isinstance(tool_obj, browser_tool_cls)
|
||||
assert tool_obj.browser_automation_preview.connection.project_connection_id == "playwright-conn"
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::FutureWarning")
|
||||
def test_get_bing_custom_search_tool() -> None:
|
||||
"""Bing custom search tool factory builds the nested search configuration."""
|
||||
bing_tool_cls = _skip_if_sdk_class_missing("BingCustomSearchPreviewTool")
|
||||
|
||||
tool_obj = FoundryChatClient.get_bing_custom_search_tool(
|
||||
connection_id="bing-conn",
|
||||
instance_name="my-custom-config",
|
||||
market="en-US",
|
||||
count=10,
|
||||
)
|
||||
assert isinstance(tool_obj, bing_tool_cls)
|
||||
configs = tool_obj.bing_custom_search_preview.search_configurations
|
||||
assert len(configs) == 1
|
||||
config = configs[0]
|
||||
assert config.project_connection_id == "bing-conn"
|
||||
assert config.instance_name == "my-custom-config"
|
||||
assert config.market == "en-US"
|
||||
assert config.count == 10
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::FutureWarning")
|
||||
def test_get_bing_grounding_tool() -> None:
|
||||
"""Bing grounding tool factory builds the nested search configuration."""
|
||||
bing_tool_cls = _skip_if_sdk_class_missing("BingGroundingTool")
|
||||
|
||||
tool_obj = FoundryChatClient.get_bing_grounding_tool(
|
||||
connection_id="bing-conn",
|
||||
market="en-US",
|
||||
set_lang="en",
|
||||
count=10,
|
||||
freshness="Day",
|
||||
)
|
||||
assert isinstance(tool_obj, bing_tool_cls)
|
||||
configs = tool_obj.bing_grounding.search_configurations
|
||||
assert len(configs) == 1
|
||||
config = configs[0]
|
||||
assert config.project_connection_id == "bing-conn"
|
||||
assert config.market == "en-US"
|
||||
assert config.set_lang == "en"
|
||||
assert config.count == 10
|
||||
assert config.freshness == "Day"
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::FutureWarning")
|
||||
def test_get_a2a_tool() -> None:
|
||||
"""A2A tool factory carries base_url, agent_card_path, and project_connection_id."""
|
||||
a2a_tool_cls = _skip_if_sdk_class_missing("A2APreviewTool")
|
||||
|
||||
tool_obj = FoundryChatClient.get_a2a_tool(
|
||||
base_url="https://agent.example.com",
|
||||
agent_card_path="/.well-known/agent-card.json",
|
||||
project_connection_id="a2a-conn",
|
||||
)
|
||||
assert isinstance(tool_obj, a2a_tool_cls)
|
||||
assert tool_obj.base_url == "https://agent.example.com"
|
||||
assert tool_obj.agent_card_path == "/.well-known/agent-card.json"
|
||||
assert tool_obj.project_connection_id == "a2a-conn"
|
||||
|
||||
|
||||
_FOUNDRY_TOOLS_FACTORY_CASES: list[tuple[str, str, dict[str, Any]]] = [
|
||||
("get_azure_ai_search_tool", "AzureAISearchTool", {"index_connection_id": "c", "index_name": "i"}),
|
||||
(
|
||||
"get_bing_grounding_tool",
|
||||
"BingGroundingTool",
|
||||
{"connection_id": "c"},
|
||||
),
|
||||
]
|
||||
|
||||
_FOUNDRY_PREVIEW_TOOLS_FACTORY_CASES: list[tuple[str, str, dict[str, Any]]] = [
|
||||
("get_sharepoint_tool", "SharepointPreviewTool", {"connection_id": "c"}),
|
||||
("get_fabric_tool", "MicrosoftFabricPreviewTool", {"connection_id": "c"}),
|
||||
(
|
||||
"get_memory_search_tool",
|
||||
"MemorySearchPreviewTool",
|
||||
{"memory_store_name": "s", "scope": "u"},
|
||||
),
|
||||
(
|
||||
"get_computer_use_tool",
|
||||
"ComputerUsePreviewTool",
|
||||
{"environment": "browser", "display_width": 1, "display_height": 1},
|
||||
),
|
||||
("get_browser_automation_tool", "BrowserAutomationPreviewTool", {"connection_id": "c"}),
|
||||
(
|
||||
"get_bing_custom_search_tool",
|
||||
"BingCustomSearchPreviewTool",
|
||||
{"connection_id": "c", "instance_name": "i"},
|
||||
),
|
||||
("get_a2a_tool", "A2APreviewTool", {"base_url": "https://a.example.com"}),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::FutureWarning")
|
||||
@pytest.mark.parametrize("factory_name, sdk_class_name, kwargs", _FOUNDRY_TOOLS_FACTORY_CASES)
|
||||
def test_foundry_tools_factories_are_marked(factory_name: str, sdk_class_name: str, kwargs: dict[str, Any]) -> None:
|
||||
"""Factories wrapping GA Foundry tool SDK classes carry FOUNDRY_TOOLS metadata."""
|
||||
_skip_if_sdk_class_missing(sdk_class_name)
|
||||
factory = getattr(FoundryChatClient, factory_name)
|
||||
assert getattr(factory, "__feature_stage__", None) == "experimental"
|
||||
assert getattr(factory, "__feature_id__", None) == "FOUNDRY_TOOLS"
|
||||
assert factory(**kwargs) is not None
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::FutureWarning")
|
||||
@pytest.mark.parametrize("factory_name, sdk_class_name, kwargs", _FOUNDRY_PREVIEW_TOOLS_FACTORY_CASES)
|
||||
def test_foundry_preview_tools_factories_are_marked(
|
||||
factory_name: str, sdk_class_name: str, kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
"""Factories wrapping preview Foundry tool SDK classes carry FOUNDRY_PREVIEW_TOOLS metadata."""
|
||||
_skip_if_sdk_class_missing(sdk_class_name)
|
||||
factory = getattr(FoundryChatClient, factory_name)
|
||||
assert getattr(factory, "__feature_stage__", None) == "experimental"
|
||||
assert getattr(factory, "__feature_id__", None) == "FOUNDRY_PREVIEW_TOOLS"
|
||||
assert factory(**kwargs) is not None
|
||||
|
||||
|
||||
def test_parse_chunk_surfaces_oauth_consent_request() -> None:
|
||||
"""An oauth_consent_request output item surfaces as Content with consent_link."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user