mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
bad05a2bdc
* Add initial harness console for python * Add textual to project * Add planning and approval flows with list selector * Address PR comments * Fix list selection bug * Fix PR #6312 round 2 review comments - Escape untrusted agent text with rich.markup.escape() in observers (text_output, planning_output, reasoning_display) to prevent markup injection - Remove non-functional 'Always approve' choices from tool_approval.py (framework lacks CreateAlwaysApproveToolResponse support) - Remove textual from root pyproject.toml dev deps (sample-specific) - Add PEP 723 inline script metadata to harness_research.py - Narrow except Exception to except NoMatches in list_selection.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix build error * Fix build errors --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
140 lines
4.6 KiB
Python
140 lines
4.6 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
"""Tool approval observer for user confirmation of tool calls.
|
|
|
|
Detects function_approval_request content items during streaming, displays
|
|
approval notifications, and after the stream completes presents one
|
|
ChoiceFollowUpQuestion per pending approval request.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from ..app_state import ChoiceFollowUpQuestion, FollowUpAction
|
|
from .base import ConsoleObserver
|
|
|
|
if TYPE_CHECKING:
|
|
from agent_framework import Agent, Content, Message
|
|
|
|
from ..state_driver import IUXStateDriver
|
|
|
|
|
|
class ToolApprovalObserver(ConsoleObserver):
|
|
"""Asks user to approve tool calls before execution.
|
|
|
|
Collects `function_approval_request` content during streaming and presents
|
|
a multi-choice approval question for each after the stream completes.
|
|
The continuation builds a `function_approval_response` Content to inject
|
|
into the next agent turn.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialize the tool approval observer."""
|
|
self._approval_requests: list[Content] = []
|
|
|
|
async def on_content(
|
|
self,
|
|
ux: IUXStateDriver,
|
|
content: Content,
|
|
agent: Agent,
|
|
session: Any,
|
|
) -> None:
|
|
"""Collect function_approval_request content for approval.
|
|
|
|
Args:
|
|
ux: The UX state driver for UI updates.
|
|
content: The content item to check.
|
|
agent: The AI agent.
|
|
session: The agent session.
|
|
"""
|
|
if content.type == "function_approval_request":
|
|
self._approval_requests.append(content)
|
|
tool_name = self._format_tool_name(content)
|
|
ux.append_info_line(f"⚠️ Approval needed: {tool_name}", "yellow")
|
|
|
|
async def on_stream_complete(
|
|
self,
|
|
ux: IUXStateDriver,
|
|
agent: Agent,
|
|
session: Any,
|
|
) -> list[FollowUpAction] | None:
|
|
"""Build approval questions for collected requests.
|
|
|
|
Args:
|
|
ux: The UX state driver for UI updates.
|
|
agent: The AI agent.
|
|
session: The agent session.
|
|
|
|
Returns:
|
|
List of ChoiceFollowUpQuestions, one per approval request.
|
|
"""
|
|
if not self._approval_requests:
|
|
return None
|
|
|
|
actions: list[FollowUpAction] = []
|
|
for request in self._approval_requests:
|
|
actions.append(self._build_approval_question(request))
|
|
|
|
self._approval_requests.clear()
|
|
return actions
|
|
|
|
def _build_approval_question(self, request: Content) -> ChoiceFollowUpQuestion:
|
|
"""Build a multi-choice approval question for a single request."""
|
|
tool_name = self._format_tool_name(request)
|
|
prompt = f"🔐 Tool approval: {tool_name}"
|
|
|
|
# TODO(westey-m): Add "Always approve" options when the framework supports
|
|
# CreateAlwaysApproveToolResponse / CreateAlwaysApproveToolWithArgumentsResponse.
|
|
choices = [
|
|
"Approve this call",
|
|
"Deny",
|
|
]
|
|
|
|
async def continuation(
|
|
selection: str,
|
|
ux: IUXStateDriver,
|
|
) -> Message | None:
|
|
from agent_framework import Message
|
|
|
|
if selection == "Deny":
|
|
response_content = request.to_function_approval_response(approved=False)
|
|
action_label = "❌ Denied"
|
|
color = "red"
|
|
else:
|
|
response_content = request.to_function_approval_response(approved=True)
|
|
action_label = "✅ Approved"
|
|
color = "green"
|
|
|
|
ux.append_info_line(
|
|
f"🔹 {prompt}\n └─ [{color}]{action_label}[/{color}]",
|
|
"dim",
|
|
)
|
|
|
|
return Message(role="user", contents=[response_content])
|
|
|
|
return ChoiceFollowUpQuestion(
|
|
prompt=prompt,
|
|
choices=choices,
|
|
allow_custom_text=False,
|
|
continuation=continuation,
|
|
)
|
|
|
|
@staticmethod
|
|
def _format_tool_name(content: Content) -> str:
|
|
"""Extract a readable tool name from approval request content."""
|
|
# The function_call is stored on the approval request content
|
|
function_call = getattr(content, "function_call", None)
|
|
if function_call is not None:
|
|
from ..formatters import build_default_formatters, format_tool_call
|
|
|
|
try:
|
|
return format_tool_call(build_default_formatters(), function_call)
|
|
except (AttributeError, TypeError):
|
|
pass
|
|
# Fall back to name attribute
|
|
name = getattr(function_call, "name", None)
|
|
if name:
|
|
return str(name)
|
|
return "unknown tool"
|