Skip to content
RasaGet a free licence
AI team casebook

AI product engineer · Insurance (external)

Mentioning an attachment does not mean it was received

Read attachment states from the intake service, preserve missing files as follow-up work, and wait for a claim acknowledgement before saying filed.

by Rod Rivera

About 6 minutesCase study

Key takeaways (3)
  • Read upload status from the service, not from a customer’s claim that they sent a file.
  • A scanning file can change the report the customer needs to confirm.
  • Filing a report does not establish coverage or a claims decision.

The customer says they sent photos and an invoice. Both remain “not provided” in the report. In another case, a scanning attachment blocks filing. When a scan changes the report version, the old confirmation is refused too.

This is actual offline output from fictional HarborCover intake:

{"follow_up": ["not provided", "not provided"]}
{"filed": false, "reason": "attachment_state_unknown", "status": "blocked", "submissions": 0}
{"new_version": 2, "old_confirmation_reason": "unconfirmed_report", "submissions": 0}

Build the report from upload evidence

The attachment service establishes whether a file arrived. A sentence in chat can describe an upload, but it cannot serve as its receipt. The material record distinguishes received, failed, not provided and unknown files.

In this fixture, a scan advances after another customer message. That is its test clock, not a production design. A real scanner or upload service must supply the transition. Keep a durable file identifier and its report association.

Customer mentions a file Read attachment service state Absent: retain follow-up item Scan unresolved: wait Received: include in draft Read current report back
FigureUpload state changes the material the customer confirms

Keep missing and unresolved material different

The guard needs each required attachment state to be known. It does not insist that every item has arrived. A known missing item remains follow-up work. An unresolved scan cannot yet tell the report what material it contains.

Attachment stateReport treatmentSubmission consequence in the sample
ReceivedLink the fileCan contribute to a confirmed report
Not provided or failedKeep the missing requirementKnown follow-up remains visible
Scan unresolvedKeep state unknownSubmission blocked
Explicitly left outKeep it as still neededState becomes known; report changes

Leaving a stuck file out is an explicit choice, not deletion of the requirement. The attachment test checks that the repair estimate remains needed. The claims owner must decide which requirements may follow after intake; the exercise is not an insurer’s policy.

Reconfirm the material that will actually be submitted

The trace checks an old confirmation after the bike-report scan completes. The draft has a new version. Submission refuses that old read-back and creates no claim.

This is the whole confirmation predicate. It compares the report tag, fresh material and the question the customer answered, rather than trusting a model-supplied ready flag.

Source excerpt:

def report_confirmed(service: ClaimsIntake, draft: Draft, memory: dict, conversation: Conversation,
                     fresh_material: dict) -> bool:
    """The engine read back this exact version and the customer answered it."""
    return (
        memory.get("claim_draft_id") == draft.draft_id
        and memory.get("claim_draft_tag") == draft.tag
        and fresh_material == draft.material
        and conversation.confirmation_question is not None
        and draft.tag in conversation.confirmation_question
        and conversation.confirmation_answered
    )

Submission refreshes the material and uses that predicate alongside the attachment-state check. The refusal returns the requirements still scanning.

Source excerpt:

fresh = material(service, draft, conversation)
facts = {
    "report_confirmed": report_confirmed(service, draft, memory, conversation, fresh),
    "required_attachment_state_known": fresh["required_attachment_state_known"],
}
reason = evaluate(facts, "request")
if reason:
    return {
        "status": "blocked", "reason": reason, "draft_id": draft.draft_id, "draft_tag": draft.tag,
        "facts": facts, "effects": 0, "filed": False,
        "still_scanning": [r["requirement"] for r in fresh["required"] if r["state"] == "unknown"],
        "next_step": (
            "Nothing was submitted. The engine has not read this version of the report back to the customer. "
            "Call check_attachments for the draft, then submit_claim_report again so the engine asks them."
            if reason == "unconfirmed_report" else
            "Nothing was submitted. A required file is still being scanned. Tell the customer, and offer to check "
            "again after their next message or to leave the file out so it is listed as still needed."
        ),
    }

Check the filing acknowledgement separately

An upload receipt proves a file arrived. It is not a filing receipt. The intake acknowledgement proves the report was accepted. It is not a coverage decision. Keep those three statements separate in the customer message and the data model.

This requires more state than a single filed flag. It also lets support identify which step is unresolved without asking the customer to upload or submit everything again. These checks do not upload files, scan malware or adjudicate any claim.

Replay the attachment and submission boundaries

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-file-claim-claude/tests
Any system
python3 -m unittest test_guard.AttachmentTests test_guard.SubmissionTests test_guard.ReceiptMessageTests -q
----------------------------------------------------------------------
Ran 16 tests in 0.002s

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

service = t.hc.ClaimsIntake()
messages = ["I sent you the photos and the invoice already."]
draft = t.drafted(service, messages)
print(json.dumps({"follow_up": [item["why"] for item in t.hc.follow_up_required(draft.material)]}, sort_keys=True))
service = t.hc.ClaimsIntake()
messages = ["Window. [attached: window-crack.jpg, glazier-quote.pdf]"]
draft = t.drafted(service, messages, loss_type="accidental_damage")
result = t.hc.submit_claim_report(service, t.ME, t.hc.memory_values(service, draft), t.confirmed(service, draft, messages), draft.draft_id)
print(json.dumps({"status": result["status"], "reason": result["reason"], "filed": result["filed"], "submissions": len(service.submissions)}, sort_keys=True))

service = t.hc.ClaimsIntake()
messages = ["My bike was stolen. [attached: bike-receipt.pdf, police-report.pdf]"]
draft = t.drafted(service, messages, loss_type="theft")
old_question = t.question_for(service, draft)
later = [*messages, "Can you check it again?"]
view = t.hc.check_attachments(service, t.ME, t.convo(*later), draft.draft_id)[0]
result = t.hc.submit_claim_report(service, t.ME, t.hc.memory_values(service, draft), t.convo(*later, question=old_question), draft.draft_id)
assert result["reason"] == "unconfirmed_report"
assert service.submissions == {}
print(json.dumps({"new_version": view["draft_version"], "old_confirmation_reason": result["reason"], "submissions": len(service.submissions)}, sort_keys=True))

Give the intake owner received, failed, absent and scanning events, then replay a material change after read-back. Ask the conversation designer to inspect the missing-item receipt. Audit a document the agent produces against the service evidence it uses.

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