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