Guide · AI product engineer
Build a card-policy assistant with LangChain and Rasa
Use your existing document search to answer card-policy questions, with Rasa asking for details and explaining the sources.
Key takeaways (3)
- Reuse your existing LangChain document search through a Rasa tool.
- Check that returned policies apply to the customer’s question.
- Check the task outcome as well as the quotation.
A customer asks whether they can replace a damaged card. Your LangChain search can find the policy; Rasa can ask for missing details and explain the answer. This tutorial connects the two and checks that personal-card questions receive personal-card policies.
A source quotation also needs to support what the assistant says. Our offline citation test pairs these exact fixture strings:
Scripted answer: Your card has already been replaced.
Cited policy: A damaged personal debit card can be replaced.
The checker returns evidence_checked. The quote is genuine; it offers no
record of a completed replacement. In a separate scope control,
a retriever constructed for a personal-card question returns a genuine
business-card policy. In our controlled fault, the document requires the nominated company
administrator’s approval. The application refuses it. Remove only the check on
returned scope, and the conversation receives that business policy instead.
The native handoff controls keep the selected personal demo fixed and replace the retriever’s return with a real fixture document. A separate citation control attaches a genuine quotation to a scripted claim that the card has already been replaced. These independent controls inspect different handoffs:
| Controlled input | Check at the handoff | Observed result |
|---|---|---|
| Personal scope and its personal policy | Returned document scope retained | Source P-101 returned |
| Personal scope but business policy returned | Returned document scope retained | evidence_unavailable |
| Same personal scope and business policy | Only returned-scope guard removed | Source B-201 and its company-administrator quotation returned |
| Genuine policy quote beside a false replacement claim | Citation identity, revision and quotation membership | evidence_checked |
This is a deliberately faulty return and a scripted answer, not an observed provider mistake. Selecting the scope when calling retrieval and checking the returned documents are separate decisions. Checking a quotation’s membership then answers another question: where the words came from. The final claim still needs to be assessed against those words and the task’s actual outcome.
If you already have useful LangChain retrieval, keep it. This example gives that retriever a Rasa conversation without treating the worker’s generated conclusion as a verified fact. The tool returns permitted source quotations; Mantle collects missing information and explains those sources to the user.
The example extends fictional Jacaranda Bank. Its card policies, public demo codes and document corpus are synthetic. No real account is accessed or changed.
Keep search where it already works
The native retriever
uses LangChain’s BaseRetriever and returns Document objects through
invoke. Each source carries an ID, scope and revision. The teaching fixture
uses keyword search over two documents. Assess your existing retrieval on its
own task corpus.
In your application, retain the existing index, loaders and ranking. Adapt its retriever to return the same metadata contract. Supply the permitted scope from your application before constructing a prompt, and keep the refusal controls when replacing the fixture.
| Existing investment | Responsibility in this composition |
|---|---|
| LangChain retriever | Find permitted documents and retain their metadata |
| Existing worker prompt/model pipeline | Remain available for worker tasks; its conclusion is not the tool’s source record |
| Application session | Select the allowed scope and refuse a change mid-conversation |
| Rasa skill and tools | Ask for missing information, retrieve sources and explain their limits |
| Account-action service | Remain separate; no replacement tool exists here |
- Rasa asks for the missing selection through the skill; the selector is a public demonstration value.
- Application code owns document scope. The retrieval tool’s argument is the question.
- The handoff returns source records without calling the worker’s generation pipeline.
Mantle still uses its configured model for the conversation. Returning quotes directly removes an additional worker-model call from this path.
Bind the source scope before retrieval
The application handoff
asks for a demo code before it retrieves anything. Code 111111 selects a
personal-card source; 222222 selects a business-card source. Once selected,
that scope cannot change within the session.
After retrieval, the application checks every returned document against the bound scope. This also catches a faulty retriever:
The following abbreviated excerpt shows the checks at the retrieval boundary.
documents = ScopedPolicyRetriever(scope=self.scope).invoke(question)
if not documents:
return {"status": "no_evidence"}
if any(d.metadata.get("scope") != self.scope for d in documents):
return {"status": "evidence_unavailable"}
return {"status": "sources_found", "sources": [
{"id": d.metadata["id"], "revision": d.metadata["revision"], "quote": d.page_content}
for d in documents
]}- The application supplies the scope to the actual LangChain retriever.
- A wrong-scope document is refused before it becomes a conversation source.
- The model receives source records, with no worker-generated answer or account-action result.
The native mutation control makes the retriever return the business policy for a
personal demo. The guarded handoff returns evidence_unavailable. Removing
only this application’s scope comparison returns business source B-201,
including its company-administrator requirement. Preserve that control when
connecting your own search service.
A production service needs an authorised identity and a data-access policy before retrieval. Public codes and REST sender IDs do not provide those checks. This demo stores its selected scope in process memory; a restart clears it. Use durable, authorised session mapping if the conversation must survive a restart.
Return the existing sources through Mantle
The decorated tools separate selecting the demo from asking the policy question:
@tool(description="Retrieve quoted synthetic card-policy sources for the current demo. Only the user's question is an argument; the application chooses document scope. Does not replace a card or execute an account action.")
async def retrieve_card_policy(question: str, context: ToolContext = None) -> ToolResult:
return ToolResult(llm_response=session().retrieve(question))
The tool gets its session from the current turn context. Its model-facing parameter is the question, with no scope parameter. The binding controls call the real decorated tool and native retriever; separate mocked sender contexts retain separate personal and business scopes. That proves the local binding, not the authenticity of a REST caller.
The skill asks for the missing demo code, explains only returned quotations, and names their source ID and revision. On missing evidence, it asks whether the user wants to clarify the question or stop. Its instruction also forbids claiming that a card replacement has occurred. Test the final wording in your own model conversation, using the sources returned by the retriever.
Run the pinned example
Check out the qualified source rather than copying a running environment:
git clone https://github.com/RasaHQ/rasa-community-resources.git
cd rasa-community-resources
git fetch origin e7db3283483f8836f28e540349f359f51c71df6f
git checkout e7db3283483f8836f28e540349f359f51c71df6f
python3 scripts/workspace.py start tutorials/frameworks-with-rasa/langchain-mantle --owner policy-conversation-demo
cd tutorials/frameworks-with-rasa/langchain-mantle
make test
make validate
If Mantle is new to you, start with the Rasa quickstart.
Use Python 3.12 and the supplied lockfile. The selected conversation environment
installs the worker from its local source, so this route needs one environment.
The tests need no provider credentials. Set your own RASA_LICENSE for training
and OPENAI_API_KEY for an actual model conversation; never commit them.
make train
make run
The supplied channel is text REST on port 5017. In another terminal:
curl -sS http://localhost:5017/webhooks/rest/webhook \
-H 'Content-Type: application/json' \
-d '{"sender":"policy-demo","message":"Can I replace a damaged card?"}'
curl -sS http://localhost:5017/webhooks/rest/webhook \
-H 'Content-Type: application/json' \
-d '{"sender":"policy-demo","message":"Use personal demo 111111"}'
These commands are a reader exercise, not a recorded model run. Keep the
same sender when supplying the missing selection. Record the question, source
and reply. Check for personal source P-101 with revision 2026-10-11, and
check that the assistant does not say a replacement has happened. Use a new
sender for business demo 222222; its source is B-201.
What the qualification establishes
The source qualification records 25 distinct passing controls: nine citation checks, five native retrieval checks, six application handoff checks and five Mantle binding checks. Project validation, licensed training and a cold archive load passed on Rasa Pro 3.21.0.dev5 with langchain-core 1.6.6.
The review-correction run printed these test summaries, first for the core suite and then for the decorated bindings:
Ran 20 tests in 0.021s
OK
Ran 5 tests in 7.927s
OK
The summaries show elapsed time for the local test suites.
The scripted model controls exercise actual LangChain interfaces without provider requests. The controls check source scope, quotation matching and tool bindings. When using your own model, evaluate whether each answer follows from the permitted sources and whether missing sources produce the intended clarification.
Can I keep the worker's existing model pipeline?
Yes. policy_worker/pipeline.py retains the retriever, prompt and supplied model.
This conversation path calls the retriever directly. If you also return worker
conclusions, distinguish them from source records and evaluate their support;
a passing exact-quote check is not enough.
What happens when retrieval finds nothing?
The handoff returns no_evidence. The skill instructs Mantle to ask whether to
clarify or stop. The native pipeline’s no-document control makes zero calls to
its scripted worker model.
How do I retire the experiment?
Stop your own server, return to the repository root, and run
python3 scripts/workspace.py retire tutorials/frameworks-with-rasa/langchain-mantle --owner policy-conversation-demo --inactive.
Keep the source, locks and any recorded acceptance results; recreate only the
environment you need for the next experiment.