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:
committed by
GitHub
Unverified
parent
01a3c5be8a
commit
47f5c3397f
@@ -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"
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user