From 072c5ad39db1cda5abfeddff8c05cc3772eb2a02 Mon Sep 17 00:00:00 2001 From: Tao Chen Date: Tue, 2 Jun 2026 15:02:24 -0700 Subject: [PATCH] Address comments and fix pipeline --- .../tests/workflow/test_workflow_agent.py | 56 +++++++++++++++++++ .../09_foundry_skills/provision_skills.py | 14 +++-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/python/packages/core/tests/workflow/test_workflow_agent.py b/python/packages/core/tests/workflow/test_workflow_agent.py index 5113fb02b8..66ea6669fd 100644 --- a/python/packages/core/tests/workflow/test_workflow_agent.py +++ b/python/packages/core/tests/workflow/test_workflow_agent.py @@ -1648,6 +1648,7 @@ class _ToolApprovalMockAgent(SupportsAgentRun): session: AgentSession | None = ..., **kwargs: Any, ) -> Awaitable[AgentResponse[Any]]: ... + @overload def run( self, @@ -1910,6 +1911,61 @@ class TestWorkflowAgentToolApproval: assert "done" in final_text assert approval_id in final_text + async def test_tool_approval_response_rejected_resumes_agent(self) -> None: + """Rejection path: ``approved=False`` is forwarded to the inner agent and clears the pending request. + + The WorkflowAgent must route a rejection response back to the paused + ``AgentExecutor`` exactly the same way as an approval — only the + ``approved`` flag differs. The inner agent decides what to do with it. + """ + approval_id = "approval-reject-1" + mock_agent = _ToolApprovalMockAgent( + name="approval-reject-agent", + tool_name="delete_file", + tool_arguments={"path": "/tmp/x"}, + approval_request_ids=[approval_id], + ) + + @executor + async def start(messages: list[Message], ctx: WorkflowContext[AgentExecutorRequest]) -> None: + await ctx.send_message(AgentExecutorRequest(messages=messages, should_respond=True)) + + workflow = WorkflowBuilder(start_executor=start).add_edge(start, mock_agent).build() + agent = WorkflowAgent(workflow=workflow, name="Approval Reject Agent") + + first_result = await agent.run("delete it") + approval = self._find_approval_request( + [c for m in first_result.messages for c in m.contents], + tool_name="delete_file", + ) + assert approval is not None + assert mock_agent.run_count == 1 + + # Reject the tool invocation. + approval_response = approval.to_function_approval_response(approved=False) # type: ignore[attr-defined] + response_message = Message(role="user", contents=[approval_response]) + + final_result = await agent.run(response_message) + assert isinstance(final_result, AgentResponse) + + # The inner agent must have been resumed and seen ``approved=False``. + assert mock_agent.run_count == 2 + approvals_seen = [ + c for m in mock_agent.last_run_messages for c in m.contents if c.type == "function_approval_response" + ] + assert len(approvals_seen) == 1 + assert approvals_seen[0].id == approval_id # type: ignore[attr-defined] + assert approvals_seen[0].approved is False # type: ignore[attr-defined] + + # Pending approval cleared regardless of approve/reject. + pending = await workflow._runner_context.get_pending_request_info_events() + assert approval_id not in pending + + # The final assistant message reflects the rejection. + final_text = " ".join(m.text or "" for m in final_result.messages) + assert "approved=False" in final_text + assert approval_id in final_text + async def test_tool_approval_request_id_matches_pending_request(self) -> None: """The approval request id surfaced by WorkflowAgent matches the workflow's pending request id. diff --git a/python/samples/04-hosting/foundry-hosted-agents/responses/09_foundry_skills/provision_skills.py b/python/samples/04-hosting/foundry-hosted-agents/responses/09_foundry_skills/provision_skills.py index c8eda282da..5f501d61e1 100644 --- a/python/samples/04-hosting/foundry-hosted-agents/responses/09_foundry_skills/provision_skills.py +++ b/python/samples/04-hosting/foundry-hosted-agents/responses/09_foundry_skills/provision_skills.py @@ -28,6 +28,7 @@ import zipfile from pathlib import Path from azure.ai.projects.aio import AIProjectClient +from azure.ai.projects.models import CreateSkillVersionFromFilesBody from azure.core.exceptions import ResourceNotFoundError from azure.identity.aio import DefaultAzureCredential from dotenv import load_dotenv @@ -68,8 +69,13 @@ async def main() -> None: name = skill_md.parent.name print(f"Provisioning skill '{name}' from {skill_md.relative_to(SKILLS_DIR.parent)}...") await _delete_skill_if_exists(project, name) - imported = await project.beta.skills.create_from_package(_zip_skill_md(skill_md)) - print(f" Imported skill '{imported.name}' (id={imported.skill_id}, has_blob={imported.has_blob}).") + imported = await project.beta.skills.create_from_files( + name, + content=CreateSkillVersionFromFilesBody( + files=[(f"{name}.zip", _zip_skill_md(skill_md), "application/zip")] + ), + ) + print(f" Imported skill '{imported.name}' (id={imported.skill_id}, version={imported.version}).") print("Verifying skills via project.beta.skills.list()...") listed = {skill.name: skill async for skill in project.beta.skills.list()} @@ -79,8 +85,8 @@ async def main() -> None: if skill is None: raise RuntimeError(f"Skill '{name}' was imported but is not present in the project listing.") print( - f" OK '{skill.name}': id={skill.skill_id}, " - f"description={skill.description!r}, has_blob={skill.has_blob}" + f" OK '{skill.name}': id={skill.id}, " + f"description={skill.description!r}, default_version={skill.default_version}" ) print("Done.")