AI product manager · Insurance (external)
Report an insurance decision already on file
Read the claims service’s recorded decision before an assistant falls back to saying it cannot answer.
Key takeaways (3)
- A cautious refusal can still contradict a recorded decision.
- Test the payload representation the guard receives, including serialized results.
- Assert that fallbacks preserve real recorded outcomes as well as blocking unsupported claims.
A customer asks whether their insurance claim is covered. The claims service already has a decision for that claim. The assistant should be able to report that decision without making a new coverage judgement.
You will test the part of the fictional HarborCover assistant that reads the service’s response. A deliberately broken JSON parser shows how the assistant can lose an existing decision and give an inaccurate refusal.
We replayed the pinned HarborCover hook functions with a recorded claim and an authored uncited reply. These actual offline outputs compare its JSON parser with a deliberately changed dictionary-only parser:
{"decisions_retained": 1, "fallback": "The HarborCover claims service has recorded this decision: claim CLM-24-0913: covered subject to the 500 USD collision deductible (decision HC-DEC-50412). It applies to that claim only. For any other loss I can pass a question to the claims team and give you a reference.", "variant": "pinned_json_parser"}
{"decisions_retained": 0, "fallback": "I can't tell you whether that loss is covered. Only the HarborCover claims service can decide that, and there is no decision on it. I can pass the question to them and give you a reference. Would you like me to do that?", "variant": "dict_only_parser"}
Replay the payload that the guard actually reads
The same claim decision enters both arms as serialized JSON. The pinned parser retains it. The changed parser accepts dictionaries only and loses the decision. After the authored reply reaches the retry limit, one arm reports the recorded decision and the other says there is no decision.
This replay extracts the unchanged hook functions and supplies stand-in payloads and a silent logger. It removes their registration decorators. It does not run the Rasa engine, a model or a real insurer. It tests the Python parsing and fallback logic only.
| Parser arm | Decisions retained | Fallback |
|---|---|---|
| Pinned JSON handling | The recorded decision | Reports that decision and its claim |
| Dictionary-only mutation | None | Incorrectly says there is no decision |
A test over hand-built dictionaries would miss the second arm’s failure. Use the actual serialized payload shape when checking a guard, and assert that it preserves a recorded outcome as well as blocking unsupported ones.
Show the counterexperiment script
Run this from the pinned companion tests directory shown below: save it as counterexperiment.py, then run python3 counterexperiment.py. It changes no files and uses no model or provider.
"""Pure Python replay of pinned hook functions with stand-in payloads.
This is not a Rasa-engine integration test and generates no model replies.
Run from the companion project's tests directory.
"""
import ast
import asyncio
import json
from collections import defaultdict
from pathlib import Path
from types import SimpleNamespace
import test_guard as t
class Payload(SimpleNamespace):
def model_copy(self, update):
return Payload(**{**vars(self), **update})
class Retry(Exception):
pass
source = (Path.cwd().parent / "hooks.py").read_text()
names = {"_as_dict", "_decision_lines", "decision_feedback", "decision_text", "remember_coverage_decisions", "block_coverage_promises"}
functions = []
for node in ast.parse(source).body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name in names:
node.decorator_list = []
functions.append(node)
assert len(functions) == len(names)
constants = {}
for node in ast.parse(source).body:
if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name) and node.targets[0].id in {"NO_DECISION_TEXT", "NO_DECISION_FEEDBACK", "MAX_CONSECUTIVE_RETRIES"}:
constants[node.targets[0].id] = ast.literal_eval(node.value)
for variant in ("pinned_json_parser", "dict_only_parser"):
ns = dict(constants, json=json, coverage_promises=t.hc.coverage_promises, _decisions=defaultdict(dict), _retries=defaultdict(int), ModelResponsePayload=Payload, ToolResultPayload=Payload, RetryModel=Retry, log=SimpleNamespace(warning=lambda *args, **kwargs: None))
exec(compile(ast.fix_missing_locations(ast.Module(body=functions, type_ignores=[])), "pinned-hook-functions", "exec"), ns)
if variant == "dict_only_parser":
ns["_as_dict"] = lambda value: value if isinstance(value, dict) else {}
value = json.dumps(t.hc.claim_status(t.ME, "CLM-24-0913"))
asyncio.run(ns["remember_coverage_decisions"](Payload(sender_id="offline", value=value)))
for _ in range(ns["MAX_CONSECUTIVE_RETRIES"]):
try:
asyncio.run(ns["block_coverage_promises"](Payload(sender_id="offline", text="The claim is covered.")))
except Retry:
pass
else:
raise AssertionError("Expected a retry for the uncited authored reply")
result = asyncio.run(ns["block_coverage_promises"](Payload(sender_id="offline", text="The claim is covered.")))
print(json.dumps({"variant": variant, "decisions_retained": len(ns["_decisions"].get("offline", {})), "fallback": result.text}, sort_keys=True))
assert ("HC-DEC-50412" in result.text) == (variant == "pinned_json_parser")
assert ("there is no decision" in result.text) == (variant == "dict_only_parser")Use the record type to decide what may be said
A product manager needs an answer boundary that can be reviewed without interpreting every model reply. Start with the questions the service records can answer.
| Record type | Question it answers | It cannot establish |
|---|---|---|
| Policy status | Is this recorded policy active or lapsed? | Whether a described loss is covered |
| Claim progress | Where does this claim stand? | The outcome of a coverage decision |
| Coverage decision | What was decided for this named claim? | Another claim’s outcome |
The sample puts those types in the tool result. An empty coverage field is deliberate. It is not an invitation for the model to finish an incomplete answer.
Inspect a status receipt without inventing a decision
The policy tool returns state, effective dates and observation time, alongside an empty coverage field. This excerpt describes the answer it can support.
answer = {
"status": "answered",
"decision_type": "policy_status",
"policy_number": number,
"product": record["product"],
"state": record["state"],
"effective_from": record["effective_from"],
"effective_to": record["effective_to"],
"observed_at": record["observed_at"],
"status_reference": status_reference(data, customer_id, number, record["observed_at"]),
"coverage_decision": None,
"not_a_coverage_decision": NOT_A_COVERAGE_DECISION,
"facts": facts,
"next_step": (
"Report the state, dates, observation time and reference. If the "
"caller asks whether a particular loss is covered, offer "
"open_coverage_question instead of answering from the state."
),
}
The claim tool has a different result. Its source carries the decision type and a scope note when the record represents a coverage decision. The tests cover the particular decided claim and the progress-only claim; they do not adjudicate either loss.

