mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
[BREAKING] Python: Merge send_responses into run method (#3720)
* Streamline workflow run api with send responses in one method * Fixes * Address copilot feedback
This commit is contained in:
@@ -267,9 +267,9 @@ async def test_agent_executor_tool_call_with_approval() -> None:
|
||||
assert approval_request.data.function_call.arguments == '{"query": "test"}'
|
||||
|
||||
# Act
|
||||
events = await workflow.send_responses({
|
||||
approval_request.request_id: approval_request.data.to_function_approval_response(True)
|
||||
})
|
||||
events = await workflow.run(
|
||||
responses={approval_request.request_id: approval_request.data.to_function_approval_response(True)}
|
||||
)
|
||||
|
||||
# Assert
|
||||
final_response = events.get_outputs()
|
||||
@@ -303,9 +303,9 @@ async def test_agent_executor_tool_call_with_approval_streaming() -> None:
|
||||
|
||||
# Act
|
||||
output: str | None = None
|
||||
async for event in workflow.send_responses_streaming({
|
||||
approval_request.request_id: approval_request.data.to_function_approval_response(True)
|
||||
}):
|
||||
async for event in workflow.run(
|
||||
stream=True, responses={approval_request.request_id: approval_request.data.to_function_approval_response(True)}
|
||||
):
|
||||
if event.type == "output":
|
||||
output = event.data
|
||||
|
||||
@@ -346,7 +346,7 @@ async def test_agent_executor_parallel_tool_call_with_approval() -> None:
|
||||
approval_request.request_id: approval_request.data.to_function_approval_response(True) # type: ignore
|
||||
for approval_request in events.get_request_info_events()
|
||||
}
|
||||
events = await workflow.send_responses(responses)
|
||||
events = await workflow.run(responses=responses)
|
||||
|
||||
# Assert
|
||||
final_response = events.get_outputs()
|
||||
@@ -385,7 +385,7 @@ async def test_agent_executor_parallel_tool_call_with_approval_streaming() -> No
|
||||
}
|
||||
|
||||
output: str | None = None
|
||||
async for event in workflow.send_responses_streaming(responses):
|
||||
async for event in workflow.run(stream=True, responses=responses):
|
||||
if event.type == "output":
|
||||
output = event.data
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ class TestRequestInfoAndResponse:
|
||||
|
||||
# Send response and continue workflow
|
||||
completed = False
|
||||
async for event in workflow.send_responses_streaming({request_info_event.request_id: True}):
|
||||
async for event in workflow.run(stream=True, responses={request_info_event.request_id: True}):
|
||||
if event.type == "status" and event.state == WorkflowRunState.IDLE:
|
||||
completed = True
|
||||
|
||||
@@ -219,7 +219,7 @@ class TestRequestInfoAndResponse:
|
||||
# Send response with calculated result
|
||||
calculated_result = 31.0
|
||||
completed = False
|
||||
async for event in workflow.send_responses_streaming({request_info_event.request_id: calculated_result}):
|
||||
async for event in workflow.run(stream=True, responses={request_info_event.request_id: calculated_result}):
|
||||
if event.type == "status" and event.state == WorkflowRunState.IDLE:
|
||||
completed = True
|
||||
|
||||
@@ -254,7 +254,7 @@ class TestRequestInfoAndResponse:
|
||||
# Send responses for both requests
|
||||
responses = {approval_event.request_id: True, calc_event.request_id: 50.0}
|
||||
completed = False
|
||||
async for event in workflow.send_responses_streaming(responses):
|
||||
async for event in workflow.run(stream=True, responses=responses):
|
||||
if event.type == "status" and event.state == WorkflowRunState.IDLE:
|
||||
completed = True
|
||||
|
||||
@@ -276,7 +276,7 @@ class TestRequestInfoAndResponse:
|
||||
|
||||
# Deny the request
|
||||
completed = False
|
||||
async for event in workflow.send_responses_streaming({request_info_event.request_id: False}):
|
||||
async for event in workflow.run(stream=True, responses={request_info_event.request_id: False}):
|
||||
if event.type == "status" and event.state == WorkflowRunState.IDLE:
|
||||
completed = True
|
||||
|
||||
@@ -303,7 +303,7 @@ class TestRequestInfoAndResponse:
|
||||
|
||||
# Continue with response
|
||||
completed = False
|
||||
async for event in workflow.send_responses_streaming({request_info_event.request_id: True}):
|
||||
async for event in workflow.run(stream=True, responses={request_info_event.request_id: True}):
|
||||
if event.type == "status" and event.state == WorkflowRunState.IDLE:
|
||||
completed = True
|
||||
|
||||
@@ -395,9 +395,12 @@ class TestRequestInfoAndResponse:
|
||||
|
||||
# Step 6: Provide response to the restored request and complete the workflow
|
||||
final_completed = False
|
||||
async for event in restored_workflow.send_responses_streaming({
|
||||
request_info_event.request_id: True # Approve the request
|
||||
}):
|
||||
async for event in restored_workflow.run(
|
||||
stream=True,
|
||||
responses={
|
||||
request_info_event.request_id: True # Approve the request
|
||||
},
|
||||
):
|
||||
if event.type == "status" and event.state == WorkflowRunState.IDLE:
|
||||
final_completed = True
|
||||
|
||||
|
||||
@@ -201,9 +201,11 @@ async def test_basic_sub_workflow() -> None:
|
||||
assert request_events[0].data.domain == "example.com"
|
||||
|
||||
# Send response through the main workflow
|
||||
await main_workflow.send_responses({
|
||||
request_events[0].request_id: True # Domain is approved
|
||||
})
|
||||
await main_workflow.run(
|
||||
responses={
|
||||
request_events[0].request_id: True # Domain is approved
|
||||
}
|
||||
)
|
||||
|
||||
# Check result
|
||||
assert parent.result is not None
|
||||
@@ -245,9 +247,11 @@ async def test_sub_workflow_with_interception():
|
||||
assert request_events[0].data.domain == "unknown.com"
|
||||
|
||||
# Send external response
|
||||
await main_workflow.send_responses({
|
||||
request_events[0].request_id: False # Domain not approved
|
||||
})
|
||||
await main_workflow.run(
|
||||
responses={
|
||||
request_events[0].request_id: False # Domain not approved
|
||||
}
|
||||
)
|
||||
assert parent.result is not None
|
||||
assert parent.result.email == "user@unknown.com"
|
||||
assert parent.result.is_valid is False
|
||||
@@ -447,7 +451,7 @@ async def test_concurrent_sub_workflow_execution() -> None:
|
||||
|
||||
# Send responses for all requests (approve all domains)
|
||||
responses = {event.request_id: True for event in request_events}
|
||||
await main_workflow.send_responses(responses)
|
||||
await main_workflow.run(responses=responses)
|
||||
|
||||
# All results should be collected
|
||||
assert len(processor.results) == len(emails)
|
||||
@@ -613,7 +617,7 @@ async def test_sub_workflow_checkpoint_restore_no_duplicate_requests() -> None:
|
||||
assert resumed_first_request_id == first_request_id
|
||||
|
||||
request_events: list[WorkflowEvent] = []
|
||||
async for event in workflow2.send_responses_streaming({resumed_first_request_id: "first_answer"}):
|
||||
async for event in workflow2.run(stream=True, responses={resumed_first_request_id: "first_answer"}):
|
||||
if event.type == "request_info":
|
||||
request_events.append(event)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from agent_framework._workflows._typing_utils import (
|
||||
normalize_type_to_list,
|
||||
resolve_type_annotation,
|
||||
serialize_type,
|
||||
try_coerce_to_type,
|
||||
)
|
||||
|
||||
# region: normalize_type_to_list tests
|
||||
@@ -420,3 +421,72 @@ def test_type_compatibility_complex() -> None:
|
||||
# Incompatible nested structure
|
||||
incompatible_target = list[dict[Union[str, bytes], int]]
|
||||
assert not is_type_compatible(source, incompatible_target)
|
||||
|
||||
|
||||
# region: try_coerce_to_type tests
|
||||
|
||||
|
||||
def test_coerce_already_correct_type() -> None:
|
||||
"""Values already matching the target type are returned as-is."""
|
||||
assert try_coerce_to_type(42, int) == 42
|
||||
assert try_coerce_to_type("hello", str) == "hello"
|
||||
assert try_coerce_to_type(True, bool) is True
|
||||
|
||||
|
||||
def test_coerce_int_to_float() -> None:
|
||||
"""JSON integers should be coercible to float."""
|
||||
result = try_coerce_to_type(1, float)
|
||||
assert result == 1.0
|
||||
assert isinstance(result, float)
|
||||
|
||||
|
||||
def test_coerce_dict_to_dataclass() -> None:
|
||||
"""Dicts (from JSON) should be coercible to dataclasses."""
|
||||
|
||||
@dataclass
|
||||
class Point:
|
||||
x: int
|
||||
y: int
|
||||
|
||||
result = try_coerce_to_type({"x": 1, "y": 2}, Point)
|
||||
assert isinstance(result, Point)
|
||||
assert result.x == 1
|
||||
assert result.y == 2
|
||||
|
||||
|
||||
def test_coerce_dict_to_dataclass_bad_keys_returns_original() -> None:
|
||||
"""Dicts with wrong keys should return the original dict, not raise."""
|
||||
|
||||
@dataclass
|
||||
class Point:
|
||||
x: int
|
||||
y: int
|
||||
|
||||
original = {"a": 1, "b": 2}
|
||||
result = try_coerce_to_type(original, Point)
|
||||
assert result is original
|
||||
|
||||
|
||||
def test_coerce_non_concrete_target_returns_original() -> None:
|
||||
"""Union and other non-concrete types should return the original value."""
|
||||
result = try_coerce_to_type(42, int | str)
|
||||
assert result == 42
|
||||
|
||||
result = try_coerce_to_type({"x": 1}, Union[str, int])
|
||||
assert result == {"x": 1}
|
||||
|
||||
|
||||
def test_coerce_unrelated_types_returns_original() -> None:
|
||||
"""Coercion between unrelated types should return the original value."""
|
||||
assert try_coerce_to_type("hello", int) == "hello"
|
||||
assert try_coerce_to_type(3.14, str) == 3.14
|
||||
assert try_coerce_to_type([1, 2], dict) == [1, 2]
|
||||
|
||||
|
||||
def test_coerce_any_returns_original() -> None:
|
||||
"""Any target type should accept any value without coercion."""
|
||||
assert try_coerce_to_type(42, Any) == 42
|
||||
assert try_coerce_to_type({"k": "v"}, Any) == {"k": "v"}
|
||||
|
||||
|
||||
# endregion: try_coerce_to_type tests
|
||||
|
||||
@@ -383,7 +383,7 @@ async def test_workflow_run_stream_from_checkpoint_with_external_storage(
|
||||
try:
|
||||
events: list[WorkflowEvent] = []
|
||||
async for event in workflow_without_checkpointing.run(
|
||||
checkpoint_id=checkpoint_id, checkpoint_storage=storage
|
||||
checkpoint_id=checkpoint_id, checkpoint_storage=storage, stream=True
|
||||
):
|
||||
events.append(event)
|
||||
if len(events) >= 2: # Limit to avoid infinite loops
|
||||
@@ -952,11 +952,11 @@ async def test_workflow_run_parameter_validation(simple_executor: Executor) -> N
|
||||
pass
|
||||
|
||||
# Invalid: none of message or checkpoint_id
|
||||
with pytest.raises(ValueError, match="Must provide either"):
|
||||
with pytest.raises(ValueError, match="Must provide at least one of"):
|
||||
await workflow.run()
|
||||
|
||||
# Invalid: none of message or checkpoint_id (streaming)
|
||||
with pytest.raises(ValueError, match="Must provide either"):
|
||||
with pytest.raises(ValueError, match="Must provide at least one of"):
|
||||
async for _ in workflow.run(stream=True):
|
||||
pass
|
||||
|
||||
@@ -1174,8 +1174,8 @@ async def test_output_executors_filtering_with_fan_in() -> None:
|
||||
assert outputs[0] == 40
|
||||
|
||||
|
||||
async def test_output_executors_filtering_with_send_responses() -> None:
|
||||
"""Test output filtering works correctly with send_responses method."""
|
||||
async def test_output_executors_filtering_with_run_responses() -> None:
|
||||
"""Test output filtering works correctly with run(responses=...) method."""
|
||||
executor = MockExecutorRequestApproval(id="approval_executor")
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).with_output_from([executor]).build()
|
||||
@@ -1189,7 +1189,7 @@ async def test_output_executors_filtering_with_send_responses() -> None:
|
||||
|
||||
# Send approval response
|
||||
responses = {request_events[0].request_id: ApprovalMessage(approved=True)}
|
||||
response_result = await workflow.send_responses(responses)
|
||||
response_result = await workflow.run(responses=responses)
|
||||
outputs = response_result.get_outputs()
|
||||
|
||||
# Output should be yielded since approval_executor is in output_executors
|
||||
@@ -1197,8 +1197,8 @@ async def test_output_executors_filtering_with_send_responses() -> None:
|
||||
assert outputs[0] == 42
|
||||
|
||||
|
||||
async def test_output_executors_filtering_with_send_responses_streaming() -> None:
|
||||
"""Test output filtering works correctly with send_responses_streaming method."""
|
||||
async def test_output_executors_filtering_with_run_responses_streaming() -> None:
|
||||
"""Test output filtering works correctly with run(responses=..., stream=True) method."""
|
||||
executor = MockExecutorRequestApproval(id="approval_executor")
|
||||
|
||||
workflow = WorkflowBuilder().set_start_executor(executor).build()
|
||||
@@ -1218,7 +1218,7 @@ async def test_output_executors_filtering_with_send_responses_streaming() -> Non
|
||||
# Send approval response via streaming
|
||||
responses = {request_events[0].request_id: ApprovalMessage(approved=True)}
|
||||
output_events: list[WorkflowEvent] = []
|
||||
async for event in workflow.send_responses_streaming(responses):
|
||||
async for event in workflow.run(responses=responses, stream=True):
|
||||
if event.type == "output":
|
||||
output_events.append(event)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user