# Report an insurance decision already on file

Source: https://rasa.community/library/casebook/insurance-policy-status/
Author: Rod Rivera
Published: 2026-10-14T09:00:00.000Z

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:

```text
{"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.

::::solution{title="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.

```python
"""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.

::::checkpoint{id="insurance-policy-status-decision" question="An active policy has no recorded decision for the described loss. What may the assistant conclude?" options="The loss is covered|The policy is active; route the coverage question|The loss must be excluded" answer="1"}

The active label answers the policy-state question. It cannot supply an absent decision about this loss.

::::

## 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.

[Source excerpt](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-text-insurance-policy-status-gemini/lib/harborcover.py#L175-L193):

```python
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.

::::diagram{title="Only a decision record carries an outcome for its named claim"}

```dot
policy [label="Policy state: active"]
progress [label="Claim progress: evidence awaited"]
decision [label="Recorded coverage decision"]
question [label="New loss: route coverage question"]
scope [label="Decision applies only to its claim"]
policy -> question
progress -> question
decision -> scope
```

::::

## 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.

::::callout{type="info" title="Route a new loss to a decision owner"}

The coverage-question test returns a review reference and no decision. A routed question has not been adjudicated. The claims owner must connect it to the queue that can actually decide.

::::

## 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.

```bash
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
```

::::run{cmd="python3 -m unittest test_guard.StatusServiceTests test_guard.RoutingTests -q"}
::::

```text
----------------------------------------------------------------------
Ran 11 tests in 0.001s

OK
```

The [companion tests](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-text-insurance-policy-status-gemini/tests/test_guard.py) contain the assertions for these cases. To print the additional fixture states yourself, run this script from the same directory:

::::solution{title="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.

```python
"""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](/library/guides/make-a-release-decision/) from those observable records.

::::cta{href="/library/guides/make-a-release-decision/" label="Record the status-service release boundary"}
::::

The [complete fixture implementation](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-text-insurance-policy-status-gemini/lib/harborcover.py#L1-L319) defines the service state and remaining branches cited here.

The [pinned hook implementation](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-text-insurance-policy-status-gemini/hooks.py#L1-L145) supplies the parser and fallback functions replayed above.