Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions use-cases/aayushmishraaa/payer-appeals/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copy to .env and fill in. Never commit .env.
SUPERDOCS_API_KEY=your-key-here
SUPERDOCS_BASE_URL=https://api.superdocs.app
4 changes: 4 additions & 0 deletions use-cases/aayushmishraaa/payer-appeals/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.env
.venv/
__pycache__/
*.pyc
131 changes: 131 additions & 0 deletions use-cases/aayushmishraaa/payer-appeals/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Payer appeals — drafted, reviewed, exported

When a health insurer denies a claim, someone on the provider side writes an appeal letter that
pairs the payer's stated reason for denial with the specific clinical facts that rebut it.
Getting that pairing right by hand is slow. Getting it *wrong* by machine is worse.

This app takes **one denial reason and one clinical fact**, drafts an appeal letter on the
SuperDocs API, holds every proposed change for a human decision, and exports only what a person
approved.

Built by **Aayush Mishra** for the SuperDocs engineer task.

---

## Run it

```bash
cp .env.example .env # add your SUPERDOCS_API_KEY
./run.sh # http://localhost:8080
```

That is the whole setup. Everything in the demo is fictional and the UI says so in a banner
that does not go away.

---

## The thing this app is actually about

A model given a denial reason and a clinical fact will write a fluent, professional appeal
letter **whether or not the fact answers the denial**. When they do not match, the way it
bridges the gap is by inventing clinical detail.

A fabricated clinical assertion in a payer appeal is not a bad user experience. It is a false
statement submitted to an insurer over a clinician's name.

So the app does three things a plain wrapper would not:

### 1. It checks relevance *before* drafting

`app/grounding.py` classifies the denial into a category — prior authorisation, medical
necessity, timely filing, coding, eligibility, duplicate — and asks whether the supplied fact
speaks to that category. The verdict is shown to the reviewer before they read a word of the
letter.

### 2. When the fact does not fit, it says so in the letter

It still drafts. Refusing would just push the user to write it by hand with no warning at all.
But it drafts honestly. Given a prior-authorisation denial and a fact about seasonal allergies,
the letter it produced was:

> "Please note that the documentation directly addressing the prior authorization requirement
> is **not enclosed** with this letter. We will forward it..."

It stated the fact accurately, admitted the gap, and asked for reconsideration once real
documentation is supplied. It did not manufacture a rebuttal.

### 3. It flags specifics nobody supplied

`unsupported_specifics()` finds numbers, dates, dosages and measurements that appear in the
drafted letter but in none of the inputs. It cannot judge meaning — but a payer will verify
every concrete detail, and a clinician will be asked to stand behind it, so the reviewer sees
those before sending.

---

## The human gate is real

- Every proposed change is shown with **before and after**, and decided individually.
- Rejecting one leaves the others untouched.
- **Export is refused with a 409 while any decision is outstanding.** A review you can skip is
decoration.
- A large edit arrives in **several rounds**; the app tells the reviewer when more changes are
waiting rather than letting them believe they have seen the whole letter.

In a live run: 4 changes proposed, 3 approved and 1 rejected. In the exported `.docx` the
rejected section still reads `[What is enclosed, and what is not.]` — the placeholder was never
filled, because that change was rejected. The rejection is visible in the artefact.

---

## What it uses

| SuperDocs surface | Used for |
| --- | --- |
| `POST /v1/chat/async` with `approval_mode: ask_every_time` | Draft into the letter template without self-applying |
| `GET /v1/jobs/{id}` | Poll until a human decision is required |
| `POST /v1/chat/{session}/approve` | Per-change approve/reject with feedback |
| `POST /v1/documents/export` | `.docx` / `.pdf` |
| `GET /v1/sessions/{id}/jobs` | Recover a session wedged by an abandoned review |

The letter starts from a **template document** rather than being generated from nothing. That
is deliberate: appeal letters have a house format a compliance team already approved, and
starting from a document means the model performs targeted edits on named sections — which is
what SuperDocs is good at — rather than generating prose, which everything is good at.

