Python: Fix tool normalization and provider sample consolidation (#3953)

* Fix tool normalization and provider samples

- restore callable/single-tool normalization paths and unset tool-choice behavior\n- consolidate and expand chat/provider samples (OpenAI/Azure/Anthropic/Ollama/Bedrock)\n- migrate Bedrock lazy import surface to agent_framework.amazon and move provider samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* small fix in sample

* Finalize provider, samples, and core cleanup

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix CopilotTool passthrough in agent

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix link

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-02-16 16:30:38 +00:00
committed by GitHub
co-authored by Copilot
parent ed113f941c
commit aab621f5eb
99 changed files with 1190 additions and 969 deletions
@@ -56,6 +56,36 @@ async def test_base_client_with_function_calling(chat_client_base: SupportsChatG
assert response.messages[2].text == "done"
async def test_base_client_with_function_calling_tools_in_kwargs(chat_client_base: SupportsChatGetResponse):
exec_counter = 0
@tool(name="test_function", approval_mode="never_require")
def ai_func(arg1: str) -> str:
nonlocal exec_counter
exec_counter += 1
return f"Processed {arg1}"
chat_client_base.run_responses = [
ChatResponse(
messages=Message(
role="assistant",
contents=[
Content.from_function_call(call_id="1", name="test_function", arguments='{"arg1": "value1"}')
],
)
),
ChatResponse(messages=Message(role="assistant", text="done")),
]
response = await chat_client_base.get_response("hello", tools=[ai_func])
assert exec_counter == 1
assert len(response.messages) == 3
assert response.messages[1].role == "tool"
assert response.messages[1].contents[0].type == "function_result"
assert response.messages[1].contents[0].result == "Processed value1"
@pytest.mark.parametrize("max_iterations", [3])
async def test_base_client_with_function_calling_resets(chat_client_base: SupportsChatGetResponse):
exec_counter = 0
@@ -921,8 +921,8 @@ def test_chat_options_tool_choice_validation():
}
assert validate_tool_mode({"mode": "none"}) == {"mode": "none"}
# None should return mode==none
assert validate_tool_mode(None) == {"mode": "none"}
# None should remain unset
assert validate_tool_mode(None) is None
with raises(ContentError):
validate_tool_mode("invalid_mode")
@@ -701,6 +701,7 @@ def test_prepare_options_basic(mock_async_openai: MagicMock) -> None:
assert run_options["model"] == "gpt-4"
assert run_options["temperature"] == 0.7
assert run_options["top_p"] == 0.9
assert "tool_choice" not in run_options
assert tool_results is None
@@ -733,6 +734,52 @@ def test_prepare_options_with_tool_tool(mock_async_openai: MagicMock) -> None:
assert run_options["tool_choice"] == "auto"
def test_prepare_options_with_tools_without_tool_choice(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options keeps tool_choice unset when not provided."""
client = create_test_openai_assistants_client(mock_async_openai)
@tool(approval_mode="never_require")
def test_function(query: str) -> str:
"""A test function."""
return f"Result for {query}"
options = {
"tools": [test_function],
}
messages = [Message(role="user", text="Hello")]
run_options, _ = client._prepare_options(messages, options) # type: ignore
assert "tools" in run_options
assert "tool_choice" not in run_options
def test_prepare_options_with_single_tool_tool(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options with a single FunctionTool (non-sequence)."""
client = create_test_openai_assistants_client(mock_async_openai)
@tool(approval_mode="never_require")
def test_function(query: str) -> str:
"""A test function."""
return f"Result for {query}"
options = {
"tools": test_function,
"tool_choice": "auto",
}
messages = [Message(role="user", text="Hello")]
run_options, tool_results = client._prepare_options(messages, options) # type: ignore
assert "tools" in run_options
assert len(run_options["tools"]) == 1
assert run_options["tools"][0]["type"] == "function"
assert "function" in run_options["tools"][0]
assert run_options["tool_choice"] == "auto"
assert tool_results is None
def test_prepare_options_with_code_interpreter(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options with code interpreter tool."""
client = create_test_openai_assistants_client(mock_async_openai)
@@ -190,6 +190,21 @@ def test_unsupported_tool_handling(openai_unit_test_env: dict[str, str]) -> None
assert result["tools"] == [dict_tool]
def test_prepare_tools_with_single_function_tool(openai_unit_test_env: dict[str, str]) -> None:
"""Test that a single FunctionTool is accepted for tool preparation."""
client = OpenAIChatClient()
@tool(approval_mode="never_require")
def test_function(query: str) -> str:
"""A test function."""
return f"Result for {query}"
result = client._prepare_tools_for_openai(test_function)
assert "tools" in result
assert len(result["tools"]) == 1
assert result["tools"][0]["type"] == "function"
@tool(approval_mode="never_require")
def get_story_text() -> str:
"""Returns a story about Emily and David."""