Skip to content
RasaGet a free licence
Menu

tutorial

Chapter 3 of 6

Confirm a replacement-card order and check its delivery address

by Rod Rivera Published

Use Mantle’s confirmation gate and an address-policy check inside the ordering tool to answer two separate questions.

A customer asks the banking assistant to order a replacement card. You need to confirm the order they intend to place and check that the requested delivery address is permitted. These are two separate decisions.

In this chapter, you will put confirmation in Mantle’s tool constraint and keep the address policy inside the ordering tool. The configuration below asks the engine to pause the pending tool call for confirmation:

tool_constraints:
  - reissue_card:
      requires_confirmation:
        enabled: true
        utter_for_confirmation: utter_confirm_reissue

Confirmation pauses the call

requires_confirmation is an execution gate, not merely a suggestion about which tools to offer. Mantle holds the pending call while it asks for confirmation; the native confirmation tests distinguish a later confirmation from an answer supplied in the original request.

That gate does not establish that an address is allowed by the bank. The tool still needs the customer’s verified tier and the address’s source. It also needs to enforce those checks if another caller invokes the underlying Python function directly, outside Mantle’s confirmation path.

Keep the address check inside the ordering tool

CheckWhere it runsWhat it establishes in this example
requires_confirmationMantle’s pending-call gateWhether the pending call may proceed after the confirmation exchange
guard_reissue()Inside the Python ordering functionWhether the address and verification satisfy the fixture policy

The checks complement one another. A confirmation does not make an address permitted; an address-policy pass does not establish that the customer confirmed the order.

The inner one looks like this, in tools/cards.py:

    address = classify_address(line1, city, postcode, customer["addresses_on_file"])

    # ---- the line before the side effect --------------------------------
    try:
        guard_reissue(address, auth_tier, on_file_since=on_file_since)
    except ReissueRefused as exc:
        return _as_dict(exc.outcome)
    # ---------------------------------------------------------------------

    reference = f"RC-{uuid.uuid4().hex[:8].upper()}"

Every function in that module that changes the world has the same shape: resolve inputs, classify provenance, guard, act, return an outcome. If you copy one thing from this tutorial, copy that ordering.

Why it raises instead of returning False

def guard_reissue(...) -> Decision:
    decision = evaluate(...)
    if decision.allowed:
        return decision
    raise ReissueRefused(...)

A boolean can be ignored by writing guard_reissue(...) on its own line and carrying on — which reads, at a glance, exactly like calling a guard. Reviewers skim past it. An exception cannot be ignored that way: the only route to the line that posts the card is a return.

The test this makes possible

You can exercise the address-policy function directly without a model:

make policy
An address supplied during the call:
  ✓ medium is NOT enough — this is the account-takeover path
      -> ok=False result=step_up_required

No LLM, no sampling, no judge. This is a fact about the process.

This exercise checks the fixture’s address policy. It does not verify caller identity or replace a conversation test. Check Mantle’s confirmation exchange separately, then inspect the service result before telling the customer that the order was placed.