mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Flow custom kwargs to agents via Workflow SharedState (#2894)
* Flow custom kwargs to agents via SharedState * Address Copilot feedback * Improve sample typing * Fix test
This commit is contained in:
committed by
GitHub
Unverified
parent
8fca71e5ad
commit
6adcac2e97
@@ -11,6 +11,7 @@ from .._agents import AgentProtocol, ChatAgent
|
||||
from .._threads import AgentThread
|
||||
from .._types import AgentRunResponse, AgentRunResponseUpdate, ChatMessage
|
||||
from ._checkpoint_encoding import decode_checkpoint_value, encode_checkpoint_value
|
||||
from ._const import WORKFLOW_RUN_KWARGS_KEY
|
||||
from ._conversation_state import encode_chat_messages
|
||||
from ._events import (
|
||||
AgentRunEvent,
|
||||
@@ -309,9 +310,12 @@ class AgentExecutor(Executor):
|
||||
Returns:
|
||||
The complete AgentRunResponse, or None if waiting for user input.
|
||||
"""
|
||||
run_kwargs: dict[str, Any] = await ctx.get_shared_state(WORKFLOW_RUN_KWARGS_KEY)
|
||||
|
||||
response = await self._agent.run(
|
||||
self._cache,
|
||||
thread=self._agent_thread,
|
||||
**run_kwargs,
|
||||
)
|
||||
await ctx.add_event(AgentRunEvent(self.id, response))
|
||||
|
||||
@@ -333,11 +337,14 @@ class AgentExecutor(Executor):
|
||||
Returns:
|
||||
The complete AgentRunResponse, or None if waiting for user input.
|
||||
"""
|
||||
run_kwargs: dict[str, Any] = await ctx.get_shared_state(WORKFLOW_RUN_KWARGS_KEY)
|
||||
|
||||
updates: list[AgentRunResponseUpdate] = []
|
||||
user_input_requests: list[FunctionApprovalRequestContent] = []
|
||||
async for update in self._agent.run_stream(
|
||||
self._cache,
|
||||
thread=self._agent_thread,
|
||||
**run_kwargs,
|
||||
):
|
||||
updates.append(update)
|
||||
await ctx.add_event(AgentRunUpdateEvent(self.id, update))
|
||||
|
||||
@@ -9,6 +9,11 @@ EXECUTOR_STATE_KEY = "_executor_state"
|
||||
# Source identifier for internal workflow messages.
|
||||
INTERNAL_SOURCE_PREFIX = "internal"
|
||||
|
||||
# SharedState key for storing run kwargs that should be passed to agent invocations.
|
||||
# Used by all orchestration patterns (Sequential, Concurrent, GroupChat, Handoff, Magentic)
|
||||
# to pass kwargs from workflow.run_stream() through to agent.run_stream() and @ai_function tools.
|
||||
WORKFLOW_RUN_KWARGS_KEY = "_workflow_run_kwargs"
|
||||
|
||||
|
||||
def INTERNAL_SOURCE_ID(executor_id: str) -> str:
|
||||
"""Generate an internal source ID for a given executor."""
|
||||
|
||||
@@ -25,7 +25,7 @@ from agent_framework import (
|
||||
|
||||
from ._base_group_chat_orchestrator import BaseGroupChatOrchestrator
|
||||
from ._checkpoint import CheckpointStorage, WorkflowCheckpoint
|
||||
from ._const import EXECUTOR_STATE_KEY
|
||||
from ._const import EXECUTOR_STATE_KEY, WORKFLOW_RUN_KWARGS_KEY
|
||||
from ._events import AgentRunUpdateEvent, WorkflowEvent
|
||||
from ._executor import Executor, handler
|
||||
from ._group_chat import (
|
||||
@@ -286,12 +286,14 @@ class _MagenticStartMessage(DictConvertible):
|
||||
"""Internal: A message to start a magentic workflow."""
|
||||
|
||||
messages: list[ChatMessage] = field(default_factory=_new_chat_message_list)
|
||||
run_kwargs: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
messages: str | ChatMessage | Sequence[str] | Sequence[ChatMessage] | None = None,
|
||||
*,
|
||||
task: ChatMessage | None = None,
|
||||
run_kwargs: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
normalized = normalize_messages_input(messages)
|
||||
if task is not None:
|
||||
@@ -299,6 +301,7 @@ class _MagenticStartMessage(DictConvertible):
|
||||
if not normalized:
|
||||
raise ValueError("MagenticStartMessage requires at least one message input.")
|
||||
self.messages: list[ChatMessage] = normalized
|
||||
self.run_kwargs: dict[str, Any] = run_kwargs or {}
|
||||
|
||||
@property
|
||||
def task(self) -> ChatMessage:
|
||||
@@ -1179,6 +1182,10 @@ class MagenticOrchestratorExecutor(BaseGroupChatOrchestrator):
|
||||
return
|
||||
logger.info("Magentic Orchestrator: Received start message")
|
||||
|
||||
# Store run_kwargs in SharedState so agent executors can access them
|
||||
# Always store (even empty dict) so retrieval is deterministic
|
||||
await context.set_shared_state(WORKFLOW_RUN_KWARGS_KEY, message.run_kwargs or {})
|
||||
|
||||
self._context = MagenticContext(
|
||||
task=message.task,
|
||||
participant_descriptions=self._participants,
|
||||
@@ -2004,10 +2011,12 @@ class MagenticAgentExecutor(Executor):
|
||||
"""
|
||||
logger.debug(f"Agent {self._agent_id}: Running with {len(self._chat_history)} messages")
|
||||
|
||||
run_kwargs: dict[str, Any] = await ctx.get_shared_state(WORKFLOW_RUN_KWARGS_KEY)
|
||||
|
||||
updates: list[AgentRunResponseUpdate] = []
|
||||
# The wrapped participant is guaranteed to be an BaseAgent when this is called.
|
||||
agent = cast("AgentProtocol", self._agent)
|
||||
async for update in agent.run_stream(messages=self._chat_history): # type: ignore[attr-defined]
|
||||
async for update in agent.run_stream(messages=self._chat_history, **run_kwargs): # type: ignore[attr-defined]
|
||||
updates.append(update)
|
||||
await self._emit_agent_delta_event(ctx, update)
|
||||
|
||||
@@ -2604,38 +2613,48 @@ class MagenticWorkflow:
|
||||
"""Access the underlying workflow."""
|
||||
return self._workflow
|
||||
|
||||
async def run_streaming_with_string(self, task_text: str) -> AsyncIterable[WorkflowEvent]:
|
||||
async def run_streaming_with_string(self, task_text: str, **kwargs: Any) -> AsyncIterable[WorkflowEvent]:
|
||||
"""Run the workflow with a task string.
|
||||
|
||||
Args:
|
||||
task_text: The task description as a string.
|
||||
**kwargs: Additional keyword arguments to pass through to agent invocations.
|
||||
These kwargs will be available in @ai_function tools via **kwargs.
|
||||
|
||||
Yields:
|
||||
WorkflowEvent: The events generated during the workflow execution.
|
||||
"""
|
||||
start_message = _MagenticStartMessage.from_string(task_text)
|
||||
start_message.run_kwargs = kwargs
|
||||
async for event in self._workflow.run_stream(start_message):
|
||||
yield event
|
||||
|
||||
async def run_streaming_with_message(self, task_message: ChatMessage) -> AsyncIterable[WorkflowEvent]:
|
||||
async def run_streaming_with_message(
|
||||
self, task_message: ChatMessage, **kwargs: Any
|
||||
) -> AsyncIterable[WorkflowEvent]:
|
||||
"""Run the workflow with a ChatMessage.
|
||||
|
||||
Args:
|
||||
task_message: The task as a ChatMessage.
|
||||
**kwargs: Additional keyword arguments to pass through to agent invocations.
|
||||
These kwargs will be available in @ai_function tools via **kwargs.
|
||||
|
||||
Yields:
|
||||
WorkflowEvent: The events generated during the workflow execution.
|
||||
"""
|
||||
start_message = _MagenticStartMessage(task_message)
|
||||
start_message = _MagenticStartMessage(task_message, run_kwargs=kwargs)
|
||||
async for event in self._workflow.run_stream(start_message):
|
||||
yield event
|
||||
|
||||
async def run_stream(self, message: Any | None = None) -> AsyncIterable[WorkflowEvent]:
|
||||
async def run_stream(self, message: Any | None = None, **kwargs: Any) -> AsyncIterable[WorkflowEvent]:
|
||||
"""Run the workflow with either a message object or the preset task string.
|
||||
|
||||
Args:
|
||||
message: The message to send. If None and task_text was provided during construction,
|
||||
uses the preset task string.
|
||||
**kwargs: Additional keyword arguments to pass through to agent invocations.
|
||||
These kwargs will be available in @ai_function tools via **kwargs.
|
||||
Example: workflow.run_stream("task", user_id="123", custom_data={...})
|
||||
|
||||
Yields:
|
||||
WorkflowEvent: The events generated during the workflow execution.
|
||||
@@ -2643,13 +2662,19 @@ class MagenticWorkflow:
|
||||
if message is None:
|
||||
if self._task_text is None:
|
||||
raise ValueError("No message provided and no preset task text available")
|
||||
message = _MagenticStartMessage.from_string(self._task_text)
|
||||
start_message = _MagenticStartMessage.from_string(self._task_text)
|
||||
elif isinstance(message, str):
|
||||
message = _MagenticStartMessage.from_string(message)
|
||||
start_message = _MagenticStartMessage.from_string(message)
|
||||
elif isinstance(message, (ChatMessage, list)):
|
||||
message = _MagenticStartMessage(message) # type: ignore[arg-type]
|
||||
start_message = _MagenticStartMessage(message) # type: ignore[arg-type]
|
||||
else:
|
||||
start_message = message
|
||||
|
||||
async for event in self._workflow.run_stream(message):
|
||||
# Attach kwargs to the start message
|
||||
if isinstance(start_message, _MagenticStartMessage):
|
||||
start_message.run_kwargs = kwargs
|
||||
|
||||
async for event in self._workflow.run_stream(start_message):
|
||||
yield event
|
||||
|
||||
async def _validate_checkpoint_participants(
|
||||
@@ -2730,46 +2755,49 @@ class MagenticWorkflow:
|
||||
f"Missing names: {missing}; unexpected names: {unexpected}."
|
||||
)
|
||||
|
||||
async def run_with_string(self, task_text: str) -> WorkflowRunResult:
|
||||
async def run_with_string(self, task_text: str, **kwargs: Any) -> WorkflowRunResult:
|
||||
"""Run the workflow with a task string and return all events.
|
||||
|
||||
Args:
|
||||
task_text: The task description as a string.
|
||||
**kwargs: Additional keyword arguments to pass through to agent invocations.
|
||||
|
||||
Returns:
|
||||
WorkflowRunResult: All events generated during the workflow execution.
|
||||
"""
|
||||
events: list[WorkflowEvent] = []
|
||||
async for event in self.run_streaming_with_string(task_text):
|
||||
async for event in self.run_streaming_with_string(task_text, **kwargs):
|
||||
events.append(event)
|
||||
return WorkflowRunResult(events)
|
||||
|
||||
async def run_with_message(self, task_message: ChatMessage) -> WorkflowRunResult:
|
||||
async def run_with_message(self, task_message: ChatMessage, **kwargs: Any) -> WorkflowRunResult:
|
||||
"""Run the workflow with a ChatMessage and return all events.
|
||||
|
||||
Args:
|
||||
task_message: The task as a ChatMessage.
|
||||
**kwargs: Additional keyword arguments to pass through to agent invocations.
|
||||
|
||||
Returns:
|
||||
WorkflowRunResult: All events generated during the workflow execution.
|
||||
"""
|
||||
events: list[WorkflowEvent] = []
|
||||
async for event in self.run_streaming_with_message(task_message):
|
||||
async for event in self.run_streaming_with_message(task_message, **kwargs):
|
||||
events.append(event)
|
||||
return WorkflowRunResult(events)
|
||||
|
||||
async def run(self, message: Any | None = None) -> WorkflowRunResult:
|
||||
async def run(self, message: Any | None = None, **kwargs: Any) -> WorkflowRunResult:
|
||||
"""Run the workflow and return all events.
|
||||
|
||||
Args:
|
||||
message: The message to send. If None and task_text was provided during construction,
|
||||
uses the preset task string.
|
||||
**kwargs: Additional keyword arguments to pass through to agent invocations.
|
||||
|
||||
Returns:
|
||||
WorkflowRunResult: All events generated during the workflow execution.
|
||||
"""
|
||||
events: list[WorkflowEvent] = []
|
||||
async for event in self.run_stream(message):
|
||||
async for event in self.run_stream(message, **kwargs):
|
||||
events.append(event)
|
||||
return WorkflowRunResult(events)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from typing import Any
|
||||
from ..observability import OtelAttr, capture_exception, create_workflow_span
|
||||
from ._agent import WorkflowAgent
|
||||
from ._checkpoint import CheckpointStorage
|
||||
from ._const import DEFAULT_MAX_ITERATIONS
|
||||
from ._const import DEFAULT_MAX_ITERATIONS, WORKFLOW_RUN_KWARGS_KEY
|
||||
from ._edge import (
|
||||
EdgeGroup,
|
||||
FanOutEdgeGroup,
|
||||
@@ -291,6 +291,7 @@ class Workflow(DictConvertible):
|
||||
initial_executor_fn: Callable[[], Awaitable[None]] | None = None,
|
||||
reset_context: bool = True,
|
||||
streaming: bool = False,
|
||||
run_kwargs: dict[str, Any] | None = None,
|
||||
) -> AsyncIterable[WorkflowEvent]:
|
||||
"""Private method to run workflow with proper tracing.
|
||||
|
||||
@@ -301,6 +302,7 @@ class Workflow(DictConvertible):
|
||||
initial_executor_fn: Optional function to execute initial executor
|
||||
reset_context: Whether to reset the context for a new run
|
||||
streaming: Whether to enable streaming mode for agents
|
||||
run_kwargs: Optional kwargs to store in SharedState for agent invocations
|
||||
|
||||
Yields:
|
||||
WorkflowEvent: The events generated during the workflow execution.
|
||||
@@ -335,6 +337,10 @@ class Workflow(DictConvertible):
|
||||
self._runner.context.reset_for_new_run()
|
||||
await self._shared_state.clear()
|
||||
|
||||
# Store run kwargs in SharedState so executors can access them
|
||||
# Always store (even empty dict) so retrieval is deterministic
|
||||
await self._shared_state.set(WORKFLOW_RUN_KWARGS_KEY, run_kwargs or {})
|
||||
|
||||
# Set streaming mode after reset
|
||||
self._runner_context.set_streaming(streaming)
|
||||
|
||||
@@ -442,6 +448,7 @@ class Workflow(DictConvertible):
|
||||
*,
|
||||
checkpoint_id: str | None = None,
|
||||
checkpoint_storage: CheckpointStorage | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[WorkflowEvent]:
|
||||
"""Run the workflow and stream events.
|
||||
|
||||
@@ -457,6 +464,9 @@ class Workflow(DictConvertible):
|
||||
- With checkpoint_id: Used to load and restore the specified checkpoint
|
||||
- Without checkpoint_id: Enables checkpointing for this run, overriding
|
||||
build-time configuration
|
||||
**kwargs: Additional keyword arguments to pass through to agent invocations.
|
||||
These are stored in SharedState and accessible in @ai_function tools
|
||||
via the **kwargs parameter.
|
||||
|
||||
Yields:
|
||||
WorkflowEvent: Events generated during workflow execution.
|
||||
@@ -475,6 +485,17 @@ class Workflow(DictConvertible):
|
||||
async for event in workflow.run_stream("start message"):
|
||||
process(event)
|
||||
|
||||
With custom context for ai_functions:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async for event in workflow.run_stream(
|
||||
"analyze data",
|
||||
custom_data={"endpoint": "https://api.example.com"},
|
||||
user_token={"user": "alice"},
|
||||
):
|
||||
process(event)
|
||||
|
||||
Enable checkpointing at runtime:
|
||||
|
||||
.. code-block:: python
|
||||
@@ -524,6 +545,7 @@ class Workflow(DictConvertible):
|
||||
),
|
||||
reset_context=reset_context,
|
||||
streaming=True,
|
||||
run_kwargs=kwargs if kwargs else None,
|
||||
):
|
||||
yield event
|
||||
finally:
|
||||
@@ -559,6 +581,7 @@ class Workflow(DictConvertible):
|
||||
checkpoint_id: str | None = None,
|
||||
checkpoint_storage: CheckpointStorage | None = None,
|
||||
include_status_events: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> WorkflowRunResult:
|
||||
"""Run the workflow to completion and return all events.
|
||||
|
||||
@@ -575,6 +598,9 @@ class Workflow(DictConvertible):
|
||||
- Without checkpoint_id: Enables checkpointing for this run, overriding
|
||||
build-time configuration
|
||||
include_status_events: Whether to include WorkflowStatusEvent instances in the result list.
|
||||
**kwargs: Additional keyword arguments to pass through to agent invocations.
|
||||
These are stored in SharedState and accessible in @ai_function tools
|
||||
via the **kwargs parameter.
|
||||
|
||||
Returns:
|
||||
A WorkflowRunResult instance containing events generated during workflow execution.
|
||||
@@ -593,6 +619,16 @@ class Workflow(DictConvertible):
|
||||
result = await workflow.run("start message")
|
||||
outputs = result.get_outputs()
|
||||
|
||||
With custom context for ai_functions:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
result = await workflow.run(
|
||||
"analyze data",
|
||||
custom_data={"endpoint": "https://api.example.com"},
|
||||
user_token={"user": "alice"},
|
||||
)
|
||||
|
||||
Enable checkpointing at runtime:
|
||||
|
||||
.. code-block:: python
|
||||
@@ -637,6 +673,7 @@ class Workflow(DictConvertible):
|
||||
self._execute_with_message_or_checkpoint, message, checkpoint_id, checkpoint_storage
|
||||
),
|
||||
reset_context=reset_context,
|
||||
run_kwargs=kwargs if kwargs else None,
|
||||
)
|
||||
]
|
||||
finally:
|
||||
|
||||
@@ -876,3 +876,204 @@ def test_magentic_builder_does_not_have_human_input_hook():
|
||||
"MagenticBuilder should not have with_human_input_hook - "
|
||||
"use with_plan_review() or with_human_input_on_stall() instead"
|
||||
)
|
||||
|
||||
|
||||
# region Message Deduplication Tests
|
||||
|
||||
|
||||
async def test_magentic_no_duplicate_messages_with_conversation_history():
|
||||
"""Test that passing list[ChatMessage] does not create duplicate messages in chat_history.
|
||||
|
||||
When a frontend passes conversation history as list[ChatMessage], the last message
|
||||
(task) should not be duplicated in the orchestrator's chat_history.
|
||||
"""
|
||||
manager = FakeManager(max_round_count=10)
|
||||
manager.satisfied_after_signoff = True # Complete immediately after first agent response
|
||||
|
||||
wf = MagenticBuilder().participants(agentA=_DummyExec("agentA")).with_standard_manager(manager).build()
|
||||
|
||||
# Simulate frontend passing conversation history
|
||||
conversation: list[ChatMessage] = [
|
||||
ChatMessage(role=Role.USER, text="previous question"),
|
||||
ChatMessage(role=Role.ASSISTANT, text="previous answer"),
|
||||
ChatMessage(role=Role.USER, text="current task"),
|
||||
]
|
||||
|
||||
# Get orchestrator to inspect chat_history after run
|
||||
orchestrator = None
|
||||
for executor in wf.executors.values():
|
||||
if isinstance(executor, MagenticOrchestratorExecutor):
|
||||
orchestrator = executor
|
||||
break
|
||||
|
||||
events: list[WorkflowEvent] = []
|
||||
async for event in wf.run_stream(conversation):
|
||||
events.append(event)
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state in (
|
||||
WorkflowRunState.IDLE,
|
||||
WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,
|
||||
):
|
||||
break
|
||||
|
||||
assert orchestrator is not None
|
||||
assert orchestrator._context is not None # type: ignore[reportPrivateUsage]
|
||||
|
||||
# Count occurrences of each message text in chat_history
|
||||
history = orchestrator._context.chat_history # type: ignore[reportPrivateUsage]
|
||||
user_task_count = sum(1 for msg in history if msg.text == "current task")
|
||||
prev_question_count = sum(1 for msg in history if msg.text == "previous question")
|
||||
prev_answer_count = sum(1 for msg in history if msg.text == "previous answer")
|
||||
|
||||
# Each input message should appear exactly once (no duplicates)
|
||||
assert prev_question_count == 1, f"Expected 1 'previous question', got {prev_question_count}"
|
||||
assert prev_answer_count == 1, f"Expected 1 'previous answer', got {prev_answer_count}"
|
||||
assert user_task_count == 1, f"Expected 1 'current task', got {user_task_count}"
|
||||
|
||||
|
||||
async def test_magentic_agent_executor_no_duplicate_messages_on_broadcast():
|
||||
"""Test that MagenticAgentExecutor does not duplicate messages from broadcasts.
|
||||
|
||||
When the orchestrator broadcasts the task ledger to all agents, each agent
|
||||
should receive it exactly once, not multiple times.
|
||||
"""
|
||||
backing_executor = _DummyExec("backing")
|
||||
agent_exec = MagenticAgentExecutor(backing_executor, "agentA")
|
||||
|
||||
# Simulate orchestrator sending a broadcast message
|
||||
broadcast_msg = ChatMessage(
|
||||
role=Role.ASSISTANT,
|
||||
text="Task ledger content",
|
||||
author_name="magentic_manager",
|
||||
)
|
||||
|
||||
# Simulate the same message being received multiple times (e.g., from checkpoint restore + live)
|
||||
from agent_framework._workflows._magentic import _MagenticResponseMessage
|
||||
|
||||
response1 = _MagenticResponseMessage(body=broadcast_msg, broadcast=True)
|
||||
response2 = _MagenticResponseMessage(body=broadcast_msg, broadcast=True)
|
||||
|
||||
# Create a mock context
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
mock_context = MagicMock()
|
||||
mock_context.send_message = AsyncMock()
|
||||
|
||||
# Call the handler twice with the same message
|
||||
await agent_exec.handle_response_message(response1, mock_context) # type: ignore[arg-type]
|
||||
await agent_exec.handle_response_message(response2, mock_context) # type: ignore[arg-type]
|
||||
|
||||
# Count how many times the broadcast message appears
|
||||
history = agent_exec._chat_history # type: ignore[reportPrivateUsage]
|
||||
broadcast_count = sum(1 for msg in history if msg.text == "Task ledger content")
|
||||
|
||||
# Each broadcast should be recorded (this is expected behavior - broadcasts are additive)
|
||||
# The test documents current behavior. If dedup is needed, this assertion would change.
|
||||
assert broadcast_count == 2, (
|
||||
f"Expected 2 broadcasts (current behavior is additive), got {broadcast_count}. "
|
||||
"If deduplication is required, update the handler logic."
|
||||
)
|
||||
|
||||
|
||||
async def test_magentic_context_no_duplicate_on_reset():
|
||||
"""Test that MagenticContext.reset() clears chat_history without leaving duplicates."""
|
||||
ctx = MagenticContext(
|
||||
task=ChatMessage(role=Role.USER, text="task"),
|
||||
participant_descriptions={"Alice": "Researcher"},
|
||||
)
|
||||
|
||||
# Add some history
|
||||
ctx.chat_history.append(ChatMessage(role=Role.ASSISTANT, text="response1"))
|
||||
ctx.chat_history.append(ChatMessage(role=Role.ASSISTANT, text="response2"))
|
||||
assert len(ctx.chat_history) == 2
|
||||
|
||||
# Reset
|
||||
ctx.reset()
|
||||
|
||||
# Verify clean slate
|
||||
assert len(ctx.chat_history) == 0, "chat_history should be empty after reset"
|
||||
|
||||
# Add new history
|
||||
ctx.chat_history.append(ChatMessage(role=Role.ASSISTANT, text="new_response"))
|
||||
assert len(ctx.chat_history) == 1, "Should have exactly 1 message after adding to reset context"
|
||||
|
||||
|
||||
async def test_magentic_start_message_messages_list_integrity():
|
||||
"""Test that _MagenticStartMessage preserves message list without internal duplication."""
|
||||
conversation: list[ChatMessage] = [
|
||||
ChatMessage(role=Role.USER, text="msg1"),
|
||||
ChatMessage(role=Role.ASSISTANT, text="msg2"),
|
||||
ChatMessage(role=Role.USER, text="msg3"),
|
||||
]
|
||||
|
||||
start_msg = _MagenticStartMessage(conversation)
|
||||
|
||||
# Verify messages list is preserved
|
||||
assert len(start_msg.messages) == 3, f"Expected 3 messages, got {len(start_msg.messages)}"
|
||||
|
||||
# Verify task is the last message (not a copy)
|
||||
assert start_msg.task is start_msg.messages[-1], "task should be the same object as messages[-1]"
|
||||
assert start_msg.task.text == "msg3"
|
||||
|
||||
|
||||
async def test_magentic_checkpoint_restore_no_duplicate_history():
|
||||
"""Test that checkpoint restore does not create duplicate messages in chat_history."""
|
||||
manager = FakeManager(max_round_count=10)
|
||||
storage = InMemoryCheckpointStorage()
|
||||
|
||||
wf = (
|
||||
MagenticBuilder()
|
||||
.participants(agentA=_DummyExec("agentA"))
|
||||
.with_standard_manager(manager)
|
||||
.with_checkpointing(storage)
|
||||
.build()
|
||||
)
|
||||
|
||||
# Run with conversation history to create initial checkpoint
|
||||
conversation: list[ChatMessage] = [
|
||||
ChatMessage(role=Role.USER, text="history_msg"),
|
||||
ChatMessage(role=Role.USER, text="task_msg"),
|
||||
]
|
||||
|
||||
async for event in wf.run_stream(conversation):
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state in (
|
||||
WorkflowRunState.IDLE,
|
||||
WorkflowRunState.IDLE_WITH_PENDING_REQUESTS,
|
||||
):
|
||||
break
|
||||
|
||||
# Get checkpoint
|
||||
checkpoints = await storage.list_checkpoints()
|
||||
assert len(checkpoints) > 0, "Should have created checkpoints"
|
||||
|
||||
latest_checkpoint = checkpoints[-1]
|
||||
|
||||
# Load checkpoint and verify no duplicates in shared state
|
||||
checkpoint_data = await storage.load_checkpoint(latest_checkpoint.checkpoint_id)
|
||||
assert checkpoint_data is not None
|
||||
|
||||
# Check the magentic_context in the checkpoint
|
||||
for _, executor_state in checkpoint_data.metadata.items():
|
||||
if isinstance(executor_state, dict) and "magentic_context" in executor_state:
|
||||
ctx_data = executor_state["magentic_context"]
|
||||
chat_history = ctx_data.get("chat_history", [])
|
||||
|
||||
# Count unique messages by text
|
||||
texts = [
|
||||
msg.get("text") or (msg.get("contents", [{}])[0].get("text") if msg.get("contents") else None)
|
||||
for msg in chat_history
|
||||
]
|
||||
text_counts: dict[str, int] = {}
|
||||
for text in texts:
|
||||
if text:
|
||||
text_counts[text] = text_counts.get(text, 0) + 1
|
||||
|
||||
# Input messages should not be duplicated
|
||||
assert text_counts.get("history_msg", 0) <= 1, (
|
||||
f"'history_msg' appears {text_counts.get('history_msg', 0)} times in checkpoint - expected <= 1"
|
||||
)
|
||||
assert text_counts.get("task_msg", 0) <= 1, (
|
||||
f"'task_msg' appears {text_counts.get('task_msg', 0)} times in checkpoint - expected <= 1"
|
||||
)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from collections.abc import AsyncIterable
|
||||
from typing import Annotated, Any
|
||||
|
||||
from agent_framework import (
|
||||
AgentRunResponse,
|
||||
AgentRunResponseUpdate,
|
||||
AgentThread,
|
||||
BaseAgent,
|
||||
ChatMessage,
|
||||
ConcurrentBuilder,
|
||||
GroupChatBuilder,
|
||||
GroupChatStateSnapshot,
|
||||
HandoffBuilder,
|
||||
Role,
|
||||
SequentialBuilder,
|
||||
TextContent,
|
||||
WorkflowRunState,
|
||||
WorkflowStatusEvent,
|
||||
ai_function,
|
||||
)
|
||||
from agent_framework._workflows._const import WORKFLOW_RUN_KWARGS_KEY
|
||||
|
||||
# Track kwargs received by tools during test execution
|
||||
_received_kwargs: list[dict[str, Any]] = []
|
||||
|
||||
|
||||
def _reset_received_kwargs() -> None:
|
||||
"""Reset the kwargs tracker before each test."""
|
||||
_received_kwargs.clear()
|
||||
|
||||
|
||||
@ai_function
|
||||
def tool_with_kwargs(
|
||||
action: Annotated[str, "The action to perform"],
|
||||
**kwargs: Any,
|
||||
) -> str:
|
||||
"""A test tool that captures kwargs for verification."""
|
||||
_received_kwargs.append(dict(kwargs))
|
||||
custom_data = kwargs.get("custom_data", {})
|
||||
user_token = kwargs.get("user_token", {})
|
||||
return f"Executed {action} with custom_data={custom_data}, user={user_token.get('user_name', 'unknown')}"
|
||||
|
||||
|
||||
class _KwargsCapturingAgent(BaseAgent):
|
||||
"""Test agent that captures kwargs passed to run/run_stream."""
|
||||
|
||||
captured_kwargs: list[dict[str, Any]]
|
||||
|
||||
def __init__(self, name: str = "test_agent") -> None:
|
||||
super().__init__(name=name, description="Test agent for kwargs capture")
|
||||
self.captured_kwargs = []
|
||||
|
||||
async def run(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
|
||||
*,
|
||||
thread: AgentThread | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AgentRunResponse:
|
||||
self.captured_kwargs.append(dict(kwargs))
|
||||
return AgentRunResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=f"{self.display_name} response")])
|
||||
|
||||
async def run_stream(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
|
||||
*,
|
||||
thread: AgentThread | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[AgentRunResponseUpdate]:
|
||||
self.captured_kwargs.append(dict(kwargs))
|
||||
yield AgentRunResponseUpdate(contents=[TextContent(text=f"{self.display_name} response")])
|
||||
|
||||
|
||||
class _EchoAgent(BaseAgent):
|
||||
"""Simple agent that echoes back for workflow completion."""
|
||||
|
||||
async def run(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
|
||||
*,
|
||||
thread: AgentThread | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AgentRunResponse:
|
||||
return AgentRunResponse(messages=[ChatMessage(role=Role.ASSISTANT, text=f"{self.display_name} reply")])
|
||||
|
||||
async def run_stream(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
|
||||
*,
|
||||
thread: AgentThread | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[AgentRunResponseUpdate]:
|
||||
yield AgentRunResponseUpdate(contents=[TextContent(text=f"{self.display_name} reply")])
|
||||
|
||||
|
||||
# region Sequential Builder Tests
|
||||
|
||||
|
||||
async def test_sequential_kwargs_flow_to_agent() -> None:
|
||||
"""Test that kwargs passed to SequentialBuilder workflow flow through to agent."""
|
||||
agent = _KwargsCapturingAgent(name="seq_agent")
|
||||
workflow = SequentialBuilder().participants([agent]).build()
|
||||
|
||||
custom_data = {"endpoint": "https://api.example.com", "version": "v1"}
|
||||
user_token = {"user_name": "alice", "access_level": "admin"}
|
||||
|
||||
async for event in workflow.run_stream(
|
||||
"test message",
|
||||
custom_data=custom_data,
|
||||
user_token=user_token,
|
||||
):
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
|
||||
break
|
||||
|
||||
# Verify agent received kwargs
|
||||
assert len(agent.captured_kwargs) >= 1, "Agent should have been invoked at least once"
|
||||
received = agent.captured_kwargs[0]
|
||||
assert "custom_data" in received, "Agent should receive custom_data kwarg"
|
||||
assert "user_token" in received, "Agent should receive user_token kwarg"
|
||||
assert received["custom_data"] == custom_data
|
||||
assert received["user_token"] == user_token
|
||||
|
||||
|
||||
async def test_sequential_kwargs_flow_to_multiple_agents() -> None:
|
||||
"""Test that kwargs flow to all agents in a sequential workflow."""
|
||||
agent1 = _KwargsCapturingAgent(name="agent1")
|
||||
agent2 = _KwargsCapturingAgent(name="agent2")
|
||||
workflow = SequentialBuilder().participants([agent1, agent2]).build()
|
||||
|
||||
custom_data = {"key": "value"}
|
||||
|
||||
async for event in workflow.run_stream("test", custom_data=custom_data):
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
|
||||
break
|
||||
|
||||
# Both agents should have received kwargs
|
||||
assert len(agent1.captured_kwargs) >= 1, "First agent should be invoked"
|
||||
assert len(agent2.captured_kwargs) >= 1, "Second agent should be invoked"
|
||||
assert agent1.captured_kwargs[0].get("custom_data") == custom_data
|
||||
assert agent2.captured_kwargs[0].get("custom_data") == custom_data
|
||||
|
||||
|
||||
async def test_sequential_run_kwargs_flow() -> None:
|
||||
"""Test that kwargs flow through workflow.run() (non-streaming)."""
|
||||
agent = _KwargsCapturingAgent(name="run_agent")
|
||||
workflow = SequentialBuilder().participants([agent]).build()
|
||||
|
||||
_ = await workflow.run("test message", custom_data={"test": True})
|
||||
|
||||
assert len(agent.captured_kwargs) >= 1
|
||||
assert agent.captured_kwargs[0].get("custom_data") == {"test": True}
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Concurrent Builder Tests
|
||||
|
||||
|
||||
async def test_concurrent_kwargs_flow_to_agents() -> None:
|
||||
"""Test that kwargs flow to all agents in a concurrent workflow."""
|
||||
agent1 = _KwargsCapturingAgent(name="concurrent1")
|
||||
agent2 = _KwargsCapturingAgent(name="concurrent2")
|
||||
workflow = ConcurrentBuilder().participants([agent1, agent2]).build()
|
||||
|
||||
custom_data = {"batch_id": "123"}
|
||||
user_token = {"user_name": "bob"}
|
||||
|
||||
async for event in workflow.run_stream(
|
||||
"concurrent test",
|
||||
custom_data=custom_data,
|
||||
user_token=user_token,
|
||||
):
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
|
||||
break
|
||||
|
||||
# Both agents should have received kwargs
|
||||
assert len(agent1.captured_kwargs) >= 1, "First concurrent agent should be invoked"
|
||||
assert len(agent2.captured_kwargs) >= 1, "Second concurrent agent should be invoked"
|
||||
|
||||
for agent in [agent1, agent2]:
|
||||
received = agent.captured_kwargs[0]
|
||||
assert received.get("custom_data") == custom_data
|
||||
assert received.get("user_token") == user_token
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region GroupChat Builder Tests
|
||||
|
||||
|
||||
async def test_groupchat_kwargs_flow_to_agents() -> None:
|
||||
"""Test that kwargs flow to agents in a group chat workflow."""
|
||||
agent1 = _KwargsCapturingAgent(name="chat1")
|
||||
agent2 = _KwargsCapturingAgent(name="chat2")
|
||||
|
||||
# Simple selector that takes GroupChatStateSnapshot
|
||||
turn_count = 0
|
||||
|
||||
def simple_selector(state: GroupChatStateSnapshot) -> str | None:
|
||||
nonlocal turn_count
|
||||
turn_count += 1
|
||||
if turn_count > 2: # Stop after 2 turns
|
||||
return None
|
||||
# state is a Mapping - access via dict syntax
|
||||
names = list(state["participants"].keys())
|
||||
return names[(turn_count - 1) % len(names)]
|
||||
|
||||
workflow = (
|
||||
GroupChatBuilder().participants(chat1=agent1, chat2=agent2).set_select_speakers_func(simple_selector).build()
|
||||
)
|
||||
|
||||
custom_data = {"session_id": "group123"}
|
||||
|
||||
async for event in workflow.run_stream("group chat test", custom_data=custom_data):
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
|
||||
break
|
||||
|
||||
# At least one agent should have received kwargs
|
||||
all_kwargs = agent1.captured_kwargs + agent2.captured_kwargs
|
||||
assert len(all_kwargs) >= 1, "At least one agent should be invoked in group chat"
|
||||
|
||||
for received in all_kwargs:
|
||||
assert received.get("custom_data") == custom_data
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region SharedState Verification Tests
|
||||
|
||||
|
||||
async def test_kwargs_stored_in_shared_state() -> None:
|
||||
"""Test that kwargs are stored in SharedState with the correct key."""
|
||||
from agent_framework import Executor, WorkflowContext, handler
|
||||
|
||||
stored_kwargs: dict[str, Any] | None = None
|
||||
|
||||
class _SharedStateInspector(Executor):
|
||||
@handler
|
||||
async def inspect(self, msgs: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None:
|
||||
nonlocal stored_kwargs
|
||||
stored_kwargs = await ctx.get_shared_state(WORKFLOW_RUN_KWARGS_KEY)
|
||||
await ctx.send_message(msgs)
|
||||
|
||||
inspector = _SharedStateInspector(id="inspector")
|
||||
workflow = SequentialBuilder().participants([inspector]).build()
|
||||
|
||||
async for event in workflow.run_stream("test", my_kwarg="my_value", another=123):
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
|
||||
break
|
||||
|
||||
assert stored_kwargs is not None, "kwargs should be stored in SharedState"
|
||||
assert stored_kwargs.get("my_kwarg") == "my_value"
|
||||
assert stored_kwargs.get("another") == 123
|
||||
|
||||
|
||||
async def test_empty_kwargs_stored_as_empty_dict() -> None:
|
||||
"""Test that empty kwargs are stored as empty dict in SharedState."""
|
||||
from agent_framework import Executor, WorkflowContext, handler
|
||||
|
||||
stored_kwargs: Any = "NOT_CHECKED"
|
||||
|
||||
class _SharedStateChecker(Executor):
|
||||
@handler
|
||||
async def check(self, msgs: list[ChatMessage], ctx: WorkflowContext[list[ChatMessage]]) -> None:
|
||||
nonlocal stored_kwargs
|
||||
stored_kwargs = await ctx.get_shared_state(WORKFLOW_RUN_KWARGS_KEY)
|
||||
await ctx.send_message(msgs)
|
||||
|
||||
checker = _SharedStateChecker(id="checker")
|
||||
workflow = SequentialBuilder().participants([checker]).build()
|
||||
|
||||
# Run without any kwargs
|
||||
async for event in workflow.run_stream("test"):
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
|
||||
break
|
||||
|
||||
# SharedState should have empty dict when no kwargs provided
|
||||
assert stored_kwargs == {}, f"Expected empty dict, got: {stored_kwargs}"
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Edge Cases
|
||||
|
||||
|
||||
async def test_kwargs_with_none_values() -> None:
|
||||
"""Test that kwargs with None values are passed through correctly."""
|
||||
agent = _KwargsCapturingAgent(name="none_test")
|
||||
workflow = SequentialBuilder().participants([agent]).build()
|
||||
|
||||
async for event in workflow.run_stream("test", optional_param=None, other_param="value"):
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
|
||||
break
|
||||
|
||||
assert len(agent.captured_kwargs) >= 1
|
||||
received = agent.captured_kwargs[0]
|
||||
assert "optional_param" in received
|
||||
assert received["optional_param"] is None
|
||||
assert received["other_param"] == "value"
|
||||
|
||||
|
||||
async def test_kwargs_with_complex_nested_data() -> None:
|
||||
"""Test that complex nested data structures flow through correctly."""
|
||||
agent = _KwargsCapturingAgent(name="nested_test")
|
||||
workflow = SequentialBuilder().participants([agent]).build()
|
||||
|
||||
complex_data = {
|
||||
"level1": {
|
||||
"level2": {
|
||||
"level3": ["a", "b", "c"],
|
||||
"number": 42,
|
||||
},
|
||||
"list": [1, 2, {"nested": True}],
|
||||
},
|
||||
"tuple_like": [1, 2, 3],
|
||||
}
|
||||
|
||||
async for event in workflow.run_stream("test", complex_data=complex_data):
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
|
||||
break
|
||||
|
||||
assert len(agent.captured_kwargs) >= 1
|
||||
received = agent.captured_kwargs[0]
|
||||
assert received.get("complex_data") == complex_data
|
||||
|
||||
|
||||
async def test_kwargs_preserved_across_workflow_reruns() -> None:
|
||||
"""Test that kwargs are correctly isolated between workflow runs."""
|
||||
agent = _KwargsCapturingAgent(name="rerun_test")
|
||||
|
||||
# Build separate workflows for each run to avoid "already running" error
|
||||
workflow1 = SequentialBuilder().participants([agent]).build()
|
||||
workflow2 = SequentialBuilder().participants([agent]).build()
|
||||
|
||||
# First run
|
||||
async for event in workflow1.run_stream("run1", run_id="first"):
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
|
||||
break
|
||||
|
||||
# Second run with different kwargs (using fresh workflow)
|
||||
async for event in workflow2.run_stream("run2", run_id="second"):
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
|
||||
break
|
||||
|
||||
assert len(agent.captured_kwargs) >= 2
|
||||
assert agent.captured_kwargs[0].get("run_id") == "first"
|
||||
assert agent.captured_kwargs[1].get("run_id") == "second"
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Handoff Builder Tests
|
||||
|
||||
|
||||
async def test_handoff_kwargs_flow_to_agents() -> None:
|
||||
"""Test that kwargs flow to agents in a handoff workflow."""
|
||||
agent1 = _KwargsCapturingAgent(name="coordinator")
|
||||
agent2 = _KwargsCapturingAgent(name="specialist")
|
||||
|
||||
workflow = (
|
||||
HandoffBuilder()
|
||||
.participants([agent1, agent2])
|
||||
.set_coordinator(agent1)
|
||||
.with_interaction_mode("autonomous")
|
||||
.build()
|
||||
)
|
||||
|
||||
custom_data = {"session_id": "handoff123"}
|
||||
|
||||
async for event in workflow.run_stream("handoff test", custom_data=custom_data):
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
|
||||
break
|
||||
|
||||
# Coordinator agent should have received kwargs
|
||||
assert len(agent1.captured_kwargs) >= 1, "Coordinator should be invoked in handoff"
|
||||
assert agent1.captured_kwargs[0].get("custom_data") == custom_data
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Magentic Builder Tests
|
||||
|
||||
|
||||
async def test_magentic_kwargs_flow_to_agents() -> None:
|
||||
"""Test that kwargs flow to agents in a magentic workflow via MagenticAgentExecutor."""
|
||||
from agent_framework import MagenticBuilder
|
||||
from agent_framework._workflows._magentic import (
|
||||
MagenticContext,
|
||||
MagenticManagerBase,
|
||||
_MagenticProgressLedger,
|
||||
_MagenticProgressLedgerItem,
|
||||
)
|
||||
|
||||
# Create a mock manager that completes after one round
|
||||
class _MockManager(MagenticManagerBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(max_stall_count=3, max_reset_count=None, max_round_count=2)
|
||||
self.task_ledger = None
|
||||
|
||||
async def plan(self, context: MagenticContext) -> ChatMessage:
|
||||
return ChatMessage(role=Role.ASSISTANT, text="Plan: Test task", author_name="manager")
|
||||
|
||||
async def replan(self, context: MagenticContext) -> ChatMessage:
|
||||
return ChatMessage(role=Role.ASSISTANT, text="Replan: Test task", author_name="manager")
|
||||
|
||||
async def create_progress_ledger(self, context: MagenticContext) -> _MagenticProgressLedger:
|
||||
# Return completed on first call
|
||||
return _MagenticProgressLedger(
|
||||
is_request_satisfied=_MagenticProgressLedgerItem(answer=True, reason="Done"),
|
||||
is_progress_being_made=_MagenticProgressLedgerItem(answer=True, reason="Progress"),
|
||||
is_in_loop=_MagenticProgressLedgerItem(answer=False, reason="Not looping"),
|
||||
instruction_or_question=_MagenticProgressLedgerItem(answer="Complete", reason="Done"),
|
||||
next_speaker=_MagenticProgressLedgerItem(answer="agent1", reason="First"),
|
||||
)
|
||||
|
||||
async def prepare_final_answer(self, context: MagenticContext) -> ChatMessage:
|
||||
return ChatMessage(role=Role.ASSISTANT, text="Final answer", author_name="manager")
|
||||
|
||||
agent = _KwargsCapturingAgent(name="agent1")
|
||||
manager = _MockManager()
|
||||
|
||||
workflow = MagenticBuilder().participants(agent1=agent).with_standard_manager(manager=manager).build()
|
||||
|
||||
custom_data = {"session_id": "magentic123"}
|
||||
|
||||
async for event in workflow.run_stream("magentic test", custom_data=custom_data):
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
|
||||
break
|
||||
|
||||
# The workflow completes immediately via prepare_final_answer without invoking agents
|
||||
# because is_request_satisfied=True. This test verifies the kwargs storage path works.
|
||||
# A more comprehensive integration test would require the manager to select an agent.
|
||||
|
||||
|
||||
async def test_magentic_kwargs_stored_in_shared_state() -> None:
|
||||
"""Test that kwargs are stored in SharedState when using MagenticWorkflow.run_stream()."""
|
||||
from agent_framework import MagenticBuilder
|
||||
from agent_framework._workflows._magentic import (
|
||||
MagenticContext,
|
||||
MagenticManagerBase,
|
||||
_MagenticProgressLedger,
|
||||
_MagenticProgressLedgerItem,
|
||||
)
|
||||
|
||||
class _MockManager(MagenticManagerBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(max_stall_count=3, max_reset_count=None, max_round_count=1)
|
||||
self.task_ledger = None
|
||||
|
||||
async def plan(self, context: MagenticContext) -> ChatMessage:
|
||||
return ChatMessage(role=Role.ASSISTANT, text="Plan", author_name="manager")
|
||||
|
||||
async def replan(self, context: MagenticContext) -> ChatMessage:
|
||||
return ChatMessage(role=Role.ASSISTANT, text="Replan", author_name="manager")
|
||||
|
||||
async def create_progress_ledger(self, context: MagenticContext) -> _MagenticProgressLedger:
|
||||
return _MagenticProgressLedger(
|
||||
is_request_satisfied=_MagenticProgressLedgerItem(answer=True, reason="Done"),
|
||||
is_progress_being_made=_MagenticProgressLedgerItem(answer=True, reason="Progress"),
|
||||
is_in_loop=_MagenticProgressLedgerItem(answer=False, reason="Not looping"),
|
||||
instruction_or_question=_MagenticProgressLedgerItem(answer="Done", reason="Done"),
|
||||
next_speaker=_MagenticProgressLedgerItem(answer="agent1", reason="First"),
|
||||
)
|
||||
|
||||
async def prepare_final_answer(self, context: MagenticContext) -> ChatMessage:
|
||||
return ChatMessage(role=Role.ASSISTANT, text="Final", author_name="manager")
|
||||
|
||||
agent = _KwargsCapturingAgent(name="agent1")
|
||||
manager = _MockManager()
|
||||
|
||||
magentic_workflow = MagenticBuilder().participants(agent1=agent).with_standard_manager(manager=manager).build()
|
||||
|
||||
# Use MagenticWorkflow.run_stream() which goes through the kwargs attachment path
|
||||
custom_data = {"magentic_key": "magentic_value"}
|
||||
|
||||
async for event in magentic_workflow.run_stream("test task", custom_data=custom_data):
|
||||
if isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
|
||||
break
|
||||
|
||||
# Verify the workflow completed (kwargs were stored, even if agent wasn't invoked)
|
||||
# The test validates the code path through MagenticWorkflow.run_stream -> _MagenticStartMessage
|
||||
|
||||
|
||||
# endregion
|
||||
Reference in New Issue
Block a user