agent-framework-foundry 1.2.1__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agent_framework_foundry-1.2.1 → agent_framework_foundry-1.3.0}/PKG-INFO +4 -29
- {agent_framework_foundry-1.2.1 → agent_framework_foundry-1.3.0}/README.md +1 -26
- {agent_framework_foundry-1.2.1 → agent_framework_foundry-1.3.0}/agent_framework_foundry/__init__.py +0 -5
- {agent_framework_foundry-1.2.1 → agent_framework_foundry-1.3.0}/agent_framework_foundry/_agent.py +18 -6
- {agent_framework_foundry-1.2.1 → agent_framework_foundry-1.3.0}/agent_framework_foundry/_chat_client.py +1 -41
- agent_framework_foundry-1.3.0/agent_framework_foundry/_tools.py +72 -0
- {agent_framework_foundry-1.2.1 → agent_framework_foundry-1.3.0}/pyproject.toml +3 -3
- agent_framework_foundry-1.2.1/agent_framework_foundry/_tools.py +0 -197
- {agent_framework_foundry-1.2.1 → agent_framework_foundry-1.3.0}/LICENSE +0 -0
- {agent_framework_foundry-1.2.1 → agent_framework_foundry-1.3.0}/agent_framework_foundry/_embedding_client.py +0 -0
- {agent_framework_foundry-1.2.1 → agent_framework_foundry-1.3.0}/agent_framework_foundry/_foundry_evals.py +0 -0
- {agent_framework_foundry-1.2.1 → agent_framework_foundry-1.3.0}/agent_framework_foundry/_memory_provider.py +0 -0
- {agent_framework_foundry-1.2.1 → agent_framework_foundry-1.3.0}/agent_framework_foundry/_oauth_helpers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-framework-foundry
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Microsoft Foundry integrations for Microsoft Agent Framework.
|
|
5
5
|
Author-email: Microsoft <af-support@microsoft.com>
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -16,8 +16,8 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
17
|
Classifier: Typing :: Typed
|
|
18
18
|
License-File: LICENSE
|
|
19
|
-
Requires-Dist: agent-framework-core>=1.
|
|
20
|
-
Requires-Dist: agent-framework-openai>=1.
|
|
19
|
+
Requires-Dist: agent-framework-core>=1.3.0,<2
|
|
20
|
+
Requires-Dist: agent-framework-openai>=1.3.0,<2
|
|
21
21
|
Requires-Dist: azure-ai-inference>=1.0.0b9,<1.0.0b10
|
|
22
22
|
Requires-Dist: azure-ai-projects>=2.1.0,<3.0
|
|
23
23
|
Project-URL: homepage, https://aka.ms/agent-framework
|
|
@@ -48,32 +48,7 @@ For hosted `FoundryAgent`, the toolbox must already be attached to the agent in
|
|
|
48
48
|
|
|
49
49
|
### Using toolboxes with `FoundryChatClient`
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
**1. Fetch, optionally filter, and pass the tools directly**
|
|
54
|
-
|
|
55
|
-
Load the toolbox from the Microsoft Foundry project, optionally select a subset of its tools, and hand them to an `Agent` alongside any other tools you own:
|
|
56
|
-
|
|
57
|
-
```python
|
|
58
|
-
from agent_framework import Agent
|
|
59
|
-
from agent_framework.foundry import FoundryChatClient, select_toolbox_tools
|
|
60
|
-
|
|
61
|
-
client = FoundryChatClient(...)
|
|
62
|
-
toolbox = await client.get_toolbox("my-toolbox", version="3")
|
|
63
|
-
|
|
64
|
-
# Pass the whole toolbox:
|
|
65
|
-
agent = Agent(client=client, tools=toolbox)
|
|
66
|
-
|
|
67
|
-
# Or filter to a subset first:
|
|
68
|
-
selected = select_toolbox_tools(toolbox, include_types=["code_interpreter", "mcp"])
|
|
69
|
-
agent = Agent(client=client, tools=selected)
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
See [`foundry_chat_client_with_toolbox.py`](../../samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox.py) for a full example, including combining multiple toolboxes.
|
|
73
|
-
|
|
74
|
-
**2. Connect to the toolbox's MCP endpoint with `MCPStreamableHTTPTool`**
|
|
75
|
-
|
|
76
|
-
Each toolbox is reachable as an MCP server. Instead of fetching and fanning out its individual tool definitions, you can point a MAF `MCPStreamableHTTPTool` at the toolbox's MCP endpoint — the agent then discovers and calls its tools over MCP at runtime:
|
|
51
|
+
Each toolbox is reachable as an MCP server. Connect to the toolbox's MCP endpoint with `MCPStreamableHTTPTool` — the agent then discovers and calls its tools over MCP at runtime:
|
|
77
52
|
|
|
78
53
|
```python
|
|
79
54
|
from agent_framework import Agent, MCPStreamableHTTPTool
|
|
@@ -21,32 +21,7 @@ For hosted `FoundryAgent`, the toolbox must already be attached to the agent in
|
|
|
21
21
|
|
|
22
22
|
### Using toolboxes with `FoundryChatClient`
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
**1. Fetch, optionally filter, and pass the tools directly**
|
|
27
|
-
|
|
28
|
-
Load the toolbox from the Microsoft Foundry project, optionally select a subset of its tools, and hand them to an `Agent` alongside any other tools you own:
|
|
29
|
-
|
|
30
|
-
```python
|
|
31
|
-
from agent_framework import Agent
|
|
32
|
-
from agent_framework.foundry import FoundryChatClient, select_toolbox_tools
|
|
33
|
-
|
|
34
|
-
client = FoundryChatClient(...)
|
|
35
|
-
toolbox = await client.get_toolbox("my-toolbox", version="3")
|
|
36
|
-
|
|
37
|
-
# Pass the whole toolbox:
|
|
38
|
-
agent = Agent(client=client, tools=toolbox)
|
|
39
|
-
|
|
40
|
-
# Or filter to a subset first:
|
|
41
|
-
selected = select_toolbox_tools(toolbox, include_types=["code_interpreter", "mcp"])
|
|
42
|
-
agent = Agent(client=client, tools=selected)
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
See [`foundry_chat_client_with_toolbox.py`](../../samples/02-agents/providers/foundry/foundry_chat_client_with_toolbox.py) for a full example, including combining multiple toolboxes.
|
|
46
|
-
|
|
47
|
-
**2. Connect to the toolbox's MCP endpoint with `MCPStreamableHTTPTool`**
|
|
48
|
-
|
|
49
|
-
Each toolbox is reachable as an MCP server. Instead of fetching and fanning out its individual tool definitions, you can point a MAF `MCPStreamableHTTPTool` at the toolbox's MCP endpoint — the agent then discovers and calls its tools over MCP at runtime:
|
|
24
|
+
Each toolbox is reachable as an MCP server. Connect to the toolbox's MCP endpoint with `MCPStreamableHTTPTool` — the agent then discovers and calls its tools over MCP at runtime:
|
|
50
25
|
|
|
51
26
|
```python
|
|
52
27
|
from agent_framework import Agent, MCPStreamableHTTPTool
|
{agent_framework_foundry-1.2.1 → agent_framework_foundry-1.3.0}/agent_framework_foundry/__init__.py
RENAMED
|
@@ -16,7 +16,6 @@ from ._foundry_evals import (
|
|
|
16
16
|
evaluate_traces,
|
|
17
17
|
)
|
|
18
18
|
from ._memory_provider import FoundryMemoryProvider
|
|
19
|
-
from ._tools import FoundryHostedToolType, get_toolbox_tool_name, get_toolbox_tool_type, select_toolbox_tools
|
|
20
19
|
|
|
21
20
|
try:
|
|
22
21
|
__version__ = importlib.metadata.version(__name__)
|
|
@@ -32,7 +31,6 @@ __all__ = [
|
|
|
32
31
|
"FoundryEmbeddingOptions",
|
|
33
32
|
"FoundryEmbeddingSettings",
|
|
34
33
|
"FoundryEvals",
|
|
35
|
-
"FoundryHostedToolType",
|
|
36
34
|
"FoundryMemoryProvider",
|
|
37
35
|
"RawFoundryAgent",
|
|
38
36
|
"RawFoundryAgentChatClient",
|
|
@@ -41,7 +39,4 @@ __all__ = [
|
|
|
41
39
|
"__version__",
|
|
42
40
|
"evaluate_foundry_target",
|
|
43
41
|
"evaluate_traces",
|
|
44
|
-
"get_toolbox_tool_name",
|
|
45
|
-
"get_toolbox_tool_type",
|
|
46
|
-
"select_toolbox_tools",
|
|
47
42
|
]
|
{agent_framework_foundry-1.2.1 → agent_framework_foundry-1.3.0}/agent_framework_foundry/_agent.py
RENAMED
|
@@ -135,6 +135,18 @@ def _uses_foundry_agent_session(conversation_id: Any) -> bool:
|
|
|
135
135
|
)
|
|
136
136
|
|
|
137
137
|
|
|
138
|
+
def _build_agent_reference(agent_name: str, agent_version: str | None) -> dict[str, str]:
|
|
139
|
+
"""Build the Responses API ``agent_reference`` payload for non-preview Foundry agent calls.
|
|
140
|
+
|
|
141
|
+
Used for both Prompt Agents and HostedAgents on the ``allow_preview=False`` code path —
|
|
142
|
+
the preview branch instead injects identity via ``project_client.get_openai_client(agent_name=...)``.
|
|
143
|
+
"""
|
|
144
|
+
ref: dict[str, str] = {"name": agent_name, "type": "agent_reference"}
|
|
145
|
+
if agent_version:
|
|
146
|
+
ref["version"] = agent_version
|
|
147
|
+
return ref
|
|
148
|
+
|
|
149
|
+
|
|
138
150
|
class RawFoundryAgentChatClient( # type: ignore[misc]
|
|
139
151
|
RawOpenAIChatClient[FoundryAgentOptionsT],
|
|
140
152
|
Generic[FoundryAgentOptionsT],
|
|
@@ -342,6 +354,12 @@ class RawFoundryAgentChatClient( # type: ignore[misc]
|
|
|
342
354
|
run_options.pop("previous_response_id", None)
|
|
343
355
|
run_options.pop("conversation", None)
|
|
344
356
|
extra_body["agent_session_id"] = conversation_id
|
|
357
|
+
# Non-preview Prompt/Hosted Agent calls need agent_reference in the request body to
|
|
358
|
+
# tell the Responses API which Foundry agent (and version) is in use, since ``model``
|
|
359
|
+
# is stripped below. The preview path injects the reference via the OpenAI client kwarg
|
|
360
|
+
# ``agent_name`` instead, so skip there. See issue #5582.
|
|
361
|
+
if not self.allow_preview:
|
|
362
|
+
extra_body.setdefault("agent_reference", _build_agent_reference(self.agent_name, self.agent_version))
|
|
345
363
|
if extra_body:
|
|
346
364
|
run_options["extra_body"] = extra_body
|
|
347
365
|
|
|
@@ -400,12 +418,6 @@ class RawFoundryAgentChatClient( # type: ignore[misc]
|
|
|
400
418
|
self,
|
|
401
419
|
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
|
|
402
420
|
) -> list[Any]:
|
|
403
|
-
"""Prepare tools for Foundry agent Responses API calls.
|
|
404
|
-
|
|
405
|
-
Mirrors ``RawFoundryChatClient`` sanitization so toolbox-fetched MCP
|
|
406
|
-
tools with extra read-model fields continue to work through the agent
|
|
407
|
-
surface.
|
|
408
|
-
"""
|
|
409
421
|
response_tools = super()._prepare_tools_for_openai(tools)
|
|
410
422
|
return [_sanitize_foundry_response_tool(tool_item) for tool_item in response_tools]
|
|
411
423
|
|
|
@@ -16,7 +16,6 @@ from agent_framework import (
|
|
|
16
16
|
load_settings,
|
|
17
17
|
)
|
|
18
18
|
from agent_framework._compaction import CompactionStrategy, TokenizerProtocol
|
|
19
|
-
from agent_framework._feature_stage import ExperimentalFeature, experimental
|
|
20
19
|
from agent_framework._telemetry import get_user_agent
|
|
21
20
|
from agent_framework.observability import ChatTelemetryLayer
|
|
22
21
|
from agent_framework_openai._chat_client import OpenAIChatOptions, RawOpenAIChatClient
|
|
@@ -36,7 +35,7 @@ from azure.core.credentials_async import AsyncTokenCredential
|
|
|
36
35
|
|
|
37
36
|
from agent_framework_foundry._oauth_helpers import try_parse_oauth_consent_event
|
|
38
37
|
|
|
39
|
-
from ._tools import _sanitize_foundry_response_tool
|
|
38
|
+
from ._tools import _sanitize_foundry_response_tool # pyright: ignore[reportPrivateUsage]
|
|
40
39
|
|
|
41
40
|
if sys.version_info >= (3, 13):
|
|
42
41
|
from typing import TypeVar # type: ignore # pragma: no cover
|
|
@@ -53,7 +52,6 @@ else:
|
|
|
53
52
|
|
|
54
53
|
if TYPE_CHECKING:
|
|
55
54
|
from agent_framework import ChatAndFunctionMiddlewareTypes, ToolTypes
|
|
56
|
-
from azure.ai.projects.models import ToolboxVersionObject
|
|
57
55
|
|
|
58
56
|
logger: logging.Logger = logging.getLogger("agent_framework.foundry")
|
|
59
57
|
|
|
@@ -234,13 +232,6 @@ class RawFoundryChatClient( # type: ignore[misc]
|
|
|
234
232
|
self,
|
|
235
233
|
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
|
|
236
234
|
) -> list[Any]:
|
|
237
|
-
"""Prepare tools for Foundry Responses API calls.
|
|
238
|
-
|
|
239
|
-
Foundry toolbox reads can surface MCP tool objects with extra fields
|
|
240
|
-
(for example ``name``) that are accepted by the toolbox API but rejected
|
|
241
|
-
by the Responses API. Sanitize those hosted-tool payloads before sending
|
|
242
|
-
them downstream.
|
|
243
|
-
"""
|
|
244
235
|
response_tools = super()._prepare_tools_for_openai(tools)
|
|
245
236
|
return [_sanitize_foundry_response_tool(tool_item) for tool_item in response_tools]
|
|
246
237
|
|
|
@@ -510,37 +501,6 @@ class RawFoundryChatClient( # type: ignore[misc]
|
|
|
510
501
|
|
|
511
502
|
# endregion
|
|
512
503
|
|
|
513
|
-
# region Toolbox methods (instance methods — these hit the network)
|
|
514
|
-
|
|
515
|
-
@experimental(feature_id=ExperimentalFeature.TOOLBOXES)
|
|
516
|
-
async def get_toolbox(
|
|
517
|
-
self,
|
|
518
|
-
name: str,
|
|
519
|
-
*,
|
|
520
|
-
version: str | None = None,
|
|
521
|
-
) -> ToolboxVersionObject:
|
|
522
|
-
"""Fetch a Foundry toolbox by name.
|
|
523
|
-
|
|
524
|
-
If ``version`` is omitted, resolves the toolbox's current default version
|
|
525
|
-
(two requests). If ``version`` is specified, fetches that version directly
|
|
526
|
-
(single request).
|
|
527
|
-
|
|
528
|
-
Args:
|
|
529
|
-
name: The name of the toolbox.
|
|
530
|
-
|
|
531
|
-
Keyword Args:
|
|
532
|
-
version: Optional immutable version identifier to pin to.
|
|
533
|
-
|
|
534
|
-
Returns:
|
|
535
|
-
A ``ToolboxVersionObject``. Pass its ``tools`` attribute to
|
|
536
|
-
``Agent(tools=toolbox.tools)``.
|
|
537
|
-
|
|
538
|
-
Raises:
|
|
539
|
-
azure.core.exceptions.ResourceNotFoundError: If the toolbox or
|
|
540
|
-
the requested version does not exist.
|
|
541
|
-
"""
|
|
542
|
-
return await fetch_toolbox(self.project_client, name, version)
|
|
543
|
-
|
|
544
504
|
|
|
545
505
|
class FoundryChatClient( # type: ignore[misc]
|
|
546
506
|
FunctionInvocationLayer[FoundryChatOptionsT],
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Shared tool helpers for Foundry chat clients.
|
|
4
|
+
|
|
5
|
+
Includes Responses-API payload sanitization for Foundry hosted tools.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from typing import Any, cast
|
|
12
|
+
|
|
13
|
+
from azure.ai.projects.models import MCPTool as FoundryMCPTool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _validate_hosted_tool_payload(sanitized: Mapping[str, Any]) -> None:
|
|
17
|
+
"""Fail fast on hosted tool payloads that would always be rejected by the Responses API.
|
|
18
|
+
|
|
19
|
+
These mismatches are not injectable defaults — the caller must supply the
|
|
20
|
+
missing information — so surfacing a clear error here points at the tool
|
|
21
|
+
definition instead of letting the API return a generic 400.
|
|
22
|
+
"""
|
|
23
|
+
tool_type = sanitized.get("type")
|
|
24
|
+
if tool_type == "file_search" and not sanitized.get("vector_store_ids"):
|
|
25
|
+
raise ValueError(
|
|
26
|
+
"'file_search' tool is missing required 'vector_store_ids'. "
|
|
27
|
+
"Update the tool definition to include at least one vector store ID."
|
|
28
|
+
)
|
|
29
|
+
if tool_type == "mcp" and not sanitized.get("server_url") and not sanitized.get("project_connection_id"):
|
|
30
|
+
raise ValueError(
|
|
31
|
+
"'mcp' tool is missing both 'server_url' and 'project_connection_id'. "
|
|
32
|
+
"Update the tool definition to include one of these."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _sanitize_foundry_response_tool(tool_item: Any) -> Any: # pyright: ignore[reportUnusedFunction]
|
|
37
|
+
"""Return a Responses-API-safe tool payload for Foundry hosted tools.
|
|
38
|
+
|
|
39
|
+
Reconciles known mismatches between hosted tool definitions and the Responses API:
|
|
40
|
+
|
|
41
|
+
1. Hosted tool objects may carry read-model fields such as top-level ``name``
|
|
42
|
+
and ``description``. The Responses API rejects at least ``name`` with
|
|
43
|
+
``Unknown parameter: 'tools[0].name'``. These fields are stripped from
|
|
44
|
+
non-function hosted tool payloads.
|
|
45
|
+
2. ``code_interpreter`` tools without a ``container`` field (the Azure SDK
|
|
46
|
+
treats it as optional) are rejected by the Responses API with
|
|
47
|
+
``Missing required parameter: 'tools[N].container'``. A default
|
|
48
|
+
``{"type": "auto"}`` container is injected when absent.
|
|
49
|
+
3. Hosted tools that are structurally incomplete in ways that cannot be
|
|
50
|
+
defaulted (``file_search`` without ``vector_store_ids``, ``mcp`` without
|
|
51
|
+
either ``server_url`` or ``project_connection_id``) raise ``ValueError``
|
|
52
|
+
with a message that points at the tool definition.
|
|
53
|
+
"""
|
|
54
|
+
if isinstance(tool_item, FoundryMCPTool):
|
|
55
|
+
sanitized: dict[str, Any] = dict(cast("Mapping[str, Any]", tool_item))
|
|
56
|
+
sanitized.pop("name", None)
|
|
57
|
+
sanitized.pop("description", None)
|
|
58
|
+
_validate_hosted_tool_payload(sanitized)
|
|
59
|
+
return sanitized
|
|
60
|
+
|
|
61
|
+
if isinstance(tool_item, Mapping):
|
|
62
|
+
mapping = cast("Mapping[str, Any]", tool_item)
|
|
63
|
+
if "type" in mapping and mapping.get("type") not in {"function", "custom"}:
|
|
64
|
+
sanitized = dict(mapping)
|
|
65
|
+
sanitized.pop("name", None)
|
|
66
|
+
sanitized.pop("description", None)
|
|
67
|
+
if sanitized.get("type") == "code_interpreter" and "container" not in sanitized:
|
|
68
|
+
sanitized["container"] = {"type": "auto"}
|
|
69
|
+
_validate_hosted_tool_payload(sanitized)
|
|
70
|
+
return sanitized
|
|
71
|
+
|
|
72
|
+
return cast(Any, tool_item)
|
|
@@ -4,7 +4,7 @@ description = "Microsoft Foundry integrations for Microsoft Agent Framework."
|
|
|
4
4
|
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.3.0"
|
|
8
8
|
license-files = ["LICENSE"]
|
|
9
9
|
urls.homepage = "https://aka.ms/agent-framework"
|
|
10
10
|
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
|
@@ -23,8 +23,8 @@ classifiers = [
|
|
|
23
23
|
"Typing :: Typed",
|
|
24
24
|
]
|
|
25
25
|
dependencies = [
|
|
26
|
-
"agent-framework-core>=1.
|
|
27
|
-
"agent-framework-openai>=1.
|
|
26
|
+
"agent-framework-core>=1.3.0,<2",
|
|
27
|
+
"agent-framework-openai>=1.3.0,<2",
|
|
28
28
|
"azure-ai-inference>=1.0.0b9,<1.0.0b10",
|
|
29
29
|
"azure-ai-projects>=2.1.0,<3.0",
|
|
30
30
|
]
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
-
|
|
3
|
-
"""Shared tool helpers for Foundry chat clients.
|
|
4
|
-
|
|
5
|
-
Includes:
|
|
6
|
-
|
|
7
|
-
* *Toolbox* helpers — a *toolbox* is a named, versioned bundle of tool
|
|
8
|
-
definitions stored in an Azure AI Foundry project.
|
|
9
|
-
* Responses-API payload sanitization for Foundry hosted tools.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
from collections.abc import Callable, Collection, Mapping, Sequence
|
|
15
|
-
from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast
|
|
16
|
-
|
|
17
|
-
from agent_framework._feature_stage import ExperimentalFeature, experimental
|
|
18
|
-
from azure.ai.projects.models import MCPTool as FoundryMCPTool
|
|
19
|
-
|
|
20
|
-
if TYPE_CHECKING:
|
|
21
|
-
from azure.ai.projects.aio import AIProjectClient
|
|
22
|
-
from azure.ai.projects.models import Tool, ToolboxVersionObject
|
|
23
|
-
|
|
24
|
-
FoundryHostedToolType: TypeAlias = (
|
|
25
|
-
Literal[
|
|
26
|
-
"code_interpreter",
|
|
27
|
-
"file_search",
|
|
28
|
-
"image_generation",
|
|
29
|
-
"mcp",
|
|
30
|
-
"web_search",
|
|
31
|
-
]
|
|
32
|
-
| str
|
|
33
|
-
)
|
|
34
|
-
ToolboxToolSelectionInput: TypeAlias = "ToolboxVersionObject | Sequence[Tool | dict[str, Any]]"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@experimental(feature_id=ExperimentalFeature.TOOLBOXES)
|
|
38
|
-
async def fetch_toolbox(
|
|
39
|
-
project_client: AIProjectClient,
|
|
40
|
-
name: str,
|
|
41
|
-
version: str | None = None,
|
|
42
|
-
) -> ToolboxVersionObject:
|
|
43
|
-
"""Fetch a toolbox version via an ``AIProjectClient``.
|
|
44
|
-
|
|
45
|
-
If ``version`` is omitted, resolves the toolbox's current default
|
|
46
|
-
version (two requests: one to ``.get(name)`` for the default version
|
|
47
|
-
pointer, one to ``.get_version(name, version)`` for the tools). If
|
|
48
|
-
``version`` is specified, fetches that version directly (single request).
|
|
49
|
-
"""
|
|
50
|
-
if version is None:
|
|
51
|
-
handle = await project_client.beta.toolboxes.get(name)
|
|
52
|
-
version = handle.default_version
|
|
53
|
-
return await project_client.beta.toolboxes.get_version(name, version)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
@experimental(feature_id=ExperimentalFeature.TOOLBOXES)
|
|
57
|
-
def get_toolbox_tool_name(tool: Tool | dict[str, Any]) -> str | None:
|
|
58
|
-
"""Return the best-effort display/selection name for a toolbox tool.
|
|
59
|
-
|
|
60
|
-
Selection precedence:
|
|
61
|
-
1. MCP ``server_label``
|
|
62
|
-
2. Generic tool ``name``
|
|
63
|
-
3. Tool ``type``
|
|
64
|
-
"""
|
|
65
|
-
if isinstance(tool, dict):
|
|
66
|
-
if server_label := tool.get("server_label"):
|
|
67
|
-
return str(server_label)
|
|
68
|
-
if name := tool.get("name"):
|
|
69
|
-
return str(name)
|
|
70
|
-
if tool_type := tool.get("type"):
|
|
71
|
-
return str(tool_type)
|
|
72
|
-
return None
|
|
73
|
-
|
|
74
|
-
if server_label := getattr(tool, "server_label", None):
|
|
75
|
-
return str(server_label)
|
|
76
|
-
if name := getattr(tool, "name", None):
|
|
77
|
-
return str(name)
|
|
78
|
-
if tool_type := getattr(tool, "type", None):
|
|
79
|
-
return str(tool_type)
|
|
80
|
-
return None
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
@experimental(feature_id=ExperimentalFeature.TOOLBOXES)
|
|
84
|
-
def get_toolbox_tool_type(tool: Tool | dict[str, Any]) -> str | None:
|
|
85
|
-
"""Return the raw tool ``type`` if present."""
|
|
86
|
-
tool_type = tool.get("type") if isinstance(tool, dict) else getattr(tool, "type", None)
|
|
87
|
-
return str(tool_type) if tool_type is not None else None
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@experimental(feature_id=ExperimentalFeature.TOOLBOXES)
|
|
91
|
-
def select_toolbox_tools(
|
|
92
|
-
tools: ToolboxToolSelectionInput,
|
|
93
|
-
*,
|
|
94
|
-
include_names: Collection[str] | None = None,
|
|
95
|
-
exclude_names: Collection[str] | None = None,
|
|
96
|
-
include_types: Collection[FoundryHostedToolType] | None = None,
|
|
97
|
-
exclude_types: Collection[FoundryHostedToolType] | None = None,
|
|
98
|
-
predicate: Callable[[Tool | dict[str, Any]], bool] | None = None,
|
|
99
|
-
) -> list[Tool | dict[str, Any]]:
|
|
100
|
-
"""Filter toolbox tools by normalized name, raw type, and/or predicate.
|
|
101
|
-
|
|
102
|
-
Normalized name precedence:
|
|
103
|
-
1. ``server_label`` for MCP tools
|
|
104
|
-
2. ``name``
|
|
105
|
-
3. ``type``
|
|
106
|
-
"""
|
|
107
|
-
tool_items: Sequence[Tool | dict[str, Any]] = (
|
|
108
|
-
tools if isinstance(tools, Sequence) else cast("Sequence[Tool | dict[str, Any]]", tools.tools)
|
|
109
|
-
)
|
|
110
|
-
include_name_set = {str(item) for item in include_names} if include_names is not None else None
|
|
111
|
-
exclude_name_set = {str(item) for item in exclude_names} if exclude_names is not None else None
|
|
112
|
-
include_type_set = {str(item) for item in include_types} if include_types is not None else None
|
|
113
|
-
exclude_type_set = {str(item) for item in exclude_types} if exclude_types is not None else None
|
|
114
|
-
|
|
115
|
-
selected: list[Tool | dict[str, Any]] = []
|
|
116
|
-
for tool in tool_items:
|
|
117
|
-
tool_name = get_toolbox_tool_name(tool)
|
|
118
|
-
tool_type = get_toolbox_tool_type(tool)
|
|
119
|
-
|
|
120
|
-
if include_name_set is not None and tool_name not in include_name_set:
|
|
121
|
-
continue
|
|
122
|
-
if exclude_name_set is not None and tool_name in exclude_name_set:
|
|
123
|
-
continue
|
|
124
|
-
if include_type_set is not None and tool_type not in include_type_set:
|
|
125
|
-
continue
|
|
126
|
-
if exclude_type_set is not None and tool_type in exclude_type_set:
|
|
127
|
-
continue
|
|
128
|
-
if predicate is not None and not predicate(tool):
|
|
129
|
-
continue
|
|
130
|
-
|
|
131
|
-
selected.append(tool)
|
|
132
|
-
|
|
133
|
-
return selected
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _validate_hosted_tool_payload(sanitized: Mapping[str, Any]) -> None:
|
|
137
|
-
"""Fail fast on hosted tool payloads that would always be rejected by the Responses API.
|
|
138
|
-
|
|
139
|
-
These mismatches are not injectable defaults — the caller must supply the
|
|
140
|
-
missing information — so surfacing a clear error here points at the toolbox
|
|
141
|
-
definition instead of letting the API return a generic 400.
|
|
142
|
-
"""
|
|
143
|
-
tool_type = sanitized.get("type")
|
|
144
|
-
if tool_type == "file_search" and not sanitized.get("vector_store_ids"):
|
|
145
|
-
raise ValueError(
|
|
146
|
-
"'file_search' tool is missing required 'vector_store_ids'. "
|
|
147
|
-
"If this came from a Foundry toolbox, update the toolbox definition "
|
|
148
|
-
"to include at least one vector store ID."
|
|
149
|
-
)
|
|
150
|
-
if tool_type == "mcp" and not sanitized.get("server_url") and not sanitized.get("project_connection_id"):
|
|
151
|
-
raise ValueError(
|
|
152
|
-
"'mcp' tool is missing both 'server_url' and 'project_connection_id'. "
|
|
153
|
-
"If this came from a Foundry toolbox, update the toolbox definition "
|
|
154
|
-
"to include one of these."
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def _sanitize_foundry_response_tool(tool_item: Any) -> Any: # pyright: ignore[reportUnusedFunction]
|
|
159
|
-
"""Return a Responses-API-safe tool payload for Foundry hosted tools.
|
|
160
|
-
|
|
161
|
-
Reconciles known mismatches between toolbox reads and the Responses API:
|
|
162
|
-
|
|
163
|
-
1. Toolbox reads can return hosted tool objects decorated with read-model
|
|
164
|
-
fields such as top-level ``name`` and ``description``. The Responses API
|
|
165
|
-
rejects at least ``name`` with ``Unknown parameter: 'tools[0].name'``.
|
|
166
|
-
These fields are stripped from non-function hosted tool payloads.
|
|
167
|
-
2. ``code_interpreter`` tools stored in a toolbox without a ``container``
|
|
168
|
-
field (the Azure SDK treats it as optional) are rejected by the Responses
|
|
169
|
-
API with ``Missing required parameter: 'tools[N].container'``. A default
|
|
170
|
-
``{"type": "auto"}`` container is injected when absent.
|
|
171
|
-
3. Hosted tools that are structurally incomplete in ways that cannot be
|
|
172
|
-
defaulted (``file_search`` without ``vector_store_ids``, ``mcp`` without
|
|
173
|
-
either ``server_url`` or ``project_connection_id``) raise ``ValueError``
|
|
174
|
-
with a message that points at the toolbox definition.
|
|
175
|
-
|
|
176
|
-
These are workarounds until the toolbox/Responses proxy normalizes payloads
|
|
177
|
-
server-side.
|
|
178
|
-
"""
|
|
179
|
-
if isinstance(tool_item, FoundryMCPTool):
|
|
180
|
-
sanitized: dict[str, Any] = dict(cast("Mapping[str, Any]", tool_item))
|
|
181
|
-
sanitized.pop("name", None)
|
|
182
|
-
sanitized.pop("description", None)
|
|
183
|
-
_validate_hosted_tool_payload(sanitized)
|
|
184
|
-
return sanitized
|
|
185
|
-
|
|
186
|
-
if isinstance(tool_item, Mapping):
|
|
187
|
-
mapping = cast("Mapping[str, Any]", tool_item)
|
|
188
|
-
if "type" in mapping and mapping.get("type") not in {"function", "custom"}:
|
|
189
|
-
sanitized = dict(mapping)
|
|
190
|
-
sanitized.pop("name", None)
|
|
191
|
-
sanitized.pop("description", None)
|
|
192
|
-
if sanitized.get("type") == "code_interpreter" and "container" not in sanitized:
|
|
193
|
-
sanitized["container"] = {"type": "auto"}
|
|
194
|
-
_validate_hosted_tool_payload(sanitized)
|
|
195
|
-
return sanitized
|
|
196
|
-
|
|
197
|
-
return cast(Any, tool_item)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|