Skip to content
RasaGet a free licence
Menu
AI team casebook

Conversation designer / UX specialist · Banking & fintech (external)

Respond to financial hardship during a repayment chat

Help a customer who cannot afford basic living costs without treating a later repayment request as a resolved hardship.

by Rod Rivera

About 6 minutesCase study

Key takeaways (3)
  • A later request for a plan cannot erase an earlier hardship disclosure.
  • A staged offer still needs a current confirmation before recording.
  • Unavailable support should preserve the request without recording a plan.

A customer discussing loan repayments tells the assistant they cannot afford rent. A moment later, they ask for a six-payment plan. How should the assistant respond without losing what the customer just said?

The fictional Northgate Bank example pauses automatic plan selection and preserves the request for support. You will check both the conversation history and the tool that records the plan, including what happens when the support team is unavailable.

Customer, authored test input: No me alcanza para la renta.

Customer, next authored test input: Bueno, anóteme en el de seis pagos.

The first line means “I don’t have enough for rent.” The next asks to be put on the six-payment plan. These are supplied text inputs, not transcribed speech. The fictional Northgate Bank tool returns:

{"memory": null, "staging": "hardship_declared"}
{"detail": "hardship_declared", "effects": 0, "reason": "choice_not_confirmed"}

Decide which earlier turns remain relevant

A conversation designer cannot treat every new request as a clean slate. In this teaching policy, difficulty meeting basic needs stops automatic repayment-plan selection. The later plan request supplies intent, but it does not establish that the difficulty was resolved.

The first refusal happens while staging the offer. The proof then inserts staged state directly and attempts the final write. Recording is refused too, with no effect. That second part catches a guard placed only at the start of the conversation.

Check whether the final refusal actually matters

The staging refusal alone does not show what the final confirmation check protects. Start instead with a valid staged offer, matching memory tag and completed read-back. Then add the hardship disclosure before recording the plan.

The three companion controls compare the normal helper with a deliberately broken copy that removes only its final hardship check. The test changes a function in memory and restores it after the call; it does not edit the installed agent or call a payment service.

Same current offer and confirmationPlan recordedRecorded effects
No hardship disclosureYes1
Hardship disclosed; final check presentNo0
Hardship disclosed; final check removedYes1

These are actual local fixture results from 13 October 2026. They isolate the final check: with the other confirmation conditions satisfied, removing it records a plan despite the disclosure. A separate result matters too: refusing the plan does not create a support referral. The assistant still needs to request support and report whether that request was accepted.

From the repository root, fetch the companion update and check out its exact revision before running the controls:

git fetch origin
git checkout eeb3c73e12f35829857f277ebcb0a169c22a4bd2
python3 -B -m unittest discover \
  -s examples/mantle-voice-banking-collections-gemini/tests \
  -p 'test_confirmation_boundary.py' -v

All three controls passed. They exercise the authored Spanish phrases and fixture policy; they do not measure a model’s recognition of hardship.

Put the hardship stop inside confirmation

The confirmation helper reads the customer history and open referrals before it checks the staged offer and read-back. The hardship stop is the first branch in this excerpt.

Source excerpt:

if hardship_declared(conversation.user_texts) or state.open_referral():
    return False, "hardship_declared"
staged = state.staged
if offer is None or not staged or staged["offer_id"] != offer["offer_id"] or memory_tag != staged["tag"]:
    return False, "not_staged"
question = fold(conversation.confirmation_question or "")
if fold(_offer_view(offer)["terms_spoken"]) not in question or fold(account["label"]) not in question:
    return False, "not_read_back"
if conversation.confirmation_answer is None:
    return False, "not_answered"

This detector reads a bounded set of authored Spanish phrases in the companion source. The checks cover those inputs. They do not establish recall across indirect disclosures, dialects or speech-recognition mistakes. Have the collections owner supply the disclosures that matter for the intended release.

Give an unavailable team a different reply

The loan hardship fixture cannot accept a referral. The callback test preserves the request and returns a callback reference. That is a different state from an accepted referral, and neither is a recorded repayment plan.

Service statePermissible next stepClaim to avoid
Hardship referral acceptedGive the referral referenceSaying a plan was agreed
Hardship team unavailablePreserve request and callback referenceSaying the team accepted a referral
Customer withdraws agreementClear staged choice or withdraw recorded planTreating an earlier yes as current
The conversation can leave the offer path before any plan is recorded
FigureThe conversation can leave the offer path before any plan is recorded

The support team needs to receive the preserved request. The fixture creates a record; it does not place a phone call. Agree who owns unfulfilled callbacks and what the customer is told when a team remains unavailable.

Test the final write, not just the first reply

Run these checks from the pinned companion project. They use Python’s standard library and make no model calls. The outputs below were recorded on 7 October 2026; elapsed times can differ.

git clone https://github.com/RasaHQ/rasa-community-resources.git
cd rasa-community-resources
git checkout 4aa0c4419dc193fef7a969c12d59edcf720f2606
cd examples/mantle-voice-banking-collections-gemini/tests
Any system
python3 -m unittest test_guard.SelectAndRecordTests test_guard.WithdrawReferralCallbackTests -q
----------------------------------------------------------------------
Ran 19 tests in 0.001s

OK

The companion tests contain the assertions for these cases. To print the first-screen trace yourself, run this script from the same directory:

Show the script that printed the fixture states

Paste it into a file such as trace.py, then run python3 trace.py. It prints selected fields from the actual tool results; it does not invent replies.

"""Run from the pinned companion project tests directory. Stdlib only."""
import json
import test_guard as t

case = t.SelectAndRecordTests()
case.setUp()
users = ("No me alcanza para la renta.", "Bueno, anóteme en el de seis pagos.")
staged, memory = case.stage("OFR-4471-6M", users=users)
print(json.dumps({"staging": staged["status"], "memory": memory}, sort_keys=True))
case.state.staged = {"account_ending": "4471", "offer_id": "OFR-4471-6M", "tag": "OFR-4471-6M/1"}
result = case.record("OFR-4471-6M", {"plan_tag": "OFR-4471-6M/1"}, users=users)
print(json.dumps({"reason": result["reason"], "detail": result["detail"], "effects": result["effects"]}, sort_keys=True))

For conversation review, replay the two authored turns before checking a normal yes. Then attempt the final write with staged state and inspect the result. Design the handoff summary so it carries the disclosure and unresolved request without inventing a resolved outcome.

The complete fixture implementation defines the service state and remaining branches cited here.