# 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}" approve_once = "Approve this call" always_tool = "Always approve this tool (any arguments)" always_tool_args = "Always approve this tool with these arguments" deny = "Deny" choices = [approve_once, always_tool, always_tool_args, deny] async def continuation( selection: str, ux: IUXStateDriver, ) -> Message | None: from agent_framework import ( Message, create_always_approve_tool_response, create_always_approve_tool_with_arguments_response, ) if selection == deny: response_content = request.to_function_approval_response(approved=False) action_label = "❌ Denied" color = "red" elif selection == always_tool: response_content = create_always_approve_tool_response( request, reason="User chose to always approve this tool" ) action_label = "✅ Always approved (any args)" color = "green" elif selection == always_tool_args: response_content = create_always_approve_tool_with_arguments_response( request, reason="User chose to always approve this tool with these arguments" ) action_label = "✅ Always approved (these args)" color = "green" 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"