---

## Things I learned against the live API

Encoded here so the next person does not spend the same afternoon:

- The upload field is **`file_base64`**, not `content_base64`. The 422 body is the only place
the correct name appears.
- `pending_changes` can be present with a **null** value, so `.get("pending_changes", [])`
returns `None`, not `[]`. That crashed this app once.
- A large edit arrives in **several approval rounds**. Approve once and wait for completion and
you poll forever.
- A job stuck in `awaiting_approval` **cannot be cancelled** — `cancel_job` returns
`400 "Job cannot be cancelled"` — even though the 409's own `suggested_action` recommends
exactly that. The way out is to **deny** its pending changes.

---

## Honest limitations

- **In-memory drafts.** A restart loses anything in flight. Fine for a demo, not for a clinic.
- **The relevance check is coarse.** It is keyword-and-category matching, not clinical
judgement. It reliably catches the obviously-wrong pairing; it will not catch a fact that is
topically right and substantively weak. It exists to make a reviewer look, not to decide.
- **`unsupported_specifics` is not a hallucination detector.** It finds concrete tokens absent
from the inputs. A fabricated *qualitative* claim — "the patient's condition was severe" —
passes it untouched.
- **One fact per appeal.** Real appeals cite several. The card specified one, and the pairing
logic is what is being demonstrated.
- **No authentication, no multi-user.** Single-reviewer demo.
- **Not a medical device and not legal advice.** It drafts a letter a qualified human must read
before it goes anywhere.

## License

