diff --git a/python/packages/foundry/README.md b/python/packages/foundry/README.md index 56d13cbd872..6f93fdf59fa 100644 --- a/python/packages/foundry/README.md +++ b/python/packages/foundry/README.md @@ -8,6 +8,19 @@ This package supports `azure-ai-projects>=2.2.0,<2.8.0`. Projects 2.5 and later `openai>=3.0.0`, so `agent-framework-foundry` requires `agent-framework-openai>=1.14.2`, which supports both OpenAI 2.x and 3.x. +## Tool ownership for preconfigured Foundry agents + +`FoundryAgent` connects to a PromptAgent or HostedAgent whose instructions, tool declarations, and tool-selection +behavior are configured in Microsoft Foundry. The `tools=` argument supplies only the local Python implementations +for function declarations already present on that remote agent; it does not replace or restrict the remote tool +inventory. + +Per-run `tool_choice` restrictions and `allow_multiple_tool_calls` values cannot be sent with an agent reference and +are ignored with a warning. The framework's unrestricted `tool_choice="auto"` default is omitted silently. Configure +tool behavior on the remote Foundry agent, or use `FoundryChatClient` when tool selection must vary per request. +`FunctionMiddleware` and tool approval policies can authorize local Python execution, but they do not change tools +that execute inside the remote Foundry agent. + ## Tracing an existing Foundry agent Install Azure Monitor to connect client and service traces: diff --git a/python/packages/foundry/agent_framework_foundry/_agent.py b/python/packages/foundry/agent_framework_foundry/_agent.py index 43374b85d29..65359d62794 100644 --- a/python/packages/foundry/agent_framework_foundry/_agent.py +++ b/python/packages/foundry/agent_framework_foundry/_agent.py @@ -103,6 +103,11 @@ class FoundryAgentOptions(OpenAIChatOptions, total=False): Keyword Args: extra_body: Additional request body values sent to the Responses API. + tool_choice: Not supported for pre-provisioned Foundry agents. The remote agent + owns tool selection, so caller-supplied values are ignored with a warning. + Use ``FoundryChatClient`` when tool selection must vary per request. + allow_multiple_tool_calls: Not supported for pre-provisioned Foundry agents. The + remote agent owns this setting, so caller-supplied values are ignored with a warning. isolation_key: Deprecated. This option no longer has any effect. """ @@ -409,6 +414,24 @@ async def _prepare_options( stripped_tools = run_options.pop("tools", None) run_options.pop("tool_choice", None) run_options.pop("parallel_tool_calls", None) + ignored_tool_options: list[str] = [] + tool_choice = options.get("tool_choice") + unrestricted_auto = tool_choice == "auto" + if isinstance(tool_choice, Mapping): + typed_tool_choice = cast(Mapping[str, Any], tool_choice) + unrestricted_auto = typed_tool_choice.get("mode") == "auto" and "allowed_tools" not in typed_tool_choice + if "tool_choice" in options and tool_choice is not None and not unrestricted_auto: + ignored_tool_options.append("tool_choice") + if "allow_multiple_tool_calls" in options: + ignored_tool_options.append("allow_multiple_tool_calls") + if ignored_tool_options: + logger.warning( + "Foundry agent '%s' owns tool selection server-side; caller-supplied options %s are ignored. " + "Configure tool behavior on the remote Foundry agent, or use FoundryChatClient for " + "per-request control.", + self.agent_name, + ignored_tool_options, + ) if stripped_tools: logger.warning( "Foundry agent '%s' was provided tools, but tool declarations cannot be sent when an " @@ -927,6 +950,10 @@ class FoundryAgent( # type: ignore[misc] Connects to an existing PromptAgent or HostedAgent in Foundry. This is the recommended class for production use. + The pre-provisioned Foundry agent owns its tool declarations and tool-selection + behavior. ``tools=`` supplies matching local Python implementations only; + per-run ``tool_choice`` restrictions and ``allow_multiple_tool_calls`` values are not propagated. + Examples: .. code-block:: python diff --git a/python/packages/foundry/tests/foundry/test_foundry_agent.py b/python/packages/foundry/tests/foundry/test_foundry_agent.py index 3fe96541499..0597f2c9f3c 100644 --- a/python/packages/foundry/tests/foundry/test_foundry_agent.py +++ b/python/packages/foundry/tests/foundry/test_foundry_agent.py @@ -458,6 +458,77 @@ def my_func() -> str: assert sum("cannot be sent when an agent is specified" in record.message for record in caplog.records) == 1 +@pytest.mark.parametrize("allow_preview", [False, True], ids=["stable", "preview"]) +async def test_raw_foundry_agent_chat_client_warns_when_explicit_tool_control_is_ignored( + caplog: pytest.LogCaptureFixture, + allow_preview: bool, +) -> None: + """Pre-provisioned agents own tool selection, so explicit request overrides are ignored visibly.""" + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + allow_preview=allow_preview, + ) + + with ( + patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", + new_callable=AsyncMock, + return_value={ + "model": "gpt-4.1", + "tool_choice": "none", + "parallel_tool_calls": False, + }, + ), + caplog.at_level("WARNING", logger="agent_framework.foundry"), + ): + result = await client._prepare_options( + messages=[Message(role="user", contents="hi")], + options={"tool_choice": "none", "allow_multiple_tool_calls": False}, + ) + + assert "tool_choice" not in result + assert "parallel_tool_calls" not in result + warning = next(record.message for record in caplog.records if "owns tool selection server-side" in record.message) + assert "tool_choice" in warning + assert "allow_multiple_tool_calls" in warning + + +@pytest.mark.parametrize("allow_preview", [False, True], ids=["stable", "preview"]) +async def test_raw_foundry_agent_chat_client_does_not_warn_for_framework_default_tool_choice( + caplog: pytest.LogCaptureFixture, + allow_preview: bool, +) -> None: + """The framework-generated auto mode must not be reported as caller intent.""" + + mock_project = MagicMock() + mock_project.get_openai_client.return_value = MagicMock() + client = RawFoundryAgentChatClient( + project_client=mock_project, + agent_name="test-agent", + allow_preview=allow_preview, + ) + + with ( + patch( + "agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options", + new_callable=AsyncMock, + return_value={"model": "gpt-4.1", "tool_choice": "auto"}, + ), + caplog.at_level("WARNING", logger="agent_framework.foundry"), + ): + result = await client._prepare_options( + messages=[Message(role="user", contents="hi")], + options={"tool_choice": "auto"}, + ) + + assert "tool_choice" not in result + assert not any("owns tool selection server-side" in record.message for record in caplog.records) + + async def test_raw_foundry_agent_chat_client_prepare_options_strips_tools_when_allow_preview( caplog: pytest.LogCaptureFixture, ) -> None: