Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions python/packages/foundry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
27 changes: 27 additions & 0 deletions python/packages/foundry/agent_framework_foundry/_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""

Expand Down Expand Up @@ -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 "
Expand Down Expand Up @@ -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

Expand Down
71 changes: 71 additions & 0 deletions python/packages/foundry/tests/foundry/test_foundry_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading