mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python [BREAKING]: support magentic agent tool call approvals and plan stalling HITL behavior (#2569)
* Provide way for HITL with magentic * support tool call approvals and hitl stall replan * human plan intervention sample * Clean up * Improve loging * updates
This commit is contained in:
committed by
GitHub
Unverified
parent
292a2078fa
commit
19ac6e57c0
@@ -74,10 +74,15 @@ from ._magentic import (
|
||||
ORCH_MSG_KIND_USER_TASK,
|
||||
MagenticBuilder,
|
||||
MagenticContext,
|
||||
MagenticHumanInputRequest,
|
||||
MagenticHumanInterventionDecision,
|
||||
MagenticHumanInterventionKind,
|
||||
MagenticHumanInterventionReply,
|
||||
MagenticHumanInterventionRequest,
|
||||
MagenticManagerBase,
|
||||
MagenticPlanReviewDecision,
|
||||
MagenticPlanReviewReply,
|
||||
MagenticPlanReviewRequest,
|
||||
MagenticStallInterventionDecision,
|
||||
MagenticStallInterventionReply,
|
||||
MagenticStallInterventionRequest,
|
||||
StandardMagenticManager,
|
||||
)
|
||||
from ._orchestration_state import OrchestrationState
|
||||
@@ -144,10 +149,15 @@ __all__ = [
|
||||
"InProcRunnerContext",
|
||||
"MagenticBuilder",
|
||||
"MagenticContext",
|
||||
"MagenticHumanInputRequest",
|
||||
"MagenticHumanInterventionDecision",
|
||||
"MagenticHumanInterventionKind",
|
||||
"MagenticHumanInterventionReply",
|
||||
"MagenticHumanInterventionRequest",
|
||||
"MagenticManagerBase",
|
||||
"MagenticPlanReviewDecision",
|
||||
"MagenticPlanReviewReply",
|
||||
"MagenticPlanReviewRequest",
|
||||
"MagenticStallInterventionDecision",
|
||||
"MagenticStallInterventionReply",
|
||||
"MagenticStallInterventionRequest",
|
||||
"ManagerDirectiveModel",
|
||||
"ManagerSelectionRequest",
|
||||
"ManagerSelectionResponse",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,16 +12,13 @@ from agent_framework import (
|
||||
AgentRunResponseUpdate,
|
||||
AgentRunUpdateEvent,
|
||||
BaseAgent,
|
||||
ChatClientProtocol,
|
||||
ChatMessage,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Executor,
|
||||
MagenticBuilder,
|
||||
MagenticHumanInterventionDecision,
|
||||
MagenticHumanInterventionReply,
|
||||
MagenticHumanInterventionRequest,
|
||||
MagenticManagerBase,
|
||||
MagenticPlanReviewDecision,
|
||||
MagenticPlanReviewReply,
|
||||
MagenticPlanReviewRequest,
|
||||
RequestInfoEvent,
|
||||
Role,
|
||||
TextContent,
|
||||
@@ -59,21 +56,25 @@ def test_magentic_start_message_from_string():
|
||||
assert msg.task.text == "Do the thing"
|
||||
|
||||
|
||||
def test_plan_review_request_defaults_and_reply_variants():
|
||||
req = MagenticPlanReviewRequest() # defaults provided by dataclass
|
||||
def test_human_intervention_request_defaults_and_reply_variants():
|
||||
from agent_framework._workflows._magentic import MagenticHumanInterventionKind
|
||||
|
||||
req = MagenticHumanInterventionRequest(kind=MagenticHumanInterventionKind.PLAN_REVIEW)
|
||||
assert hasattr(req, "request_id")
|
||||
assert req.task_text == "" and req.facts_text == "" and req.plan_text == ""
|
||||
assert isinstance(req.round_index, int) and req.round_index == 0
|
||||
|
||||
# Replies: approve, revise with comments, revise with edited text
|
||||
approve = MagenticPlanReviewReply(decision=MagenticPlanReviewDecision.APPROVE)
|
||||
revise_comments = MagenticPlanReviewReply(decision=MagenticPlanReviewDecision.REVISE, comments="Tighten scope")
|
||||
revise_text = MagenticPlanReviewReply(
|
||||
decision=MagenticPlanReviewDecision.REVISE,
|
||||
approve = MagenticHumanInterventionReply(decision=MagenticHumanInterventionDecision.APPROVE)
|
||||
revise_comments = MagenticHumanInterventionReply(
|
||||
decision=MagenticHumanInterventionDecision.REVISE, comments="Tighten scope"
|
||||
)
|
||||
revise_text = MagenticHumanInterventionReply(
|
||||
decision=MagenticHumanInterventionDecision.REVISE,
|
||||
edited_plan_text="- Step 1\n- Step 2",
|
||||
)
|
||||
|
||||
assert approve.decision == MagenticPlanReviewDecision.APPROVE
|
||||
assert approve.decision == MagenticHumanInterventionDecision.APPROVE
|
||||
assert revise_comments.comments == "Tighten scope"
|
||||
assert revise_text.edited_plan_text is not None and revise_text.edited_plan_text.startswith("- Step 1")
|
||||
|
||||
@@ -220,15 +221,14 @@ async def test_magentic_workflow_plan_review_approval_to_completion():
|
||||
|
||||
req_event: RequestInfoEvent | None = None
|
||||
async for ev in wf.run_stream("do work"):
|
||||
if isinstance(ev, RequestInfoEvent) and ev.request_type is MagenticPlanReviewRequest:
|
||||
if isinstance(ev, RequestInfoEvent) and ev.request_type is MagenticHumanInterventionRequest:
|
||||
req_event = ev
|
||||
assert req_event is not None
|
||||
|
||||
completed = False
|
||||
output: list[ChatMessage] | None = None
|
||||
async for ev in wf.send_responses_streaming(
|
||||
responses={req_event.request_id: MagenticPlanReviewReply(decision=MagenticPlanReviewDecision.APPROVE)}
|
||||
):
|
||||
reply = MagenticHumanInterventionReply(decision=MagenticHumanInterventionDecision.APPROVE)
|
||||
async for ev in wf.send_responses_streaming(responses={req_event.request_id: reply}):
|
||||
if isinstance(ev, WorkflowStatusEvent) and ev.state == WorkflowRunState.IDLE:
|
||||
completed = True
|
||||
elif isinstance(ev, WorkflowOutputEvent):
|
||||
@@ -265,7 +265,7 @@ async def test_magentic_plan_review_approve_with_comments_replans_and_proceeds()
|
||||
# Wait for the initial plan review request
|
||||
req_event: RequestInfoEvent | None = None
|
||||
async for ev in wf.run_stream("do work"):
|
||||
if isinstance(ev, RequestInfoEvent) and ev.request_type is MagenticPlanReviewRequest:
|
||||
if isinstance(ev, RequestInfoEvent) and ev.request_type is MagenticHumanInterventionRequest:
|
||||
req_event = ev
|
||||
assert req_event is not None
|
||||
|
||||
@@ -274,13 +274,13 @@ async def test_magentic_plan_review_approve_with_comments_replans_and_proceeds()
|
||||
completed = False
|
||||
async for ev in wf.send_responses_streaming(
|
||||
responses={
|
||||
req_event.request_id: MagenticPlanReviewReply(
|
||||
decision=MagenticPlanReviewDecision.APPROVE,
|
||||
req_event.request_id: MagenticHumanInterventionReply(
|
||||
decision=MagenticHumanInterventionDecision.APPROVE,
|
||||
comments="Looks good; consider Z",
|
||||
)
|
||||
}
|
||||
):
|
||||
if isinstance(ev, RequestInfoEvent) and ev.request_type is MagenticPlanReviewRequest:
|
||||
if isinstance(ev, RequestInfoEvent) and ev.request_type is MagenticHumanInterventionRequest:
|
||||
saw_second_review = True
|
||||
if isinstance(ev, WorkflowStatusEvent) and ev.state == WorkflowRunState.IDLE:
|
||||
completed = True
|
||||
@@ -338,7 +338,7 @@ async def test_magentic_checkpoint_resume_round_trip():
|
||||
task_text = "checkpoint task"
|
||||
req_event: RequestInfoEvent | None = None
|
||||
async for ev in wf.run_stream(task_text):
|
||||
if isinstance(ev, RequestInfoEvent) and ev.request_type is MagenticPlanReviewRequest:
|
||||
if isinstance(ev, RequestInfoEvent) and ev.request_type is MagenticHumanInterventionRequest:
|
||||
req_event = ev
|
||||
assert req_event is not None
|
||||
|
||||
@@ -359,13 +359,13 @@ async def test_magentic_checkpoint_resume_round_trip():
|
||||
|
||||
orchestrator = next(exec for exec in wf_resume.executors.values() if isinstance(exec, MagenticOrchestratorExecutor))
|
||||
|
||||
reply = MagenticPlanReviewReply(decision=MagenticPlanReviewDecision.APPROVE)
|
||||
reply = MagenticHumanInterventionReply(decision=MagenticHumanInterventionDecision.APPROVE)
|
||||
completed: WorkflowOutputEvent | None = None
|
||||
req_event = None
|
||||
async for event in wf_resume.run_stream(
|
||||
resume_checkpoint.checkpoint_id,
|
||||
):
|
||||
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
|
||||
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticHumanInterventionRequest:
|
||||
req_event = event
|
||||
assert req_event is not None
|
||||
|
||||
@@ -430,25 +430,33 @@ async def test_magentic_agent_executor_on_checkpoint_save_and_restore_roundtrip(
|
||||
from agent_framework import StandardMagenticManager # noqa: E402
|
||||
|
||||
|
||||
class _StubChatClient(ChatClientProtocol):
|
||||
@property
|
||||
def additional_properties(self) -> dict[str, Any]:
|
||||
"""Get additional properties associated with the client."""
|
||||
return {}
|
||||
class _StubManagerAgent(BaseAgent):
|
||||
"""Stub agent for testing StandardMagenticManager."""
|
||||
|
||||
async def get_response(self, messages, **kwargs): # type: ignore[override]
|
||||
return ChatResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="ok")])
|
||||
async def run(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
|
||||
*,
|
||||
thread: Any = None,
|
||||
**kwargs: Any,
|
||||
) -> AgentRunResponse:
|
||||
return AgentRunResponse(messages=[ChatMessage(role=Role.ASSISTANT, text="ok")])
|
||||
|
||||
def get_streaming_response(self, messages, **kwargs) -> AsyncIterable[ChatResponseUpdate]: # type: ignore[override]
|
||||
async def _gen():
|
||||
if False:
|
||||
yield ChatResponseUpdate() # pragma: no cover
|
||||
def run_stream(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
|
||||
*,
|
||||
thread: Any = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[AgentRunResponseUpdate]:
|
||||
async def _gen() -> AsyncIterable[AgentRunResponseUpdate]:
|
||||
yield AgentRunResponseUpdate(message_deltas=[ChatMessage(role=Role.ASSISTANT, text="ok")])
|
||||
|
||||
return _gen()
|
||||
|
||||
|
||||
async def test_standard_manager_plan_and_replan_via_complete_monkeypatch():
|
||||
mgr = StandardMagenticManager(chat_client=_StubChatClient())
|
||||
mgr = StandardMagenticManager(agent=_StubManagerAgent())
|
||||
|
||||
async def fake_complete_plan(messages: list[ChatMessage], **kwargs: Any) -> ChatMessage:
|
||||
# Return a different response depending on call order length
|
||||
@@ -481,7 +489,7 @@ async def test_standard_manager_plan_and_replan_via_complete_monkeypatch():
|
||||
|
||||
|
||||
async def test_standard_manager_progress_ledger_success_and_error():
|
||||
mgr = StandardMagenticManager(chat_client=_StubChatClient())
|
||||
mgr = StandardMagenticManager(agent=_StubManagerAgent())
|
||||
ctx = MagenticContext(
|
||||
task=ChatMessage(role=Role.USER, text="task"),
|
||||
participant_descriptions={"alice": "desc"},
|
||||
@@ -718,7 +726,7 @@ async def test_magentic_checkpoint_resume_rejects_participant_renames():
|
||||
|
||||
req_event: RequestInfoEvent | None = None
|
||||
async for event in workflow.run_stream("task"):
|
||||
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
|
||||
if isinstance(event, RequestInfoEvent) and event.request_type is MagenticHumanInterventionRequest:
|
||||
req_event = event
|
||||
|
||||
assert req_event is not None
|
||||
|
||||
Reference in New Issue
Block a user