mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Fix streamed workflow agent continuation context by finalizing AgentExecutor streams (#3882)
* Fix streamed workflow agent continuation context by finalizing AgentExecutor streams * Fix stream handling * Fixes * Fix DevUI and tests
This commit is contained in:
committed by
GitHub
Unverified
parent
2203fa0f8b
commit
a276c1295a
@@ -17,6 +17,7 @@ from agent_framework import (
|
||||
BaseContextProvider,
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
FunctionTool,
|
||||
Message,
|
||||
@@ -154,6 +155,111 @@ async def test_chat_client_agent_run_with_session(chat_client_base: SupportsChat
|
||||
assert session.service_session_id == "123"
|
||||
|
||||
|
||||
async def test_chat_client_agent_updates_existing_session_id_non_streaming(
|
||||
chat_client_base: SupportsChatGetResponse,
|
||||
) -> None:
|
||||
chat_client_base.run_responses = [
|
||||
ChatResponse(
|
||||
messages=[Message(role="assistant", contents=[Content.from_text("test response")])],
|
||||
conversation_id="resp_new_123",
|
||||
)
|
||||
]
|
||||
|
||||
agent = Agent(client=chat_client_base)
|
||||
session = agent.get_session(service_session_id="resp_old_123")
|
||||
|
||||
await agent.run("Hello", session=session)
|
||||
assert session.service_session_id == "resp_new_123"
|
||||
|
||||
|
||||
async def test_chat_client_agent_update_session_id_streaming_uses_conversation_id(
|
||||
chat_client_base: SupportsChatGetResponse,
|
||||
) -> None:
|
||||
chat_client_base.streaming_responses = [
|
||||
[
|
||||
ChatResponseUpdate(
|
||||
contents=[Content.from_text("stream part 1")],
|
||||
role="assistant",
|
||||
response_id="resp_stream_123",
|
||||
conversation_id="conv_stream_456",
|
||||
),
|
||||
ChatResponseUpdate(
|
||||
contents=[Content.from_text(" stream part 2")],
|
||||
role="assistant",
|
||||
response_id="resp_stream_123",
|
||||
conversation_id="conv_stream_456",
|
||||
finish_reason="stop",
|
||||
),
|
||||
]
|
||||
]
|
||||
|
||||
agent = Agent(client=chat_client_base)
|
||||
session = agent.create_session()
|
||||
|
||||
stream = agent.run("Hello", session=session, stream=True)
|
||||
async for _ in stream:
|
||||
pass
|
||||
result = await stream.get_final_response()
|
||||
assert result.text == "stream part 1 stream part 2"
|
||||
assert session.service_session_id == "conv_stream_456"
|
||||
|
||||
|
||||
async def test_chat_client_agent_updates_existing_session_id_streaming(
|
||||
chat_client_base: SupportsChatGetResponse,
|
||||
) -> None:
|
||||
chat_client_base.streaming_responses = [
|
||||
[
|
||||
ChatResponseUpdate(
|
||||
contents=[Content.from_text("stream part 1")],
|
||||
role="assistant",
|
||||
response_id="resp_stream_123",
|
||||
conversation_id="resp_new_456",
|
||||
),
|
||||
ChatResponseUpdate(
|
||||
contents=[Content.from_text(" stream part 2")],
|
||||
role="assistant",
|
||||
response_id="resp_stream_123",
|
||||
conversation_id="resp_new_456",
|
||||
finish_reason="stop",
|
||||
),
|
||||
]
|
||||
]
|
||||
|
||||
agent = Agent(client=chat_client_base)
|
||||
session = agent.get_session(service_session_id="resp_old_456")
|
||||
|
||||
stream = agent.run("Hello", session=session, stream=True)
|
||||
async for _ in stream:
|
||||
pass
|
||||
await stream.get_final_response()
|
||||
assert session.service_session_id == "resp_new_456"
|
||||
|
||||
|
||||
async def test_chat_client_agent_update_session_id_streaming_does_not_use_response_id(
|
||||
chat_client_base: SupportsChatGetResponse,
|
||||
) -> None:
|
||||
chat_client_base.streaming_responses = [
|
||||
[
|
||||
ChatResponseUpdate(
|
||||
contents=[Content.from_text("stream response without conversation id")],
|
||||
role="assistant",
|
||||
response_id="resp_only_123",
|
||||
finish_reason="stop",
|
||||
),
|
||||
]
|
||||
]
|
||||
|
||||
agent = Agent(client=chat_client_base)
|
||||
session = agent.create_session()
|
||||
|
||||
stream = agent.run("Hello", session=session, stream=True)
|
||||
async for _ in stream:
|
||||
pass
|
||||
result = await stream.get_final_response()
|
||||
assert result.text == "stream response without conversation id"
|
||||
assert session.service_session_id is None
|
||||
|
||||
|
||||
async def test_chat_client_agent_update_session_messages(client: SupportsChatGetResponse) -> None:
|
||||
agent = Agent(client=client)
|
||||
session = agent.create_session()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
@@ -50,6 +50,57 @@ class _CountingAgent(BaseAgent):
|
||||
return _run()
|
||||
|
||||
|
||||
class _StreamingHookAgent(BaseAgent):
|
||||
"""Agent that exposes whether its streaming result hook was executed."""
|
||||
|
||||
def __init__(self, **kwargs: Any):
|
||||
super().__init__(**kwargs)
|
||||
self.result_hook_called = False
|
||||
|
||||
def run(
|
||||
self,
|
||||
messages: str | Message | list[str] | list[Message] | None = None,
|
||||
*,
|
||||
stream: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
|
||||
if stream:
|
||||
|
||||
async def _stream() -> AsyncIterable[AgentResponseUpdate]:
|
||||
yield AgentResponseUpdate(
|
||||
contents=[Content.from_text(text="hook test")],
|
||||
role="assistant",
|
||||
)
|
||||
|
||||
async def _mark_result_hook_called(response: AgentResponse) -> AgentResponse:
|
||||
self.result_hook_called = True
|
||||
return response
|
||||
|
||||
return ResponseStream(_stream(), finalizer=AgentResponse.from_updates).with_result_hook(
|
||||
_mark_result_hook_called
|
||||
)
|
||||
|
||||
async def _run() -> AgentResponse:
|
||||
return AgentResponse(messages=[Message("assistant", ["hook test"])])
|
||||
|
||||
return _run()
|
||||
|
||||
|
||||
async def test_agent_executor_streaming_finalizes_stream_and_runs_result_hooks() -> None:
|
||||
"""AgentExecutor should call get_final_response() so stream result hooks execute."""
|
||||
agent = _StreamingHookAgent(id="hook_agent", name="HookAgent")
|
||||
executor = AgentExecutor(agent, id="hook_exec")
|
||||
workflow = SequentialBuilder(participants=[executor]).build()
|
||||
|
||||
output_events: list[Any] = []
|
||||
async for event in workflow.run("run hook test", stream=True):
|
||||
if event.type == "output":
|
||||
output_events.append(event)
|
||||
|
||||
assert output_events
|
||||
assert agent.result_hook_called
|
||||
|
||||
|
||||
async def test_agent_executor_checkpoint_stores_and_restores_state() -> None:
|
||||
"""Test that workflow checkpoint stores AgentExecutor's cache and session states and restores them correctly."""
|
||||
storage = InMemoryCheckpointStorage()
|
||||
|
||||
@@ -12,7 +12,7 @@ from agent_framework import (
|
||||
WorkflowRunState,
|
||||
)
|
||||
from agent_framework._workflows._checkpoint_encoding import (
|
||||
_PICKLE_MARKER,
|
||||
_PICKLE_MARKER, # type: ignore
|
||||
encode_checkpoint_value,
|
||||
)
|
||||
from agent_framework._workflows._events import WorkflowEvent
|
||||
|
||||
Reference in New Issue
Block a user