Files
microsoft--agent-framework/python/samples/02-agents/harness/console/observers/tool_approval.py
T
wehub-resource-sync db620d33df
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:25 +08:00

155 lines
5.5 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}"
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"