MIT.
Empty file.
175 changes: 175 additions & 0 deletions use-cases/aayushmishraaa/payer-appeals/app/grounding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"""Does the supplied clinical fact actually address the denial reason?

This module exists because of the specific way an appeal-drafting tool can be dangerous.

The reviewer supplies one denial reason and one clinical fact. Sometimes the fact rebuts the
denial. Sometimes it plainly does not — a note about a patient's blood pressure does not answer
a denial for missing prior authorisation. A model asked to "write an appeal" from those two
inputs will write a fluent, confident, professional letter either way, and the way it bridges
the gap is by inventing clinical detail that nobody documented.

A fabricated clinical assertion in a payer appeal is not a bad user experience. It is a false
statement submitted to an insurer over a clinician's name.

So relevance is checked BEFORE drafting, and the result is shown to the reviewer either way.
The tool still drafts when the fact does not fit — refusing would just push the user to write it
by hand with no warning at all — but it drafts a letter that says what it actually has, and it
tells the reviewer plainly that the fact does not address the stated ground.
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from enum import StrEnum


class Relevance(StrEnum):
ADDRESSES = "addresses"
"""The fact bears directly on the stated ground for denial."""

PARTIAL = "partial"
"""Related, but does not by itself answer the ground given."""

DOES_NOT_ADDRESS = "does_not_address"
"""No discernible connection. Drafting will not invent one."""


@dataclass(frozen=True, slots=True)
class RelevanceCheck:
verdict: Relevance
reason: str
matched_theme: str = ""

@property
def should_warn(self) -> bool:
return self.verdict is not Relevance.ADDRESSES


# Denial grounds and the clinical evidence that actually answers them. Data, not code: a new
# denial category is an entry here.
#
# These are deliberately coarse. The check is a guard against the obviously-wrong pairing, not
# a clinical judgement — it exists to make the reviewer look, not to decide for them.
THEMES: tuple[tuple[str, tuple[str, ...], tuple[str, ...]], ...] = (
(
"prior authorisation",
("prior auth", "precert", "pre-cert", "preauth", "authorization", "authorisation",
"carc 197", "no auth", "notification absent"),
("authorization", "authorisation", "auth number", "approved", "reference",
"pa-", "certification", "obtained", "granted", "on file"),
),
(
"medical necessity",
("medical necessity", "not medically necessary", "carc 50", "not deemed",
"experimental", "investigational"),
("conservative", "failed", "physical therapy", "nsaid", "injection", "imaging",
"mri", "radiograph", "x-ray", "grade", "stenosis", "refractory", "symptoms",
"diagnosis", "documented", "weeks", "months", "trial of"),
),
(
"timely filing",
("timely filing", "filed late", "untimely", "carc 29", "past the deadline"),
("submitted", "date of submission", "postmark", "acknowledg", "received on",
"claim date", "resubmit"),
),
(
"coding",
("coding", "invalid code", "cpt", "modifier", "unbundl", "carc 16", "ma130",
"incomplete", "invalid information", "missing information"),
("cpt", "code", "modifier", "icd", "npi", "corrected claim", "operative report",
"procedure performed"),
),
(
"eligibility",
("eligibility", "not covered", "coverage terminated", "not eligible",
"member not found", "carc 27"),
("active coverage", "eligibility", "effective date", "enrolled", "plan end",
"verification", "270", "271"),
),
(
"duplicate",
("duplicate", "already adjudicated", "carc 18"),
("distinct", "separate", "different date", "bilateral", "modifier 59", "unrelated"),
),
)


def _hits(text: str, needles: tuple[str, ...]) -> list[str]:
lowered = text.lower()
return [n for n in needles if n in lowered]


def check(denial_reason: str, clinical_fact: str) -> RelevanceCheck:
"""Decide whether `clinical_fact` bears on `denial_reason`."""
if not denial_reason.strip():
return RelevanceCheck(
Relevance.DOES_NOT_ADDRESS, "No denial reason was supplied, so nothing to answer."
)
if not clinical_fact.strip():
return RelevanceCheck(
Relevance.DOES_NOT_ADDRESS,
"No clinical fact was supplied. An appeal with no supporting fact is a letter of "
"opinion, and this tool will not dress one up as evidence.",
)

for theme, denial_markers, evidence_markers in THEMES:
if not _hits(denial_reason, denial_markers):
continue

evidence = _hits(clinical_fact, evidence_markers)
if evidence:
return RelevanceCheck(
Relevance.ADDRESSES,
f"The denial is on {theme} grounds and the clinical fact speaks to it "
f"(matched: {', '.join(evidence[:3])}).",
matched_theme=theme,
)

return RelevanceCheck(
Relevance.DOES_NOT_ADDRESS,
f"The denial is on {theme} grounds, but the clinical fact supplied does not "
f"appear to speak to {theme}. The letter will state the fact accurately and will "
f"NOT claim it rebuts this ground — check whether you meant to supply different "
f"documentation.",
matched_theme=theme,
)

# The denial did not match a known theme. Say so rather than guessing, and let the letter
# be drafted conservatively.
return RelevanceCheck(
Relevance.PARTIAL,
"The denial reason does not match a recognised category, so relevance could not be "
"assessed. The letter will pair them as given; read it before sending.",
)


# --------------------------------------------------------------------------------------
# Post-draft check: did the letter invent anything?
# --------------------------------------------------------------------------------------

# Clinical specifics that would be alarming in a letter if they never appeared in the input:
# a fabricated measurement, date or dosage reads as documented fact to a payer.
_SPECIFIC = re.compile(
r"\b\d+\s?(?:mg|mcg|ml|mm|cm|weeks?|months?|years?|days?|sessions?|degrees?)\b"
r"|\b\d{4}-\d{2}-\d{2}\b"
r"|\bgrade\s+[1-4iv]+\b"
r"|\b\d+\s?(?:percent|%)\b",
re.IGNORECASE,
)


def unsupported_specifics(draft: str, *sources: str) -> list[str]:
"""Clinical specifics in the draft that appear in none of the source inputs.

Not a hallucination detector — it cannot judge meaning. It catches the concrete,
checkable case: a number, date or dosage the letter asserts that nobody supplied. Those
are exactly the details a payer will verify and a clinician will be asked to stand behind.
"""
haystack = " ".join(sources).lower()
found: list[str] = []
for match in _SPECIFIC.finditer(draft):
token = match.group(0)
if token.lower() not in haystack and token not in found:
found.append(token)
return found
Loading