mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
8457533c69
commit
1b10b051fd
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user