AI scenarios
Tool approval
Bit.BlazorUI.AI Scripted transportA destructive tool stops and waits for a human. Allow once, allow always, or decline - and a provider can ask for the same decision itself.
Live demo
Ask for something destructive: delete_records is declared destructive, so the session will not run it unattended.
Approval the transport asks for
Ask about the weather: get_weather is read-only, so this app's policy would run it unattended - but this transport asks for the user's consent itself, and says why.
How it works
The transport asks for the tool as any tool call is asked for (BitAiToolCallStartedEvent, the
arguments streamed as BitAiToolCallDeltaEvents, BitAiToolCallCompletedEvent) and
ends the reply with BitAiFinishReason.ToolCalls. Before the session runs the call, it asks the
approval policy (IBitAiToolApprovalPolicy.RequiresApprovalAsync(definition, invocation)). When a
decision is needed it moves the call to AwaitingApproval and raises
BitAiChatSession.ApprovalRequested, and the call's card shows the prompt, whose buttons record
the decision with BitAiChatSession.Approve(toolCallId, decision) - Approved,
ApprovedAlways or Rejected.
Allowing once and allowing always are different decisions. Collapsing them into a single button forces the user either to re-approve constantly or to grant more than they meant to, so both are offered, beside a decline.
What needs a decision is the IBitAiToolApprovalPolicy's call. The default one asks for every
tool whose Risk is Destructive (or that says RequiresApproval) and
remembers the tools a user allowed always - which is why it is scoped on Blazor Server, where a singleton
would carry one user's "always" over to every other user. ai.UseApprovalPolicy<T>()
replaces it, for a decision that depends on the arguments or on the user.
While a call waits, the turn is genuinely paused: the tool has not run, the model has not been asked again,
and stopping the turn cancels the wait. A declined call is marked Rejected and given a failed
result, which the transport reads off the call on the next request, so the model can explain that nothing
was done.
A provider whose own policy wants consent asks for it with a BitAiToolApprovalRequestedEvent,
as the second demo's transport does right after starting the call. It puts the call in
AwaitingApproval with its Reason, which the prompt shows as the question, and the
session waits for the user just as it would for its own policy - even for a tool that policy runs
unattended, and whatever the user allowed always before: the request is the provider's, not the policy's
to overrule. The session still runs the tool once it is allowed, so the tool is registered as usual.
Components used
BitAiToolApproval- the prompt: allow once, allow always, or decline.BitAiToolCallView- the card the prompt sits in.
Feedback
Found a mistake, a gap, or something that could be clearer? Every page and every component is one click from its source.