mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
e56e6dad4d
* Remove Foundry toolbox helpers; standardize on MCP for toolbox consumption - Remove RawFoundryChatClient.get_toolbox() and its fetch_toolbox import - Remove fetch_toolbox, select_toolbox_tools, get_toolbox_tool_name, get_toolbox_tool_type, FoundryHostedToolType, ToolboxToolSelectionInput from agent_framework_foundry._tools - Remove ExperimentalFeature.TOOLBOXES from _feature_stage.py (no consumers) - Drop toolbox re-exports from agent_framework_foundry/__init__.py and agent_framework.foundry namespace - Update _sanitize_foundry_response_tool docstring to remove toolbox framing; sanitization logic itself is unchanged - Update _agent.py docstring: 'toolbox-fetched MCP' → 'hosted MCP' - Delete tests/test_toolbox.py (all tests covered removed helpers) - Update test_foundry_chat_client.py: rename/redoc tests that mentioned toolbox but test sanitization that remains - Delete foundry_chat_client_with_toolbox.py (bespoke toolbox API sample) - Delete foundry_toolbox_context_provider.py (relied on select_toolbox_tools) - Rename foundry_chat_client_with_toolbox_mcp.py → foundry_chat_client_with_toolbox.py (canonical MCP pattern) - Rewrite 04_foundry_toolbox/main.py to use MCPStreamableHTTPTool - Update provider/README, context_providers/README, 04_foundry_toolbox/README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(samples): update 06_files sample to consume toolbox via MCP (#5670) Replace removed get_toolbox/select_toolbox_tools APIs with MCPStreamableHTTPTool, using allowed_tools=["code_interpreter"] to select only the code interpreter from the toolbox endpoint. Update .env.example and README to use FOUNDRY_TOOLBOX_ENDPOINT instead of TOOLBOX_NAME. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(foundry): remove non-existent toolbox helper APIs from README (#5670) Remove the 'fetch, optionally filter, and pass tools directly' pattern from the FoundryChatClient toolbox documentation, as select_toolbox_tools and get_toolbox were removed. Only the MCP endpoint pattern is documented. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(foundry): remove residual toolbox docstring references and reproduction report Remove REPRODUCTION_REPORT.md (workflow artifact that should not be committed), and update two remaining docstring references that still said 'toolbox reads' /'toolbox definition' after the toolbox helpers were removed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Python: Remove bespoke Foundry toolbox helpers; standardize on MCP for toolbox consumption Fixes #5670 * fix(#5670): resolve toolbox endpoint from TOOLBOX_NAME fallback; add namespace regression tests - Add _resolve_toolbox_endpoint() helper in 04_foundry_toolbox/main.py and 06_files/main.py that prefers FOUNDRY_TOOLBOX_ENDPOINT but falls back to deriving the MCP URL from FOUNDRY_PROJECT_ENDPOINT + TOOLBOX_NAME — fixing the startup KeyError when agents are deployed via azd provision (which injects TOOLBOX_NAME, not FOUNDRY_TOOLBOX_ENDPOINT). - Update 04_foundry_toolbox/.env.example to use FOUNDRY_TOOLBOX_ENDPOINT (consistent with 06_files). - Add TOOLBOX_NAME env var to 06_files/agent.yaml so deployed agents have it available for the fallback derivation. - Update both READMEs to document the two ways to supply the toolbox endpoint. - Add test_foundry_namespace_no_longer_exposes_toolbox_helpers() with negative assertions for FoundryHostedToolType, get_toolbox_tool_name, get_toolbox_tool_type, and select_toolbox_tools — guarding against accidental re-introduction of removed symbols. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(samples): fail fast on empty FOUNDRY_TOOLBOX_ENDPOINT; add unit tests Addresses review feedback for #5670: - In _resolve_toolbox_endpoint() (04_foundry_toolbox/main.py and 06_files/main.py) change the walrus-operator check from a truthy test to an explicit 'is not None' guard. An explicitly set empty string now raises ValueError immediately with a clear message instead of silently falling through to the fallback URL construction. - Add tests/samples/hosting/test_toolbox_endpoint.py covering both sample modules: (a) FOUNDRY_TOOLBOX_ENDPOINT set → returned as-is (b) FOUNDRY_TOOLBOX_ENDPOINT set to empty string → ValueError (c) fallback constructs URL from FOUNDRY_PROJECT_ENDPOINT + TOOLBOX_NAME, stripping trailing slashes (d) neither variable group set → KeyError Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback: remove extraneous test and docstring content - Remove test_foundry_namespace_no_longer_exposes_toolbox_helpers (no longer warranted) - Remove docstring from _agent.py _prepare_tools_for_openai (extraneous) - Trim _chat_client.py _prepare_tools_for_openai docstring to one-liner (toolbox references no longer relevant) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: remove remaining extraneous docstring from RawFoundryChatClient._prepare_tools_for_openai Address review comment on PR #5671: reviewer noted the description isn't warranted now that toolbox helpers have been removed. Matches the pattern in RawFoundryAgentChatClient which has no docstring. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
172 lines
5.7 KiB
Python
172 lines
5.7 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from agent_framework import Content, Message
|
|
|
|
from agent_framework_bedrock import BedrockChatClient
|
|
|
|
|
|
class _StubBedrockRuntime:
|
|
def __init__(self) -> None:
|
|
self.calls: list[dict[str, Any]] = []
|
|
|
|
def converse(self, **kwargs: Any) -> dict[str, Any]:
|
|
self.calls.append(kwargs)
|
|
return {
|
|
"modelId": kwargs["modelId"],
|
|
"responseId": "resp-123",
|
|
"usage": {"inputTokens": 10, "outputTokens": 5, "totalTokens": 15},
|
|
"output": {
|
|
"completionReason": "end_turn",
|
|
"message": {
|
|
"id": "msg-1",
|
|
"role": "assistant",
|
|
"content": [{"text": "Bedrock says hi"}],
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def _make_client() -> BedrockChatClient:
|
|
"""Create a BedrockChatClient with a stub runtime for unit tests."""
|
|
return BedrockChatClient(
|
|
model="amazon.titan-text",
|
|
region="us-west-2",
|
|
client=_StubBedrockRuntime(),
|
|
)
|
|
|
|
|
|
async def test_get_response_invokes_bedrock_runtime() -> None:
|
|
stub = _StubBedrockRuntime()
|
|
client = BedrockChatClient(
|
|
model="amazon.titan-text",
|
|
region="us-west-2",
|
|
client=stub,
|
|
)
|
|
|
|
messages = [
|
|
Message(role="system", contents=[Content.from_text(text="You are concise.")]),
|
|
Message(role="user", contents=[Content.from_text(text="hello")]),
|
|
]
|
|
|
|
response = await client.get_response(messages=messages, options={"max_tokens": 32})
|
|
|
|
assert stub.calls, "Expected the runtime client to be called"
|
|
payload = stub.calls[0]
|
|
assert payload["modelId"] == "amazon.titan-text"
|
|
assert payload["messages"][0]["content"][0]["text"] == "hello"
|
|
assert response.messages[0].contents[0].text == "Bedrock says hi"
|
|
assert response.usage_details and response.usage_details["input_token_count"] == 10
|
|
|
|
|
|
def test_build_request_requires_non_system_messages() -> None:
|
|
client = BedrockChatClient(
|
|
model="amazon.titan-text",
|
|
region="us-west-2",
|
|
client=_StubBedrockRuntime(),
|
|
)
|
|
|
|
messages = [Message(role="system", contents=[Content.from_text(text="Only system text")])]
|
|
|
|
with pytest.raises(ValueError):
|
|
client._prepare_options(messages, {})
|
|
|
|
|
|
def test_prepare_options_tool_choice_none_omits_tool_config() -> None:
|
|
"""When tool_choice='none', toolConfig must be omitted entirely.
|
|
|
|
Bedrock's Converse API only accepts 'auto', 'any', or 'tool' as valid
|
|
toolChoice keys. Sending {"none": {}} causes a ParamValidationError.
|
|
The fix omits toolConfig so the model won't attempt tool calls.
|
|
|
|
Fixes #4529.
|
|
"""
|
|
client = _make_client()
|
|
messages = [Message(role="user", contents=[Content.from_text(text="hello")])]
|
|
|
|
# Even when tools are provided, tool_choice="none" should strip toolConfig
|
|
options: dict[str, Any] = {
|
|
"tool_choice": "none",
|
|
"tools": [
|
|
{"toolSpec": {"name": "get_weather", "description": "Get weather", "inputSchema": {"json": {}}}},
|
|
],
|
|
}
|
|
|
|
request = client._prepare_options(messages, options)
|
|
|
|
assert "toolConfig" not in request, (
|
|
f"toolConfig should be omitted when tool_choice='none', got: {request.get('toolConfig')}"
|
|
)
|
|
|
|
|
|
def test_prepare_options_tool_choice_auto_includes_tool_config() -> None:
|
|
"""When tool_choice='auto', toolConfig.toolChoice should be {'auto': {}}."""
|
|
client = _make_client()
|
|
messages = [Message(role="user", contents=[Content.from_text(text="hello")])]
|
|
|
|
options: dict[str, Any] = {
|
|
"tool_choice": "auto",
|
|
"tools": [
|
|
{"toolSpec": {"name": "get_weather", "description": "Get weather", "inputSchema": {"json": {}}}},
|
|
],
|
|
}
|
|
|
|
request = client._prepare_options(messages, options)
|
|
|
|
assert "toolConfig" in request
|
|
assert request["toolConfig"]["toolChoice"] == {"auto": {}}
|
|
|
|
|
|
def test_prepare_options_tool_choice_required_includes_any() -> None:
|
|
"""When tool_choice='required' (no specific function), toolChoice should be {'any': {}}."""
|
|
client = _make_client()
|
|
messages = [Message(role="user", contents=[Content.from_text(text="hello")])]
|
|
|
|
options: dict[str, Any] = {
|
|
"tool_choice": "required",
|
|
"tools": [
|
|
{"toolSpec": {"name": "get_weather", "description": "Get weather", "inputSchema": {"json": {}}}},
|
|
],
|
|
}
|
|
|
|
request = client._prepare_options(messages, options)
|
|
|
|
assert "toolConfig" in request
|
|
assert request["toolConfig"]["toolChoice"] == {"any": {}}
|
|
|
|
|
|
def test_prepare_options_tool_choice_auto_without_tools_omits_tool_config() -> None:
|
|
"""When tool_choice='auto' but no tools are provided, toolConfig must be omitted.
|
|
|
|
Without tools, setting toolChoice would cause a ParamValidationError from Bedrock.
|
|
"""
|
|
client = _make_client()
|
|
messages = [Message(role="user", contents=[Content.from_text(text="hello")])]
|
|
|
|
options: dict[str, Any] = {
|
|
"tool_choice": "auto",
|
|
}
|
|
|
|
request = client._prepare_options(messages, options)
|
|
|
|
assert "toolConfig" not in request, (
|
|
f"toolConfig should be omitted when no tools are provided, got: {request.get('toolConfig')}"
|
|
)
|
|
|
|
|
|
def test_prepare_options_tool_choice_required_without_tools_raises() -> None:
|
|
"""When tool_choice='required' but no tools are provided, a ValueError must be raised."""
|
|
client = _make_client()
|
|
messages = [Message(role="user", contents=[Content.from_text(text="hello")])]
|
|
|
|
options: dict[str, Any] = {
|
|
"tool_choice": "required",
|
|
}
|
|
|
|
with pytest.raises(ValueError, match="tool_choice='required' requires at least one tool"):
|
|
client._prepare_options(messages, options)
|