Python: [BREAKING] Fix #3613 chat/agent message typing alignment (#3920)

* Fix #3613 message typing across chat and agents

* Address #3613 review feedback and sample input style

* refactor: use shared AgentRunMessages aliases (#3613)

* refactor: rename agent run input aliases for #3613

* samples: inline image content in run calls

* core: export AgentRunInputs from package init

* core: use explicit init re-exports without __all__

* updated logging and inits

* Fix core mypy export and samples XML note

* Remove AgentRunInputsOrNone and dedupe loggers

* Remove prepare_messages helper

* fix integration tests
This commit is contained in:
Eduard van Valkenburg
2026-02-16 16:27:25 +01:00
committed by GitHub
Unverified
parent 503eb10fdd
commit dc9439a75a
87 changed files with 422 additions and 578 deletions
@@ -334,8 +334,10 @@ async def test_integration_options(
messages = [Message(role="user", text="What is the weather in Seattle?")]
elif option_name == "response_format":
# Use prompt that works well with structured output
messages = [Message(role="user", text="The weather in Seattle is sunny")]
messages.append(Message(role="user", text="What is the weather in Seattle?"))
messages = [
Message(role="user", text="The weather in Seattle is sunny"),
Message(role="user", text="What is the weather in Seattle?"),
]
else:
# Generic prompt for simple options
messages = [Message(role="user", text="Say 'Hello World' briefly.")]
@@ -396,7 +398,12 @@ async def test_integration_web_search() -> None:
for streaming in [False, True]:
content = {
"messages": "Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.",
"messages": [
Message(
role="user",
text="Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.",
)
],
"options": {
"tool_choice": "auto",
"tools": [AzureOpenAIResponsesClient.get_web_search_tool()],
@@ -416,7 +423,7 @@ async def test_integration_web_search() -> None:
# Test that the client will use the web search tool with location
content = {
"messages": "What is the current weather? Do not ask for my current location.",
"messages": [Message(role="user", text="What is the current weather? Do not ask for my current location.")],
"options": {
"tool_choice": "auto",
"tools": [
@@ -498,7 +505,7 @@ async def test_integration_client_agent_hosted_mcp_tool() -> None:
"""Integration test for MCP tool with Azure Response Agent using Microsoft Learn MCP."""
client = AzureOpenAIResponsesClient(credential=AzureCliCredential())
response = await client.get_response(
"How to create an Azure storage account using az cli?",
messages=[Message(role="user", text="How to create an Azure storage account using az cli?")],
options={
# this needs to be high enough to handle the full MCP tool response.
"max_tokens": 5000,
@@ -523,7 +530,7 @@ async def test_integration_client_agent_hosted_code_interpreter_tool():
client = AzureOpenAIResponsesClient(credential=AzureCliCredential())
response = await client.get_response(
"Calculate the sum of numbers from 1 to 10 using Python code.",
messages=[Message(role="user", text="Calculate the sum of numbers from 1 to 10 using Python code.")],
options={
"tools": [AzureOpenAIResponsesClient.get_code_interpreter_tool()],
},
@@ -43,6 +43,12 @@ async def test_agent_run(agent: SupportsAgentRun) -> None:
assert response.messages[0].text == "Response"
async def test_agent_run_with_content(agent: SupportsAgentRun) -> None:
response = await agent.run(Content.from_text("test"))
assert response.messages[0].role == "assistant"
assert response.messages[0].text == "Response"
async def test_agent_run_streaming(agent: SupportsAgentRun) -> None:
async def collect_updates(updates: AsyncIterable[AgentResponseUpdate]) -> list[AgentResponseUpdate]:
return [u async for u in updates]
@@ -21,13 +21,13 @@ def test_chat_client_type(client: SupportsChatGetResponse):
async def test_chat_client_get_response(client: SupportsChatGetResponse):
response = await client.get_response(Message(role="user", text="Hello"))
response = await client.get_response([Message(role="user", text="Hello")])
assert response.text == "test response"
assert response.messages[0].role == "assistant"
async def test_chat_client_get_response_streaming(client: SupportsChatGetResponse):
async for update in client.get_response(Message(role="user", text="Hello"), stream=True):
async for update in client.get_response([Message(role="user", text="Hello")], stream=True):
assert update.text == "test streaming response " or update.text == "another update"
assert update.role == "assistant"
@@ -38,13 +38,13 @@ def test_base_client(chat_client_base: SupportsChatGetResponse):
async def test_base_client_get_response(chat_client_base: SupportsChatGetResponse):
response = await chat_client_base.get_response(Message(role="user", text="Hello"))
response = await chat_client_base.get_response([Message(role="user", text="Hello")])
assert response.messages[0].role == "assistant"
assert response.messages[0].text == "test response - Hello"
async def test_base_client_get_response_streaming(chat_client_base: SupportsChatGetResponse):
async for update in chat_client_base.get_response(Message(role="user", text="Hello"), stream=True):
async for update in chat_client_base.get_response([Message(role="user", text="Hello")], stream=True):
assert update.text == "update - Hello" or update.text == "another update"
@@ -59,7 +59,9 @@ async def test_chat_client_instructions_handling(chat_client_base: SupportsChatG
"_inner_get_response",
side_effect=fake_inner_get_response,
) as mock_inner_get_response:
await chat_client_base.get_response("hello", options={"instructions": instructions})
await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"instructions": instructions}
)
mock_inner_get_response.assert_called_once()
_, kwargs = mock_inner_get_response.call_args
messages = kwargs.get("messages", [])
@@ -38,7 +38,9 @@ async def test_base_client_with_function_calling(chat_client_base: SupportsChatG
),
ChatResponse(messages=Message(role="assistant", text="done")),
]
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [ai_func]})
response = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [ai_func]}
)
assert exec_counter == 1
assert len(response.messages) == 3
assert response.messages[0].role == "assistant"
@@ -83,7 +85,9 @@ async def test_base_client_with_function_calling_resets(chat_client_base: Suppor
),
ChatResponse(messages=Message(role="assistant", text="done")),
]
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [ai_func]})
response = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [ai_func]}
)
assert exec_counter == 2
assert len(response.messages) == 5
assert response.messages[0].role == "assistant"
@@ -388,11 +392,13 @@ async def test_function_invocation_scenarios(
options["conversation_id"] = conversation_id
if not streaming:
response = await chat_client_base.get_response("hello", options=options)
response = await chat_client_base.get_response([Message(role="user", text="hello")], options=options)
messages = response.messages
else:
updates = []
async for update in chat_client_base.get_response("hello", options=options, stream=True):
async for update in chat_client_base.get_response(
[Message(role="user", text="hello")], options=options, stream=True
):
updates.append(update)
messages = updates
@@ -776,7 +782,9 @@ async def test_max_iterations_limit(chat_client_base: SupportsChatGetResponse):
# Set max_iterations to 1 in additional_properties
chat_client_base.function_invocation_configuration["max_iterations"] = 1
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [ai_func]})
response = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [ai_func]}
)
# With max_iterations=1, we should:
# 1. Execute first function call (exec_counter=1)
@@ -803,7 +811,9 @@ async def test_function_invocation_config_enabled_false(chat_client_base: Suppor
# Disable function invocation
chat_client_base.function_invocation_configuration["enabled"] = False
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [ai_func]})
response = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [ai_func]}
)
# Function should not be executed - when enabled=False, the loop doesn't run
assert exec_counter == 0
@@ -859,7 +869,9 @@ async def test_function_invocation_config_max_consecutive_errors(chat_client_bas
# Set max_consecutive_errors to 2
chat_client_base.function_invocation_configuration["max_consecutive_errors_per_request"] = 2
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [error_func]})
response = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [error_func]}
)
# Should stop after 2 consecutive errors and force a non-tool response
error_results = [
@@ -904,7 +916,9 @@ async def test_function_invocation_config_terminate_on_unknown_calls_false(chat_
# Set terminate_on_unknown_calls to False (default)
chat_client_base.function_invocation_configuration["terminate_on_unknown_calls"] = False
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [known_func]})
response = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [known_func]}
)
# Should have a result message indicating the tool wasn't found
assert len(response.messages) == 3
@@ -940,7 +954,9 @@ async def test_function_invocation_config_terminate_on_unknown_calls_true(chat_c
# Should raise an exception when encountering an unknown function
with pytest.raises(KeyError, match='Error: Requested function "unknown_function" not found'):
await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [known_func]})
await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [known_func]}
)
assert exec_counter == 0
@@ -978,7 +994,9 @@ async def test_function_invocation_config_additional_tools(chat_client_base: Sup
chat_client_base.function_invocation_configuration["additional_tools"] = [hidden_func]
# Only pass visible_func in the tools parameter
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [visible_func]})
response = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [visible_func]}
)
# Additional tools are treated as declaration_only, so not executed
# The function call should be in the messages but not executed
@@ -1016,7 +1034,9 @@ async def test_function_invocation_config_include_detailed_errors_false(chat_cli
# Set include_detailed_errors to False (default)
chat_client_base.function_invocation_configuration["include_detailed_errors"] = False
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [error_func]})
response = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [error_func]}
)
# Should have a generic error message
error_result = next(
@@ -1050,7 +1070,9 @@ async def test_function_invocation_config_include_detailed_errors_true(chat_clie
# Set include_detailed_errors to True
chat_client_base.function_invocation_configuration["include_detailed_errors"] = True
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [error_func]})
response = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [error_func]}
)
# Should have detailed error message
error_result = next(
@@ -1120,7 +1142,9 @@ async def test_argument_validation_error_with_detailed_errors(chat_client_base:
# Set include_detailed_errors to True
chat_client_base.function_invocation_configuration["include_detailed_errors"] = True
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [typed_func]})
response = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [typed_func]}
)
# Should have detailed validation error
error_result = next(
@@ -1154,7 +1178,9 @@ async def test_argument_validation_error_without_detailed_errors(chat_client_bas
# Set include_detailed_errors to False (default)
chat_client_base.function_invocation_configuration["include_detailed_errors"] = False
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [typed_func]})
response = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [typed_func]}
)
# Should have generic validation error
error_result = next(
@@ -1219,7 +1245,9 @@ async def test_unapproved_tool_execution_raises_exception(chat_client_base: Supp
]
# Get approval request
response1 = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [test_func]})
response1 = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [test_func]}
)
approval_req = [c for c in response1.messages[0].contents if c.type == "function_approval_request"][0]
@@ -1277,7 +1305,9 @@ async def test_approved_function_call_with_error_without_detailed_errors(chat_cl
chat_client_base.function_invocation_configuration["include_detailed_errors"] = False
# Get approval request
response1 = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [error_func]})
response1 = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [error_func]}
)
approval_req = [c for c in response1.messages[0].contents if c.type == "function_approval_request"][0]
@@ -1340,7 +1370,9 @@ async def test_approved_function_call_with_error_with_detailed_errors(chat_clien
chat_client_base.function_invocation_configuration["include_detailed_errors"] = True
# Get approval request
response1 = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [error_func]})
response1 = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [error_func]}
)
approval_req = [c for c in response1.messages[0].contents if c.type == "function_approval_request"][0]
@@ -1403,7 +1435,9 @@ async def test_approved_function_call_with_validation_error(chat_client_base: Su
chat_client_base.function_invocation_configuration["include_detailed_errors"] = True
# Get approval request
response1 = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [typed_func]})
response1 = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [typed_func]}
)
approval_req = [c for c in response1.messages[0].contents if c.type == "function_approval_request"][0]
@@ -1459,7 +1493,9 @@ async def test_approved_function_call_successful_execution(chat_client_base: Sup
]
# Get approval request
response1 = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [success_func]})
response1 = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [success_func]}
)
approval_req = [c for c in response1.messages[0].contents if c.type == "function_approval_request"][0]
@@ -1575,7 +1611,9 @@ async def test_multiple_function_calls_parallel_execution(chat_client_base: Supp
ChatResponse(messages=Message(role="assistant", text="done")),
]
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [func1, func2]})
response = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [func1, func2]}
)
# Both functions should have been executed
assert "func1_start" in exec_order
@@ -1612,7 +1650,9 @@ async def test_callable_function_converted_to_tool(chat_client_base: SupportsCha
]
# Pass plain function (will be auto-converted)
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [plain_function]})
response = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [plain_function]}
)
# Function should be executed
assert exec_counter == 1
@@ -1644,7 +1684,9 @@ async def test_conversation_id_handling(chat_client_base: SupportsChatGetRespons
),
]
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [test_func]})
response = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [test_func]}
)
# Should have executed the function
results = [content for msg in response.messages for content in msg.contents if content.type == "function_result"]
@@ -1671,7 +1713,9 @@ async def test_function_result_appended_to_existing_assistant_message(chat_clien
ChatResponse(messages=Message(role="assistant", text="done")),
]
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [test_func]})
response = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [test_func]}
)
# Should have messages with both function call and function result
assert len(response.messages) >= 2
@@ -1716,7 +1760,9 @@ async def test_error_recovery_resets_counter(chat_client_base: SupportsChatGetRe
ChatResponse(messages=Message(role="assistant", text="done")),
]
response = await chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [sometimes_fails]})
response = await chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [sometimes_fails]}
)
# Should have both an error and a success
error_results = [
@@ -1990,7 +2036,9 @@ async def test_streaming_function_invocation_config_terminate_on_unknown_calls_t
# Should raise an exception when encountering an unknown function
with pytest.raises(KeyError, match='Error: Requested function "unknown_function" not found'):
async for _ in chat_client_base.get_response("hello", options={"tool_choice": "auto", "tools": [known_func]}):
async for _ in chat_client_base.get_response(
[Message(role="user", text="hello")], options={"tool_choice": "auto", "tools": [known_func]}
):
pass
assert exec_counter == 0
@@ -1,39 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import pytest
from agent_framework import get_logger
from agent_framework.exceptions import AgentFrameworkException
def test_get_logger():
"""Test that the logger is created with the correct name."""
logger = get_logger()
assert logger.name == "agent_framework"
def test_get_logger_custom_name():
"""Test that the logger can be created with a custom name."""
custom_name = "agent_framework.custom"
logger = get_logger(custom_name)
assert logger.name == custom_name
def test_get_logger_invalid_name():
"""Test that an exception is raised for an invalid logger name."""
with pytest.raises(AgentFrameworkException):
get_logger("invalid_name")
def test_log(caplog):
"""Test that the logger can log messages and adheres to the expected format."""
logger = get_logger()
with caplog.at_level("DEBUG"):
logger.debug("This is a debug message")
assert len(caplog.records) == 1
record = caplog.records[0]
assert record.levelname == "DEBUG"
assert record.message == "This is a debug message"
assert record.name == "agent_framework"
assert record.pathname.endswith("test_logging.py")
@@ -1083,7 +1083,12 @@ async def test_integration_web_search() -> None:
# Use static method for web search tool
web_search_tool = OpenAIChatClient.get_web_search_tool()
content = {
"messages": "Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.",
"messages": [
Message(
role="user",
text="Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.",
)
],
"options": {
"tool_choice": "auto",
"tools": [web_search_tool],
@@ -1110,7 +1115,7 @@ async def test_integration_web_search() -> None:
}
)
content = {
"messages": "What is the current weather? Do not ask for my current location.",
"messages": [Message(role="user", text="What is the current weather? Do not ask for my current location.")],
"options": {
"tool_choice": "auto",
"tools": [web_search_tool_with_location],
@@ -2416,7 +2416,12 @@ async def test_integration_web_search() -> None:
# Use static method for web search tool
web_search_tool = OpenAIResponsesClient.get_web_search_tool()
content = {
"messages": "Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.",
"messages": [
Message(
role="user",
text="Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.",
)
],
"options": {
"tool_choice": "auto",
"tools": [web_search_tool],
@@ -2438,7 +2443,7 @@ async def test_integration_web_search() -> None:
user_location={"country": "US", "city": "Seattle"},
)
content = {
"messages": "What is the current weather? Do not ask for my current location.",
"messages": [Message(role="user", text="What is the current weather? Do not ask for my current location.")],
"options": {
"tool_choice": "auto",
"tools": [web_search_tool_with_location],
@@ -39,7 +39,7 @@ class _ToolCallingAgent(BaseAgent):
def run(
self,
messages: str | Message | Sequence[str | Message] | None = None,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
@@ -35,7 +35,7 @@ class _SimpleAgent(BaseAgent):
def run(
self,
messages: str | Message | Sequence[str | Message] | None = None,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
@@ -105,7 +105,7 @@ class _CaptureAgent(BaseAgent):
def run(
self,
messages: str | Message | Sequence[str | Message] | None = None,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
@@ -835,7 +835,7 @@ class _StreamingTestAgent(BaseAgent):
def run(
self,
messages: str | Message | Sequence[str | Message] | None = None,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
@@ -52,7 +52,7 @@ class _KwargsCapturingAgent(BaseAgent):
def run(
self,
messages: str | Message | Sequence[str | Message] | None = None,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,
@@ -85,7 +85,7 @@ class _OptionsAwareAgent(BaseAgent):
def run(
self,
messages: str | Message | Sequence[str | Message] | None = None,
messages: str | Content | Message | Sequence[str | Content | Message] | None = None,
*,
stream: bool = False,
session: AgentSession | None = None,