Guide · AI product engineer
Investigate card charges with Deep Agents and Rasa
Build a bank voice assistant that uses Deep Agents to research a card charge and asks the caller before submitting a review.
Key takeaways (3)
- Use Deep Agents for charge research and Rasa for the voice conversation.
- Keep the caller’s selected charge and approval under application control.
- Reject a pending request when the caller changes the charge.
A customer calls about a card charge they do not recognise. This tutorial uses Deep Agents to research the charge and Rasa Mantle to handle the conversation, including the caller’s approval before submitting a review.
The caller can change their mind. Our offline test corrects the selected charge while a review request waits for approval. Mantle resumes the pending call, which still describes the first charge. The application records nothing. Remove only two checks, and that old approval records the new charge:
| Controlled run | Arguments waiting for approval | Current selection | Result |
|---|---|---|---|
| Guard present | TX-101, £49 | TX-102, £149 | Refused; no record |
| Two checks removed | TX-101, £49 | TX-102, £149 | Records TX-102 |
This is a controlled tracker running the actual Mantle confirmation gate and tool invoker. The unsafe variant removes two application checks in test code. The test exposes a boundary you need when a research agent sits behind a voice assistant: research can explain a charge, but it cannot own the caller’s current choice or approval.
Here we build a fictional bank assistant that combines both frameworks. Mantle handles the conversation, speech and confirmation. Deep Agents investigates one selected charge through a read-only evidence tool. It receives no function that can record a review request.
Use the synthetic Jacaranda Bank fixtures throughout. Their published demo codes select an identity; they are not authentication. No real bank, account, customer or card data is involved. A recorded request is an in-memory demo record, not a dispute sent to a bank. No fraud or refund is decided.
Keep research behind the conversation
A caller sees an unfamiliar payment descriptor. The assistant looks up the selected charge, asks Deep Agents to read the descriptor and review policy, then explains the returned facts. The caller may recognise it and stop there. If they still want staff review, Mantle prepares a proposal and asks for a separate confirmation before recording it.
- The pending call keeps its arguments. Later approval resumes the proposal prepared for the original charge.
- The application keeps the current selection. A correction removes the old proposal. Its guard refuses the resumed call before recording a request.
The two paths have different authority. Deep Agents can read a snapshot and return evidence references. Mantle’s application service holds the selected charge, proposal and demo records. A matching merchant descriptor identifies the merchant. Establish payment authorisation from separate evidence.
The Deep Agents customisation guide
describes tools= as “Domain tools the agent can call”. Our extra step is to
put that graph behind a conversation tool with no submission capability.
The tests below check the graph’s offered tools and the proposal binding.
They do not audit every Deep Agents feature.
Bind approval to the current charge
Preparing review creates a proposal containing the selected transaction, merchant, amount, currency and date. Those fields become arguments to the submission tool, so they remain available for confirmation. The function checks them against the current proposal before recording anything.
This abbreviated excerpt of the case service invalidates the proposal before checking a new selection:
def select(self, transaction_id):
self.revision += 1
self.proposal = None
self.selected = None
# Check ownership before accepting the new selection.- A research result started under the earlier revision is discarded.
- The pending call’s proposal no longer exists, so submission refuses it.
- Even a denied correction leaves no selected charge to reuse accidentally.
The submission guard then checks both the proposal and its readback:
if not self.proposal or proposal_id != self.proposal["proposal_id"] or self.selected != self.proposal["transaction_id"]:
return {"error": "proposal_changed_or_missing"}
if any(self.proposal[key] != value for key, value in (("merchant", merchant), ("amount", amount), ("currency", currency), ("date", date))):
return {"error": "readback_changed"}
These excerpts omit the remaining selection and recording code. The native correction test resumes the old call through Mantle’s actual invoker after changing the charge. It checks the refusal and the empty record store.
The Mantle skill adds the confirmation gate:
tool_constraints:
- submit_review:
requires_confirmation:
enabled: true
The original request to file review cannot also approve it in that same turn. Mantle pauses and waits for a later answer. The separate native controls print:
same-turn approval | recorded=0
The later-turn control printed a separate excerpt:
next-turn approval | recorded=1
A correction invalidates the old proposal, including a correction to a charge that the caller cannot access. Approval of that old call then fails. Changing the readback amount also fails. Repeating submission of the same current proposal returns the same demo reference, rather than creating a second record.
To check which layer stops the record, a second offline test removes just the two refusal checks from the actual Python method. It resumes the same old call through Mantle, after the same correction.
Both runs receive a later approval. In the unsafe variant, the remaining recording code uses the current selection, although the pending arguments still describe the first charge. Confirmation permits the call to run; it does not reconcile those two states. The application must do that.
The mutation exists only inside the native test. It removes the two checks inside the controlled test service. Keep the checks in the runnable service. The comparison uses synthetic state and a controlled tracker, with no model conversation or provider calls.
Install two selected environments
Start with the runnable companion. You need Python 3.12, uv, an OpenAI key and a Rasa Pro licence. Voice also needs Deepgram and Rime keys. The companion pins an experimental Mantle build and separate Deep Agents dependencies; use it for this developer exercise.
One combined environment does not resolve. The pinned Mantle package requires OpenAI SDK versions below those accepted by the pinned LangChain integration. Two lockfiles keep each runtime’s dependency requirements intact. Do not resolve the conflict by forcing an unsupported version.
From the resources repository root:
python3 scripts/workspace.py start tutorials/deepagents-voice-disputes/worker --owner jacaranda-worker
python3 scripts/workspace.py start tutorials/deepagents-voice-disputes/mantle --owner jacaranda-mantle
cd tutorials/deepagents-voice-disputes
export BANK_RESEARCH_PYTHON="$PWD/worker/.venv/bin/python"
From that tutorial directory, run the native proposal controls:
mantle/.venv/bin/python -B -m unittest discover -s tests -p test_mantle_native.py -v
Two separate literal output excerpts contrast the guarded call with the mutation that removes the proposal checks:
guard present | pending=TX-101/49.00 GBP | current=TX-102/149.00 GBP | recorded=0
guard removed | pending=TX-101/49.00 GBP | current=TX-102/149.00 GBP | recorded=1 | stored=TX-102
The second deliberately records the current charge from an old call. Keep the first outcome when adapting this service.
Set the model key, licence and speech keys in that terminal, following the companion’s template. Keep values local. The worker receives the explicit model key; it does not receive the licence or speech keys. This process boundary also separates available functions. It is not an operating-system sandbox.
For these tiny fixtures, a Python lookup is enough. Keep Deep Agents when you already have a useful research workflow with several evidence sources and want to add a voice front end. Use a direct lookup for a fixed descriptor list.
Pass one selected case to Deep Agents
First select the synthetic identity and a charge belonging to it. Demo A uses
code 111111 and can select TX-101 or TX-102. Demo B uses 222222 and can
select TX-201. Asking for another identity’s charge returns an unavailable
error without its details.
The Mantle research tool copies the selected case’s evidence and starts the worker. It sends no owner identifier, customer directory or submit function. The worker builds a fresh graph for that snapshot:
return create_deep_agent(
model=model,
tools=[read_case_evidence],
subagents=[],
system_prompt=instructions,
)
This is a shortened excerpt; use the complete companion for the registered profile and instructions. Its profile removes shell execution, filesystem operations and delegation. The actual graph test checks those tool names are absent, rather than assuming that an empty subagent list disables everything.
The graph returns evidence references. The worker returns the original facts behind those references and drops model-authored decisions. The parent checks that those facts match its original snapshot. Unknown references, a missing worker, malformed output and a deadline expiry produce an error.
The worker accepts text blocks as well as a plain string. The worker parser and its regression test handle both forms before validating evidence references. A valid JSON reply is still rejected if it names evidence outside the selected snapshot.
If the selected charge changes while research is running, Mantle discards the result. A valid answer about the old charge is still the wrong answer for the current selection.
Run the tests before speaking
From the tutorial directory, run each suite with its intended interpreter:
mantle/.venv/bin/python -B -m unittest discover -s tests -p test_casework.py -v
mantle/.venv/bin/python -B -m unittest discover -s tests -p test_bridge.py -v
mantle/.venv/bin/python -B -m unittest discover -s tests -p test_mantle_native.py -v
worker/.venv/bin/python -B -m unittest discover -s tests -p test_worker_native.py -v
mantle/.venv/bin/python -B -m unittest discover -s tests -p test_replay_text.py -v
| Check | What it establishes | Additional acceptance needed |
|---|---|---|
| Case state | Ownership, changed readback, proposal invalidation and repeated submission | Production authentication or consent |
| Worker bridge | Selected snapshot, credential allowlist, deadline cleanup and unchanged evidence | An operating-system sandbox |
| Native Deep Agents graph | Real tool execution with a controlled model and no offered submission, shell or filesystem tools | Model accuracy or production case coverage |
| Native Mantle gate | Same-turn refusal, denial, later approval and stale-call refusal with a controlled tracker | Speech understanding or a full model conversation |
The verification record lists these proof types. The companion also passed project validation and licensed training locally. A live Deep Agents smoke test read the synthetic transaction, descriptor and review policy. That checks one worker run, not a success rate. The native suites remain repeatable without provider requests.
Replay a complete text conversation
Before connecting a microphone, start the trained local runtime. From mantle/,
run make train, then make run. In another terminal, from the tutorial
directory:
python3 replay_text.py --case approval --out /tmp/jacaranda-approval.json
The client sends four synthetic messages: hello, the demo code and charge, a request for staff review, then confirmation. It inspects tool results rather than accepting the assistant’s words as proof of a recorded request.
The 10 October baseline prepared the review and asked for a yes before arming the pause. The submission then paused in that same turn, and Mantle refused same-turn resolution. That synthetic failure is retained in the pinned acquisition record.
Call submit_review before asking for confirmation: it establishes the pause
that a subsequent user turn must resolve. The preparation result and skill
state that order; the runtime gate and application checks enforce execution.
| Point in the revised approval run | Tool result | Summary of returned text |
|---|---|---|
| Research finishes | research_complete | The descriptor belongs to the fictional Aster Reading membership; payment authorisation remains unresolved |
| Review is requested | prepared, then awaiting_confirmation | A readback of the £49 charge and a confirmation question |
| Caller confirms in a new turn | demo_recorded with a DEMO- reference | Staff review was recorded; no refund was decided |
These synthetic REST text runs are dated 10 October 2026 and retain their
original source revision, before the second merchant fixture was renamed.
The current replay uses Larchwick Lodge for TX-102; its renamed source has
offline qualification. Both Mantle and the Deep Agents worker used pinned
gpt-5.5-2026-04-23. The table summarises returned text.
The saved approval trace retains its original incomplete status from a
reader that counted a resumed result twice. Its unchanged bytes pass the
corrected offline reader, which counts distinct receipt identities. Acquisition
and parser revalidation are separate: the latter used no server or provider
request. The dated qualification record preserves both the original trace hash
and its corrected readback result.
Each case has one recorded text conversation. The replay reads the tool results and counts distinct receipt identities:
| Replay case | Messages | Distinct recorded receipts | Charge recorded |
|---|---|---|---|
approval | 4 | 1 | TX-101, £49 |
denial | 4 | 0 | None |
correction | 6 | 1 | TX-102, £149, after a fresh proposal and confirmation |
Use --case denial or --case correction with a different output path to
try those paths. Assess the intended dialogue with repeated user-task trials;
assess transcription, synthesis, accents and interruption with connected audio.
Demo codes remain public fixture selectors, never trusted authentication.
One resumed submission appears under both submit_review and its confirmation
resolver in the tracker. Those are two result events for one receipt. The
replay counts distinct receipt identities and rejects a wrong charge or a
second receipt. Its negative controls also reject a missing pause and a
same-turn resolution error.
The server uses paid model calls, including the Deep Agents worker. A message can cause several requests. Run one selected case, inspect failures and keep the saved trace; the client does not retry or overwrite it. Recheck its tool outcomes without a server or further model requests:
python3 replay_text.py --case approval --read /tmp/jacaranda-approval.json
The output contains raw tracker data. Keep it local rather than committing or publishing it. Model wording can vary on a later run; the checks require the same tool ordering and charge, not the same sentence.
Connect the voice channel
From mantle/:
make validate
make train
make inspect
Open the local Inspector URL printed by the CLI, select voice and allow the microphone. The configuration uses Deepgram Nova-3 for recognition and Rime Mist v3 with the ironwood speaker for synthesis. The supplied setup starts with these providers. Interruptions are disabled.
Use the demo identity, then ask about the 49-pound ASTER*ANNUAL charge. The
fixture descriptor points to the fictional Aster Reading annual membership.
Say whether you recognise it. If you still want staff review, request it and
answer the separate confirmation. Try a denial and a correction before approval.
Measure audio performance in the combined service. Test actual speech, names, amounts, corrections and interruptions before offering a real service. Keep replies short and give a reference only after the tool reports a recorded request. A timeout should lead to an offer of staff review, not a guessed finding.
Show the checks for common setup failures
| Symptom | Check first |
|---|---|
| Research is not configured | Absolute worker interpreter path and exported model key |
| Research is unavailable | Worker lock, provider account, deadline or returned evidence format |
| Proposal changed or missing | Select and prepare again after a correction |
| Readback changed | Use the prepared merchant, amount, currency and date unchanged |
| Voice will not connect | Speech keys, local Inspector, microphone permission and channel configuration |
| Records disappear after restart | Expected: this demo stores them in memory |
Decide what to keep
The Deep Agents SQL migration guide shows how to move a task to one runtime. Here we keep both. Use a migration when you want one runtime to own the task. Use this tool boundary when an existing research workflow remains useful and Mantle should own its customer conversation.
Before production, replace fixture codes with trusted identity, move case state into a durable service, expire sessions and proposals, record consent, and set provider concurrency and data-retention policies. Assign the application team the submission service, the conversation team the readback, and the platform team the worker lifecycle. Qualification needs real recordings and operational failure tests; synthetic examples cannot supply them.
After your experiment finishes, stop your own runtime and retire both selected environments with the companion’s commands. Keep both manifests and locks. Do not copy environments into worktrees or backups. This pair needs dependency isolation; completed experiments do not need permanent installed copies.