Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions python/packages/core/agent_framework/_feature_stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class ExperimentalFeature(str, Enum):
EVALS = "EVALS"
FILE_HISTORY = "FILE_HISTORY"
FIDES = "FIDES"
FOUNDRY_TOOLS = "FOUNDRY_TOOLS"
FUNCTIONAL_WORKFLOWS = "FUNCTIONAL_WORKFLOWS"
HARNESS = "HARNESS"
SKILLS = "SKILLS"
Expand Down
48 changes: 48 additions & 0 deletions python/packages/foundry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,51 @@ 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`.

> **Experimental — `ExperimentalFeature.FOUNDRY_TOOLS`.** The following
> factories wrap preview Foundry tool types and may change or be removed before
> they reach GA. Calls to any of these factories emit an `ExperimentalWarning`
> the first time the `FOUNDRY_TOOLS` feature is exercised in a process; the
> warning is then deduplicated across all eight factories (one warning per
> feature id per process).

| Factory | Foundry SDK tool |
|---------|-----------------|
| `get_azure_ai_search_tool(index_connection_id, index_name, ...)` | `AzureAISearchTool` |
| `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` |

The preview tool classes are resolved from `azure-ai-projects` lazily. If your
installed version doesn't expose one of them, the call raises a clear
`ImportError` pointing at the missing symbol — upgrade `azure-ai-projects` to
fix it.
293 changes: 293 additions & 0 deletions python/packages/foundry/agent_framework_foundry/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
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
Expand Down Expand Up @@ -110,6 +111,26 @@ def resolve_file_ids(file_ids: Sequence[str | Content] | None) -> list[str] | No
FoundryChatOptions = OpenAIChatOptions


def _require_sdk_class(name: str) -> Any:
"""Resolve an ``azure.ai.projects.models`` class lazily.

Preview SDK classes (``*PreviewTool`` and their parameter helpers) are added
on a rolling cadence; importing them at module load time would break the
package on older ``azure-ai-projects`` versions. This helper resolves them
on demand and raises a clear ``ImportError`` when the installed SDK is too
old to provide the requested symbol.
"""
from azure.ai.projects import models as _projects_models

cls = getattr(_projects_models, name, None)
if cls is None:
raise ImportError(
f"{name!r} is not available in the installed azure-ai-projects package. "
"Upgrade azure-ai-projects to a version that exposes this Foundry tool."
)
return cls


class RawFoundryChatClient( # type: ignore[misc]
RawOpenAIChatClient[FoundryChatOptionsT],
Generic[FoundryChatOptionsT],
Expand Down Expand Up @@ -501,6 +522,278 @@ def get_mcp_tool(

# endregion

# region Experimental Foundry tool factories (preview SDK types)

@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_TOOLS)
Comment thread
eavanvalkenburg marked this conversation as resolved.
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,
) -> Any:
"""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.
"""
tool_cls = _require_sdk_class("AzureAISearchTool")
resource_cls = _require_sdk_class("AzureAISearchToolResource")
index_cls = _require_sdk_class("AISearchIndexResource")
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 tool_cls(azure_ai_search=resource_cls(indexes=[index_cls(**index_kwargs)]))

@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_TOOLS)
def get_sharepoint_tool(
*,
connection_id: str,
**kwargs: Any,
) -> Any:
"""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.
"""
tool_cls = _require_sdk_class("SharepointPreviewTool")
params_cls = _require_sdk_class("SharepointGroundingToolParameters")
connection_cls = _require_sdk_class("ToolProjectConnection")
return tool_cls(
sharepoint_grounding_preview=params_cls(
project_connections=[connection_cls(project_connection_id=connection_id)],
**kwargs,
)
)

@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_TOOLS)
def get_fabric_tool(
*,
connection_id: str,
**kwargs: Any,
) -> Any:
"""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.
"""
tool_cls = _require_sdk_class("MicrosoftFabricPreviewTool")
params_cls = _require_sdk_class("FabricDataAgentToolParameters")
connection_cls = _require_sdk_class("ToolProjectConnection")
return tool_cls(
fabric_dataagent_preview=params_cls(
project_connections=[connection_cls(project_connection_id=connection_id)],
**kwargs,
)
)

@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_TOOLS)
def get_memory_search_tool(
*,
memory_store_name: str,
scope: str,
search_options: Any | None = None,
update_delay: int | None = None,
**kwargs: Any,
) -> Any:
"""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.
"""
tool_cls = _require_sdk_class("MemorySearchPreviewTool")
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 tool_cls(**params)

@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_TOOLS)
def get_computer_use_tool(
*,
environment: str,
display_width: int,
display_height: int,
**kwargs: Any,
) -> Any:
"""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.
"""
tool_cls = _require_sdk_class("ComputerUsePreviewTool")
return tool_cls(
environment=environment,
display_width=display_width,
display_height=display_height,
**kwargs,
)

@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_TOOLS)
def get_browser_automation_tool(
*,
connection_id: str,
**kwargs: Any,
) -> Any:
"""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.
"""
tool_cls = _require_sdk_class("BrowserAutomationPreviewTool")
params_cls = _require_sdk_class("BrowserAutomationToolParameters")
connection_cls = _require_sdk_class("BrowserAutomationToolConnectionParameters")
return tool_cls(
browser_automation_preview=params_cls(
connection=connection_cls(project_connection_id=connection_id),
**kwargs,
)
)

@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_TOOLS)
def get_bing_custom_search_tool(
Comment thread
eavanvalkenburg marked this conversation as resolved.
Outdated
*,
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,
) -> Any:
"""Create a Bing Custom Search tool configuration for Foundry.

Keyword Args:
connection_id: The Foundry project connection ID for grounding with Bing search.
instance_name: The custom configuration instance name.
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.
"""
tool_cls = _require_sdk_class("BingCustomSearchPreviewTool")
params_cls = _require_sdk_class("BingCustomSearchToolParameters")
config_cls = _require_sdk_class("BingCustomSearchConfiguration")
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
search_params = params_cls(search_configurations=[config_cls(**config_kwargs)])
return tool_cls(bing_custom_search_preview=search_params)

@staticmethod
@experimental(feature_id=ExperimentalFeature.FOUNDRY_TOOLS)
def get_a2a_tool(
*,
base_url: str | None = None,
agent_card_path: str | None = None,
project_connection_id: str | None = None,
**kwargs: Any,
) -> Any:
"""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.
"""
tool_cls = _require_sdk_class("A2APreviewTool")
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 tool_cls(**params)

# endregion


class FoundryChatClient( # type: ignore[misc]
FunctionInvocationLayer[FoundryChatOptionsT],
Expand Down
Loading
Loading