Python: (samples): adopt AzureOpenAIResponsesClient, reorganize orchestration examples, and fix workflow/orchestration bugs (#3873)

* adopt AzureOpenAIResponsesClient, reorganize orchestration examples, and fix workflow/orchestration bugs

* Updates

* add comment
This commit is contained in:
Evan Mattson
2026-02-12 19:46:58 +09:00
committed by GitHub
Unverified
parent 8457533c69
commit 1b10b051fd
73 changed files with 1612 additions and 686 deletions
@@ -677,6 +677,40 @@ def test_prepare_content_for_openai_hosted_vector_store_content() -> None:
assert result == {}
def test_prepare_content_for_openai_text_uses_role_specific_type() -> None:
"""Text content should use input_text for user and output_text for assistant."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
text_content = Content.from_text(text="hello")
user_result = client._prepare_content_for_openai("user", text_content, {})
assistant_result = client._prepare_content_for_openai("assistant", text_content, {})
assert user_result["type"] == "input_text"
assert assistant_result["type"] == "output_text"
assert assistant_result["annotations"] == []
assert user_result["text"] == "hello"
assert assistant_result["text"] == "hello"
def test_prepare_messages_for_openai_assistant_history_uses_output_text_with_annotations() -> None:
"""Assistant history should be output_text and include required annotations."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
messages = [
Message(role="user", text="What is async/await?"),
Message(role="assistant", text="Async/await enables non-blocking concurrency."),
]
prepared = client._prepare_messages_for_openai(messages)
assert prepared[0]["role"] == "user"
assert prepared[0]["content"][0]["type"] == "input_text"
assert prepared[1]["role"] == "assistant"
assert prepared[1]["content"][0]["type"] == "output_text"
assert prepared[1]["content"][0]["annotations"] == []
def test_parse_response_from_openai_with_mcp_server_tool_result() -> None:
"""Test _parse_response_from_openai with MCP server tool result."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
@@ -8,6 +8,7 @@ from typing_extensions import Never
from agent_framework import (
AgentExecutor,
AgentExecutorRequest,
AgentExecutorResponse,
AgentResponse,
AgentResponseUpdate,
@@ -150,3 +151,64 @@ async def test_sequential_adapter_uses_full_conversation() -> None:
assert len(seen) == 2
assert seen[0].role == "user" and "hello seq" in (seen[0].text or "")
assert seen[1].role == "assistant" and "A1 reply" in (seen[1].text or "")
class _RoundTripCoordinator(Executor):
"""Loops once back to the same agent with full conversation + feedback."""
def __init__(self, *, target_agent_id: str, id: str = "round_trip_coordinator") -> None:
super().__init__(id=id)
self._target_agent_id = target_agent_id
self._seen = 0
@handler
async def handle_response(
self,
response: AgentExecutorResponse,
ctx: WorkflowContext[Never, dict[str, Any]],
) -> None:
self._seen += 1
if self._seen == 1:
assert response.full_conversation is not None
await ctx.send_message(
AgentExecutorRequest(
messages=list(response.full_conversation) + [Message(role="user", text="apply feedback")],
should_respond=True,
),
target_id=self._target_agent_id,
)
return
assert response.full_conversation is not None
await ctx.yield_output({
"roles": [m.role for m in response.full_conversation],
"texts": [m.text for m in response.full_conversation],
})
async def test_agent_executor_full_conversation_round_trip_does_not_duplicate_history() -> None:
"""When full history is replayed, AgentExecutor should not duplicate prior turns."""
agent = _SimpleAgent(id="writer_agent", name="Writer", reply_text="draft reply")
agent_exec = AgentExecutor(agent, id="writer_agent")
coordinator = _RoundTripCoordinator(target_agent_id="writer_agent")
wf = (
WorkflowBuilder(start_executor=agent_exec, output_executors=[coordinator])
.add_edge(agent_exec, coordinator)
.add_edge(coordinator, agent_exec)
.build()
)
result = await wf.run("initial prompt")
outputs = result.get_outputs()
assert len(outputs) == 1
payload = outputs[0]
assert isinstance(payload, dict)
# Expected conversation after one loop:
# user(initial), assistant(first reply), user(feedback), assistant(second reply)
assert payload["roles"] == ["user", "assistant", "user", "assistant"]
assert payload["texts"][0] == "initial prompt"
assert payload["texts"][1] == "draft reply"
assert payload["texts"][2] == "apply feedback"
assert payload["texts"][3] == "draft reply"
@@ -72,6 +72,41 @@ class _KwargsCapturingAgent(BaseAgent):
return _run()
class _OptionsAwareAgent(BaseAgent):
"""Test agent that captures explicit `options` and kwargs passed to run()."""
captured_options: list[dict[str, Any] | None]
captured_kwargs: list[dict[str, Any]]
def __init__(self, name: str = "options_agent") -> None:
super().__init__(name=name, description="Test agent for options capture")
self.captured_options = []
self.captured_kwargs = []
def run(
self,
messages: str | Message | Sequence[str | Message] | None = None,
*,
stream: bool = False,
thread: AgentThread | None = None,
options: dict[str, Any] | None = None,
**kwargs: Any,
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
self.captured_options.append(dict(options) if options is not None else None)
self.captured_kwargs.append(dict(kwargs))
if stream:
async def _stream() -> AsyncIterable[AgentResponseUpdate]:
yield AgentResponseUpdate(contents=[Content.from_text(text=f"{self.name} response")])
return ResponseStream(_stream(), finalizer=AgentResponse.from_updates)
async def _run() -> AgentResponse:
return AgentResponse(messages=[Message("assistant", [f"{self.name} response"])])
return _run()
# region Sequential Builder Tests
@@ -131,6 +166,106 @@ async def test_sequential_run_kwargs_flow() -> None:
assert agent.captured_kwargs[0].get("custom_data") == {"test": True}
async def test_sequential_run_options_does_not_conflict_with_agent_options() -> None:
"""Test workflow.run(options=...) does not conflict with Agent.run(options=...)."""
agent = _OptionsAwareAgent(name="options_agent")
workflow = SequentialBuilder(participants=[agent]).build()
custom_data = {"session_id": "abc123"}
user_token = {"user_name": "alice"}
provided_options = {
"store": False,
"additional_function_arguments": {"source": "workflow-options"},
}
async for event in workflow.run(
"test message",
stream=True,
options=provided_options,
custom_data=custom_data,
user_token=user_token,
):
if event.type == "status" and event.state == WorkflowRunState.IDLE:
break
assert len(agent.captured_options) >= 1
captured_options = agent.captured_options[0]
assert captured_options is not None
assert captured_options.get("store") is False
additional_args = captured_options.get("additional_function_arguments")
assert isinstance(additional_args, dict)
assert additional_args.get("source") == "workflow-options"
assert additional_args.get("custom_data") == custom_data
assert additional_args.get("user_token") == user_token
# "options" should be passed once via the dedicated options parameter,
# not duplicated in **kwargs.
assert len(agent.captured_kwargs) >= 1
captured_kwargs = agent.captured_kwargs[0]
assert "options" not in captured_kwargs
assert captured_kwargs.get("custom_data") == custom_data
assert captured_kwargs.get("user_token") == user_token
async def test_sequential_run_additional_function_arguments_flattened() -> None:
"""Test workflow.run(additional_function_arguments=...) maps directly to tool kwargs."""
agent = _OptionsAwareAgent(name="options_agent")
workflow = SequentialBuilder(participants=[agent]).build()
custom_data = {"session_id": "abc123"}
user_token = {"user_name": "alice"}
async for event in workflow.run(
"test message",
stream=True,
additional_function_arguments={"custom_data": custom_data, "user_token": user_token},
):
if event.type == "status" and event.state == WorkflowRunState.IDLE:
break
assert len(agent.captured_options) >= 1
captured_options = agent.captured_options[0]
assert captured_options is not None
additional_args = captured_options.get("additional_function_arguments")
assert isinstance(additional_args, dict)
assert additional_args.get("custom_data") == custom_data
assert additional_args.get("user_token") == user_token
assert "additional_function_arguments" not in additional_args
assert len(agent.captured_kwargs) >= 1
captured_kwargs = agent.captured_kwargs[0]
assert "additional_function_arguments" not in captured_kwargs
async def test_sequential_run_additional_function_arguments_merges_with_options() -> None:
"""Test workflow additional_function_arguments merges with workflow options."""
agent = _OptionsAwareAgent(name="options_agent")
workflow = SequentialBuilder(participants=[agent]).build()
async for event in workflow.run(
"test message",
stream=True,
options={"additional_function_arguments": {"source": "workflow-options"}},
additional_function_arguments={"custom_data": {"session_id": "abc123"}},
user_token={"user_name": "alice"},
):
if event.type == "status" and event.state == WorkflowRunState.IDLE:
break
assert len(agent.captured_options) >= 1
captured_options = agent.captured_options[0]
assert captured_options is not None
additional_args = captured_options.get("additional_function_arguments")
assert isinstance(additional_args, dict)
assert additional_args.get("source") == "workflow-options"
assert additional_args.get("custom_data") == {"session_id": "abc123"}
assert additional_args.get("user_token") == {"user_name": "alice"}
assert "additional_function_arguments" not in additional_args
# endregion