Make an old observation visible
A stale claim result is blocked as current status, but preserves the last-known stage and timestamp. Keep that date in the support record. Withholding a current answer does not mean the old observation was false.
The claims owner should define when an observation expires and how it is refreshed. This sample uses the fixture’s declared clock and policy. A production decision needs the data source’s update contract, not a confidence score from the model.
Decide what is ready to release
Approve separate response contracts for status, progress and a recorded decision. Require the record subject, observation time and decision scope in the supporting evidence. Where one is absent, assign the review route rather than allowing a guessed result.
This is a product boundary, not a policy interpretation service. The offline tests read synthetic records. They do not establish policy terms, coverage entitlement or accuracy of a real insurer’s source data.
Replay the answer types and stale-status route
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-text-insurance-policy-status-gemini/tests
- Any system
python3 -m unittest test_guard.StatusServiceTests test_guard.RoutingTests -q
----------------------------------------------------------------------
Ran 11 tests in 0.001s
OK
The companion tests contain the assertions for these cases. To print the additional fixture states 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
policy = t.hc.policy_status(t.ME, "HC-HO-440120")
print(json.dumps({"type": policy["decision_type"], "state": policy["state"], "coverage_decision": policy["coverage_decision"]}, sort_keys=True))
for number in ("CLM-24-0871", "CLM-24-0913"):
claim = t.hc.claim_status(t.ME, number)
print(json.dumps({"claim": number, "type": claim["decision_type"], "decision_reference": (claim["coverage_decision"] or {}).get("reference"), "scope": (claim["coverage_decision"] or {}).get("applies_to")}, sort_keys=True))Give the claims owner the three receipt types above and ask the conversation designer to write one response per type. Use an old observation and a new loss as the refusal cases. Make a release decision from those observable records.
The complete fixture implementation defines the service state and remaining branches cited here.
The pinned hook implementation supplies the parser and fallback functions replayed above.