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:
Eduard van Valkenburg
2026-05-21 10:39:08 +02:00
committed by GitHub
Unverified
parent 01a3c5be8a
commit 47f5c3397f
4 changed files with 699 additions and 10 deletions
@@ -49,6 +49,8 @@ class ExperimentalFeature(str, Enum):
EVALS = "EVALS"
FILE_HISTORY = "FILE_HISTORY"
FIDES = "FIDES"
FOUNDRY_TOOLS = "FOUNDRY_TOOLS"
FOUNDRY_PREVIEW_TOOLS = "FOUNDRY_PREVIEW_TOOLS"
FUNCTIONAL_WORKFLOWS = "FUNCTIONAL_WORKFLOWS"
HARNESS = "HARNESS"
SKILLS = "SKILLS"
+67
View File
@@ -39,3 +39,70 @@ async with Agent(
result = await agent.run("What tools are available?")
print(result.text)
```
## Hosted tool factories
`FoundryChatClient` exposes static factory methods that return Foundry SDK tool
configurations ready to pass to an `Agent`'s `tools=[...]` argument. These
factories don't require a `FoundryChatClient` instance — you can call them
statically and reuse the same tool configuration across agents.
```python
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
agent = Agent(
client=FoundryChatClient(...),
instructions="...",
tools=[
FoundryChatClient.get_web_search_tool(),
FoundryChatClient.get_code_interpreter_tool(),
],
)
```
Generally available factories: `get_code_interpreter_tool`,
`get_file_search_tool`, `get_web_search_tool`,
`get_image_generation_tool`, `get_mcp_tool`.
> **Choosing a web grounding tool.** `get_web_search_tool` is the recommended
> default — it requires no separate Bing resource and works with Azure OpenAI
> models out of the box. Reach for `get_bing_grounding_tool` (experimental,
> see below) when you need finer Bing parameters (`count`, `freshness`,
> `market`, `set_lang`), are grounding non-OpenAI Foundry models, or are
> migrating from Grounding with Bing Search on the classic platform — it
> requires a Grounding with Bing Search Azure resource that you manage.
> `get_bing_custom_search_tool` (also experimental) is for grounding
> restricted to a curated list of domains via a Bing Custom Search instance.
> See the
> [web grounding overview](https://learn.microsoft.com/azure/foundry/agents/how-to/tools/web-overview)
> for the full comparison.
> **Experimental — `ExperimentalFeature.FOUNDRY_TOOLS`.** The following
> factories wrap GA Foundry tool SDK classes but are new wrappers in
> `agent-framework-foundry` and may change before the wrappers themselves
> reach GA. Calls emit an `ExperimentalWarning` the first time the
> `FOUNDRY_TOOLS` feature is exercised in a process (then deduplicated).
| Factory | Foundry SDK tool |
|---------|-----------------|
| `get_azure_ai_search_tool(index_connection_id, index_name, ...)` | `AzureAISearchTool` |
| `get_bing_grounding_tool(connection_id, ...)` | `BingGroundingTool` |
> **Experimental — `ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS`.** The
> following factories wrap **preview** Foundry tool SDK types — the underlying
> Foundry capability itself is in preview and may change or be removed before
> reaching GA. Calls emit a separate `ExperimentalWarning` the first time the
> `FOUNDRY_PREVIEW_TOOLS` feature is exercised in a process (then
> deduplicated). Use `FOUNDRY_TOOLS` for "wrapper is new" and
> `FOUNDRY_PREVIEW_TOOLS` for "underlying Foundry feature is preview".
| Factory | Foundry SDK tool |
|---------|-----------------|
| `get_sharepoint_tool(connection_id)` | `SharepointPreviewTool` |
| `get_fabric_tool(connection_id)` | `MicrosoftFabricPreviewTool` |
| `get_memory_search_tool(memory_store_name, scope, ...)` | `MemorySearchPreviewTool` |
| `get_computer_use_tool(environment, display_width, display_height)` | `ComputerUsePreviewTool` |
| `get_browser_automation_tool(connection_id)` | `BrowserAutomationPreviewTool` |
| `get_bing_custom_search_tool(connection_id, instance_name, ...)` | `BingCustomSearchPreviewTool` |
| `get_a2a_tool(base_url=..., project_connection_id=..., ...)` | `A2APreviewTool` |
@@ -16,14 +16,35 @@ from agent_framework import (
load_settings,
)
from agent_framework._compaction import CompactionStrategy, TokenizerProtocol
from agent_framework._feature_stage import ExperimentalFeature, experimental
from agent_framework._telemetry import get_user_agent
from agent_framework.observability import ChatTelemetryLayer
from agent_framework_openai._chat_client import OpenAIChatOptions, RawOpenAIChatClient
from azure.ai.projects.aio import AIProjectClient
from azure.ai.projects.models import (
A2APreviewTool,
AISearchIndexResource,
AutoCodeInterpreterToolParam,
AzureAISearchTool,
AzureAISearchToolResource,
BingCustomSearchConfiguration,
BingCustomSearchPreviewTool,
BingCustomSearchToolParameters,
BingGroundingSearchConfiguration,
BingGroundingSearchToolParameters,
BingGroundingTool,
BrowserAutomationPreviewTool,
BrowserAutomationToolConnectionParameters,
BrowserAutomationToolParameters,
CodeInterpreterTool,
ComputerUsePreviewTool,
FabricDataAgentToolParameters,
ImageGenTool,
MemorySearchPreviewTool,
MicrosoftFabricPreviewTool,
SharepointGroundingToolParameters,
SharepointPreviewTool,
ToolProjectConnection,
WebSearchApproximateLocation,
WebSearchTool,
WebSearchToolFilters,
@@ -381,17 +402,44 @@ class RawFoundryChatClient( # type: ignore[misc]
custom_search_configuration: dict[str, Any] | None = None,
**kwargs: Any,
) -> WebSearchTool:
"""Create a web search tool configuration for Microsoft Foundry.
"""Create a Web Search tool configuration for Microsoft Foundry.
**Choosing a web grounding tool.** Foundry exposes three options that all reach
the public web via Bing. Pick the one that matches your scenario:
* :py:meth:`get_web_search_tool` (this one, GA) — recommended starting point.
The Bing resource is managed by Microsoft, no extra Azure setup is required,
and only Azure OpenAI models are supported. Parameters are limited to
``user_location`` and ``search_context_size``.
* :py:meth:`get_bing_grounding_tool` (preview) — use when you need finer Bing parameters (``count``,
``freshness``, ``market``, ``set_lang``), want to ground non-OpenAI
Foundry models, or are migrating from Grounding with Bing Search on the
classic agents platform. You manage the Grounding with Bing Search
resource yourself (Contributor/Owner to create the resource, Foundry
Project Manager to wire the connection).
* :py:meth:`get_bing_custom_search_tool` (preview) — use when you need to
restrict grounding to a curated set of domains defined in a Bing Custom
Search instance.
For all three, search data flows outside the Azure compliance boundary. See
https://learn.microsoft.com/azure/foundry/agents/how-to/tools/web-overview for
the full comparison.
Keyword Args:
user_location: Location context with keys like "city", "country", "region", "timezone".
search_context_size: Amount of context from search results ("low", "medium", "high").
allowed_domains: List of domains to restrict search results to.
custom_search_configuration: Custom Bing search configuration.
**kwargs: Additional arguments passed to the SDK WebSearchTool constructor.
user_location: Location context with keys like ``"city"``, ``"country"``,
``"region"``, ``"timezone"``.
search_context_size: Amount of context from search results
(``"low"``, ``"medium"``, ``"high"``).
allowed_domains: List of domains to restrict search results to. Wrapped
into ``WebSearchToolFilters`` and passed as the ``filters`` field on
the SDK ``WebSearchTool``.
custom_search_configuration: Custom Bing search configuration for
domain-restricted scenarios.
**kwargs: Additional arguments passed to the SDK ``WebSearchTool``
constructor.
Returns:
A WebSearchTool ready to pass to an Agent.
A ``WebSearchTool`` ready to pass to an Agent.
"""
ws_kwargs: dict[str, Any] = {**kwargs}
if search_context_size:
@@ -400,15 +448,137 @@ class RawFoundryChatClient( # type: ignore[misc]
ws_kwargs["filters"] = WebSearchToolFilters(allowed_domains=allowed_domains)
if custom_search_configuration:
ws_kwargs["custom_search_configuration"] = custom_search_configuration
ws_tool = WebSearchTool(**ws_kwargs)
if user_location:
ws_tool.user_location = WebSearchApproximateLocation(
ws_kwargs["user_location"] = WebSearchApproximateLocation(
city=user_location.get("city"),
country=user_location.get("country"),
region=user_location.get("region"),
timezone=user_location.get("timezone"),
)
return ws_tool
return WebSearchTool(**ws_kwargs)
@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_TOOLS)
def get_bing_grounding_tool(
*,
connection_id: str,
market: str | None = None,
set_lang: str | None = None,
count: int | None = None,
freshness: str | None = None,
**kwargs: Any,
) -> BingGroundingTool:
"""Create a Grounding with Bing Search tool configuration for Foundry.
Use this factory when :py:meth:`get_web_search_tool` is too restrictive — for
example when you need ``count``/``freshness``/``market``/``set_lang``
parameters, want to ground a non-OpenAI Foundry model, or are migrating an
agent that already uses Grounding with Bing Search on the classic agents
platform. You manage the Grounding with Bing Search Azure resource yourself
(Contributor or Owner to create the resource, Foundry Project Manager to
create the project connection). Search data flows outside the Azure
compliance boundary.
For domain-restricted grounding to a curated allow-list, use
:py:meth:`get_bing_custom_search_tool` instead. For a zero-setup default that
works for most agents, see :py:meth:`get_web_search_tool`. The full
comparison lives at
https://learn.microsoft.com/azure/foundry/agents/how-to/tools/web-overview.
Keyword Args:
connection_id: The Foundry project connection ID for the Grounding with
Bing Search resource.
market: Optional Bing market identifier (e.g. ``"en-US"``).
set_lang: Optional UI language code passed to the Bing API.
count: Optional number of search results to return.
freshness: Optional time-range filter for search results. See
https://learn.microsoft.com/bing/search-apis/bing-web-search/reference/query-parameters
for accepted values.
**kwargs: Additional arguments forwarded to the SDK
``BingGroundingSearchConfiguration``.
Returns:
A ``BingGroundingTool`` ready to pass to an Agent.
"""
config_kwargs: dict[str, Any] = {
**kwargs,
"project_connection_id": connection_id,
}
if market is not None:
config_kwargs["market"] = market
if set_lang is not None:
config_kwargs["set_lang"] = set_lang
if count is not None:
config_kwargs["count"] = count
if freshness is not None:
config_kwargs["freshness"] = freshness
return BingGroundingTool(
bing_grounding=BingGroundingSearchToolParameters(
search_configurations=[BingGroundingSearchConfiguration(**config_kwargs)],
),
)
@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS)
def get_bing_custom_search_tool(
*,
connection_id: str,
instance_name: str,
market: str | None = None,
set_lang: str | None = None,
count: int | None = None,
freshness: str | None = None,
**kwargs: Any,
) -> BingCustomSearchPreviewTool:
"""Create a Grounding with Bing Custom Search tool configuration for Foundry.
Use this factory (preview) when you need to restrict grounding to a curated
list of domains. The allow/block list is defined ahead of time on a Bing
Custom Search resource (in the Bing portal) and referenced here by
``instance_name``. Like the other Bing-backed tools, search data flows
outside the Azure compliance boundary, and you must create the Bing Custom
Search resource yourself.
For unrestricted public-web grounding with no extra Azure setup, prefer
:py:meth:`get_web_search_tool`. For unrestricted grounding with finer Bing
parameters or non-OpenAI models, prefer :py:meth:`get_bing_grounding_tool`.
See
https://learn.microsoft.com/azure/foundry/agents/how-to/tools/web-overview
for the full comparison.
Keyword Args:
connection_id: The Foundry project connection ID for the Grounding with
Bing Custom Search resource.
instance_name: The custom configuration instance name defined on the
Bing Custom Search resource.
market: Optional Bing market identifier (e.g. ``"en-US"``).
set_lang: Optional UI language code passed to the Bing API.
count: Optional number of search results to return.
freshness: Optional time-range filter for search results.
**kwargs: Additional arguments forwarded to the SDK
``BingCustomSearchConfiguration``.
Returns:
A ``BingCustomSearchPreviewTool`` ready to pass to an Agent.
"""
config_kwargs: dict[str, Any] = {
**kwargs,
"project_connection_id": connection_id,
"instance_name": instance_name,
}
if market is not None:
config_kwargs["market"] = market
if set_lang is not None:
config_kwargs["set_lang"] = set_lang
if count is not None:
config_kwargs["count"] = count
if freshness is not None:
config_kwargs["freshness"] = freshness
return BingCustomSearchPreviewTool(
bing_custom_search_preview=BingCustomSearchToolParameters(
search_configurations=[BingCustomSearchConfiguration(**config_kwargs)],
),
)
@staticmethod
def get_image_generation_tool( # type: ignore[override]
@@ -513,6 +683,219 @@ class RawFoundryChatClient( # type: ignore[misc]
# endregion
# region Experimental Foundry tool factories (preview SDK types)
@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_TOOLS)
def get_azure_ai_search_tool(
*,
index_connection_id: str,
index_name: str,
query_type: str | None = None,
top_k: int | None = None,
filter: str | None = None,
index_asset_id: str | None = None,
**kwargs: Any,
) -> AzureAISearchTool:
"""Create an Azure AI Search tool configuration for Foundry.
Keyword Args:
index_connection_id: The Foundry project connection ID for the Azure AI Search index.
index_name: The name of the index to search.
query_type: Optional query type (``"simple"``, ``"semantic"``, ``"vector"``,
``"vector_simple_hybrid"``, or ``"vector_semantic_hybrid"``).
top_k: Optional number of documents to retrieve.
filter: Optional OData filter expression.
index_asset_id: Optional index asset id for the search resource.
**kwargs: Additional arguments forwarded to the SDK ``AISearchIndexResource``.
Returns:
An ``AzureAISearchTool`` ready to pass to an Agent.
"""
index_kwargs: dict[str, Any] = {
**kwargs,
"project_connection_id": index_connection_id,
"index_name": index_name,
}
if query_type is not None:
index_kwargs["query_type"] = query_type
if top_k is not None:
index_kwargs["top_k"] = top_k
if filter is not None:
index_kwargs["filter"] = filter
if index_asset_id is not None:
index_kwargs["index_asset_id"] = index_asset_id
return AzureAISearchTool(
azure_ai_search=AzureAISearchToolResource(indexes=[AISearchIndexResource(**index_kwargs)]),
)
@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS)
def get_sharepoint_tool(
*,
connection_id: str,
**kwargs: Any,
) -> SharepointPreviewTool:
"""Create a SharePoint grounding tool configuration for Foundry.
Keyword Args:
connection_id: The Foundry project connection ID for the SharePoint resource.
**kwargs: Additional arguments forwarded to the SDK
``SharepointGroundingToolParameters``.
Returns:
A ``SharepointPreviewTool`` ready to pass to an Agent.
"""
return SharepointPreviewTool(
sharepoint_grounding_preview=SharepointGroundingToolParameters(
project_connections=[ToolProjectConnection(project_connection_id=connection_id)],
**kwargs,
)
)
@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS)
def get_fabric_tool(
*,
connection_id: str,
**kwargs: Any,
) -> MicrosoftFabricPreviewTool:
"""Create a Microsoft Fabric data agent tool configuration for Foundry.
Keyword Args:
connection_id: The Foundry project connection ID for the Fabric data agent.
**kwargs: Additional arguments forwarded to the SDK
``FabricDataAgentToolParameters``.
Returns:
A ``MicrosoftFabricPreviewTool`` ready to pass to an Agent.
"""
return MicrosoftFabricPreviewTool(
fabric_dataagent_preview=FabricDataAgentToolParameters(
project_connections=[ToolProjectConnection(project_connection_id=connection_id)],
**kwargs,
)
)
@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS)
def get_memory_search_tool(
*,
memory_store_name: str,
scope: str,
search_options: Any | None = None,
update_delay: int | None = None,
**kwargs: Any,
) -> MemorySearchPreviewTool:
"""Create a Memory Search tool configuration for Foundry.
Keyword Args:
memory_store_name: The name of the memory store to use.
scope: The namespace used to group and isolate memories (e.g. a user ID).
Use ``"{{$userId}}"`` to scope memories to the current signed-in user.
search_options: Optional ``MemorySearchOptions`` instance.
update_delay: Optional seconds to wait before updating memories after inactivity.
**kwargs: Additional arguments forwarded to the SDK ``MemorySearchPreviewTool``.
Returns:
A ``MemorySearchPreviewTool`` ready to pass to an Agent.
"""
params: dict[str, Any] = {
**kwargs,
"memory_store_name": memory_store_name,
"scope": scope,
}
if search_options is not None:
params["search_options"] = search_options
if update_delay is not None:
params["update_delay"] = update_delay
return MemorySearchPreviewTool(**params)
@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS)
def get_computer_use_tool(
*,
environment: str,
display_width: int,
display_height: int,
**kwargs: Any,
) -> ComputerUsePreviewTool:
"""Create a Computer Use tool configuration for Foundry.
Keyword Args:
environment: The computer environment to control. One of ``"windows"``,
``"mac"``, ``"linux"``, ``"ubuntu"``, or ``"browser"``.
display_width: The width of the computer display.
display_height: The height of the computer display.
**kwargs: Additional arguments forwarded to the SDK ``ComputerUsePreviewTool``.
Returns:
A ``ComputerUsePreviewTool`` ready to pass to an Agent.
"""
return ComputerUsePreviewTool(
environment=environment,
display_width=display_width,
display_height=display_height,
**kwargs,
)
@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS)
def get_browser_automation_tool(
*,
connection_id: str,
**kwargs: Any,
) -> BrowserAutomationPreviewTool:
"""Create a Browser Automation tool configuration for Foundry.
Keyword Args:
connection_id: The Foundry project connection ID for the Azure Playwright resource.
**kwargs: Additional arguments forwarded to the SDK
``BrowserAutomationToolParameters``.
Returns:
A ``BrowserAutomationPreviewTool`` ready to pass to an Agent.
"""
return BrowserAutomationPreviewTool(
browser_automation_preview=BrowserAutomationToolParameters(
connection=BrowserAutomationToolConnectionParameters(project_connection_id=connection_id),
**kwargs,
)
)
@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_PREVIEW_TOOLS)
def get_a2a_tool(
*,
base_url: str | None = None,
agent_card_path: str | None = None,
project_connection_id: str | None = None,
**kwargs: Any,
) -> A2APreviewTool:
"""Create an Agent-to-Agent (A2A) tool configuration for Foundry.
Keyword Args:
base_url: Base URL of the remote A2A agent.
agent_card_path: Path to the agent card relative to ``base_url``.
Defaults to ``"/.well-known/agent-card.json"`` server-side.
project_connection_id: Foundry connection ID for the A2A server. Stores
authentication and other connection details.
**kwargs: Additional arguments forwarded to the SDK ``A2APreviewTool``.
Returns:
An ``A2APreviewTool`` ready to pass to an Agent.
"""
params: dict[str, Any] = dict(kwargs)
if base_url is not None:
params["base_url"] = base_url
if agent_card_path is not None:
params["agent_card_path"] = agent_card_path
if project_connection_id is not None:
params["project_connection_id"] = project_connection_id
return A2APreviewTool(**params)
# endregion
class FoundryChatClient( # type: ignore[misc]
FunctionInvocationLayer[FoundryChatOptionsT],
@@ -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."""