Deploy ClaimReady: on-device Gemma 3 via llama.cpp
Browse files- .gitignore +4 -0
- README.md +18 -8
- app.py +164 -0
- llm.py +321 -0
- packages.json +446 -0
- packages.py +94 -0
- requirements.txt +6 -0
.gitignore
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
__pycache__/
|
| 3 |
+
venv/
|
| 4 |
+
*.pyc
|
README.md
CHANGED
|
@@ -1,15 +1,25 @@
|
|
| 1 |
---
|
| 2 |
-
title: Claim
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.18.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
-
license: apache-2.0
|
| 12 |
short_description: Catch claim errors before you submit — runs locally
|
| 13 |
---
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ClaimReady — Claim Submission Check
|
| 3 |
+
emoji: 🏥
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: gradio
|
|
|
|
|
|
|
| 7 |
app_file: app.py
|
| 8 |
pinned: false
|
|
|
|
| 9 |
short_description: Catch claim errors before you submit — runs locally
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# 🏥 ClaimReady
|
| 13 |
+
|
| 14 |
+
Pre-check hospital insurance **claim** and **pre-authorization** documents against the
|
| 15 |
+
Standard Treatment Guidelines **before** they are uploaded to the government portal —
|
| 16 |
+
catching compliance errors early to reduce rejections.
|
| 17 |
+
|
| 18 |
+
**Runs fully on-device:** OCR + content verification are done by **Gemma 3 12B**
|
| 19 |
+
(a small, ≤32B open model) through **llama.cpp** — no cloud APIs.
|
| 20 |
+
|
| 21 |
+
## How it works
|
| 22 |
+
1. Pick a package code and stage (pre-auth / claim).
|
| 23 |
+
2. Upload the supporting documents (images or PDFs).
|
| 24 |
+
3. The model OCRs each file and checks it against the required-document checklist
|
| 25 |
+
and the STG content rules, returning a ✅ / ❌ / ⚠️ verdict.
|
app.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hospital Claim Verification — PMJAY compliance pre-check.
|
| 3 |
+
|
| 4 |
+
A Gradio app for hospital billing staff to verify PMJAY pre-auth / claim document
|
| 5 |
+
sets against the Standard Treatment Guidelines BEFORE uploading to the Government
|
| 6 |
+
of India portal — catching compliance errors early to reduce rejections.
|
| 7 |
+
|
| 8 |
+
Step 2: verification wired to Gemma 3 12B (via OpenRouter) for OCR + content
|
| 9 |
+
checks against the Standard Treatment Guidelines.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import gradio as gr
|
| 13 |
+
|
| 14 |
+
from llm import LLMConfigError, verify
|
| 15 |
+
from packages import (
|
| 16 |
+
STAGE_PREAUTH,
|
| 17 |
+
STAGE_CLAIM,
|
| 18 |
+
STAGE_LABELS,
|
| 19 |
+
dropdown_choices,
|
| 20 |
+
get_package,
|
| 21 |
+
required_documents,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
STAGE_RADIO_CHOICES = [
|
| 25 |
+
(STAGE_LABELS[STAGE_PREAUTH], STAGE_PREAUTH),
|
| 26 |
+
(STAGE_LABELS[STAGE_CLAIM], STAGE_CLAIM),
|
| 27 |
+
]
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def package_info_md(code):
|
| 31 |
+
"""Markdown summary of the selected package."""
|
| 32 |
+
pkg = get_package(code)
|
| 33 |
+
if not pkg:
|
| 34 |
+
return ""
|
| 35 |
+
return (
|
| 36 |
+
f"**{pkg['code']} — {pkg['name']}** \n"
|
| 37 |
+
f"Procedure: {pkg.get('procedure', '—')} \n"
|
| 38 |
+
f"Specialty: {pkg.get('specialty', '—')} \n"
|
| 39 |
+
f"Package price: {pkg.get('price', '—')}"
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def checklist_md(code, stage):
|
| 44 |
+
"""Markdown checklist of required documents for a package + stage."""
|
| 45 |
+
if not code or not stage:
|
| 46 |
+
return "_Select a package and stage to see the required documents._"
|
| 47 |
+
pkg = get_package(code)
|
| 48 |
+
docs = required_documents(code, stage)
|
| 49 |
+
if not docs:
|
| 50 |
+
return "_No documents configured for this selection._"
|
| 51 |
+
|
| 52 |
+
lines = [
|
| 53 |
+
f"### Required documents — {STAGE_LABELS[stage]}",
|
| 54 |
+
f"_{pkg['name']} ({pkg['code']})_",
|
| 55 |
+
"",
|
| 56 |
+
]
|
| 57 |
+
for i, doc in enumerate(docs, 1):
|
| 58 |
+
line = f"{i}. **{doc['label']}** — {doc['verify']}"
|
| 59 |
+
if doc.get("note"):
|
| 60 |
+
line += f" \n _Note: {doc['note']}_"
|
| 61 |
+
lines.append(line)
|
| 62 |
+
|
| 63 |
+
rules = pkg.get("rules", [])
|
| 64 |
+
if rules:
|
| 65 |
+
lines.append("")
|
| 66 |
+
lines.append("**Compliance checks (content):**")
|
| 67 |
+
for r in rules:
|
| 68 |
+
lines.append(f"- {r['check']}")
|
| 69 |
+
return "\n".join(lines)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def on_package_change(code, stage):
|
| 73 |
+
return package_info_md(code), checklist_md(code, stage)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def on_stage_change(code, stage):
|
| 77 |
+
return checklist_md(code, stage)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def run_verification(code, stage, files):
|
| 81 |
+
"""OCR the uploaded files and verify them against the checklist + STG rules.
|
| 82 |
+
|
| 83 |
+
Delegates to the Gemma 3 12B model (via OpenRouter) in llm.verify.
|
| 84 |
+
"""
|
| 85 |
+
if not code:
|
| 86 |
+
return "⚠️ Please select a package code first."
|
| 87 |
+
if not stage:
|
| 88 |
+
return "⚠️ Please select a stage (Pre-Auth or Claim)."
|
| 89 |
+
if not files:
|
| 90 |
+
return "⚠️ Please upload at least one document to verify."
|
| 91 |
+
|
| 92 |
+
pkg = get_package(code)
|
| 93 |
+
docs = required_documents(code, stage)
|
| 94 |
+
|
| 95 |
+
# Gradio gives file paths (str) or objects with a .name attribute.
|
| 96 |
+
paths = [getattr(f, "name", f) for f in files]
|
| 97 |
+
|
| 98 |
+
try:
|
| 99 |
+
return verify(pkg, STAGE_LABELS[stage], docs, pkg.get("rules", []), paths)
|
| 100 |
+
except LLMConfigError as e:
|
| 101 |
+
return f"## ⚠️ Configuration error\n\n{e}"
|
| 102 |
+
except Exception as e: # surface API/network errors to the user, don't crash the UI
|
| 103 |
+
return (
|
| 104 |
+
"## ❌ Verification failed\n\n"
|
| 105 |
+
f"The model call did not complete: `{type(e).__name__}: {e}`\n\n"
|
| 106 |
+
"Check your OpenRouter key/credits and network connection, then retry."
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
with gr.Blocks(title="Hospital Claim Verification — PMJAY") as demo:
|
| 111 |
+
gr.Markdown(
|
| 112 |
+
"# 🏥 Hospital Claim Verification\n"
|
| 113 |
+
"Pre-check PMJAY **pre-authorization** and **claim submission** documents "
|
| 114 |
+
"against the Standard Treatment Guidelines — *before* uploading to the "
|
| 115 |
+
"Government portal. Catch compliance errors early and reduce rejections."
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
with gr.Row():
|
| 119 |
+
with gr.Column(scale=1):
|
| 120 |
+
package_dd = gr.Dropdown(
|
| 121 |
+
choices=dropdown_choices(),
|
| 122 |
+
label="1. Select Package Code",
|
| 123 |
+
info="PMJAY Health Benefit Package",
|
| 124 |
+
)
|
| 125 |
+
package_info = gr.Markdown()
|
| 126 |
+
stage_radio = gr.Radio(
|
| 127 |
+
choices=STAGE_RADIO_CHOICES,
|
| 128 |
+
label="2. Select Stage",
|
| 129 |
+
info="Pre-auth = before treatment · Claim = after treatment",
|
| 130 |
+
)
|
| 131 |
+
files_in = gr.File(
|
| 132 |
+
label="3. Upload documents",
|
| 133 |
+
file_count="multiple",
|
| 134 |
+
file_types=["image", ".pdf"],
|
| 135 |
+
)
|
| 136 |
+
verify_btn = gr.Button("Run Verification", variant="primary")
|
| 137 |
+
|
| 138 |
+
with gr.Column(scale=1):
|
| 139 |
+
checklist_out = gr.Markdown(
|
| 140 |
+
"_Select a package and stage to see the required documents._"
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
gr.Markdown("---")
|
| 144 |
+
result_out = gr.Markdown()
|
| 145 |
+
|
| 146 |
+
package_dd.change(
|
| 147 |
+
on_package_change,
|
| 148 |
+
inputs=[package_dd, stage_radio],
|
| 149 |
+
outputs=[package_info, checklist_out],
|
| 150 |
+
)
|
| 151 |
+
stage_radio.change(
|
| 152 |
+
on_stage_change,
|
| 153 |
+
inputs=[package_dd, stage_radio],
|
| 154 |
+
outputs=[checklist_out],
|
| 155 |
+
)
|
| 156 |
+
verify_btn.click(
|
| 157 |
+
run_verification,
|
| 158 |
+
inputs=[package_dd, stage_radio, files_in],
|
| 159 |
+
outputs=[result_out],
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
if __name__ == "__main__":
|
| 164 |
+
demo.launch()
|
llm.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LLM verification — local, on-device (no cloud APIs).
|
| 3 |
+
|
| 4 |
+
Runs Gemma 3 (a small, <=32B open model) through llama.cpp via llama-cpp-python:
|
| 5 |
+
1. OCR / read each uploaded file (images, or PDF pages rendered to images), and
|
| 6 |
+
2. Check the document set against the required-documents checklist and the STG
|
| 7 |
+
content rules for the selected package + stage.
|
| 8 |
+
|
| 9 |
+
Model is configurable by environment variable so the same code runs a small
|
| 10 |
+
model locally (e.g. Gemma 3 4B on a 16GB laptop) and the full model on the
|
| 11 |
+
Space (Gemma 3 12B on 32GB):
|
| 12 |
+
|
| 13 |
+
CLAIMREADY_GGUF_REPO default: ggml-org/gemma-3-12b-it-GGUF
|
| 14 |
+
CLAIMREADY_GGUF_FILE default: gemma-3-12b-it-Q4_K_M.gguf
|
| 15 |
+
CLAIMREADY_MMPROJ_FILE default: mmproj-model-f16.gguf
|
| 16 |
+
CLAIMREADY_N_CTX default: 8192
|
| 17 |
+
|
| 18 |
+
PDFs are rendered to images with PyMuPDF (no poppler), images are downscaled to
|
| 19 |
+
keep vision encoding fast, and everything is fed to the model as images. The
|
| 20 |
+
model is asked to return strict JSON, which we render into a readable verdict.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
import base64
|
| 24 |
+
import io
|
| 25 |
+
import json
|
| 26 |
+
import os
|
| 27 |
+
|
| 28 |
+
import fitz # PyMuPDF
|
| 29 |
+
from PIL import Image
|
| 30 |
+
|
| 31 |
+
# Vision tiling: keep the longest side at/under this so each image is ~one tile.
|
| 32 |
+
_MAX_IMAGE_SIDE = 896
|
| 33 |
+
# Safety cap on total images per verification (PDF pages + image files).
|
| 34 |
+
_MAX_IMAGES = 16
|
| 35 |
+
# DPI used when rasterizing PDF pages.
|
| 36 |
+
_PDF_DPI = 150
|
| 37 |
+
|
| 38 |
+
_IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tiff", ".tif"}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class LLMConfigError(RuntimeError):
|
| 42 |
+
"""Raised when the model cannot be loaded."""
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# --- Model (loaded once, lazily) ----------------------------------------------
|
| 46 |
+
|
| 47 |
+
_MODEL = None
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _get_model():
|
| 51 |
+
global _MODEL
|
| 52 |
+
if _MODEL is not None:
|
| 53 |
+
return _MODEL
|
| 54 |
+
try:
|
| 55 |
+
from huggingface_hub import hf_hub_download
|
| 56 |
+
from llama_cpp import Llama
|
| 57 |
+
from llama_cpp.llama_chat_format import MTMDChatHandler
|
| 58 |
+
|
| 59 |
+
repo = os.environ.get("CLAIMREADY_GGUF_REPO", "ggml-org/gemma-3-12b-it-GGUF")
|
| 60 |
+
model_file = os.environ.get("CLAIMREADY_GGUF_FILE", "gemma-3-12b-it-Q4_K_M.gguf")
|
| 61 |
+
mmproj_file = os.environ.get("CLAIMREADY_MMPROJ_FILE", "mmproj-model-f16.gguf")
|
| 62 |
+
n_ctx = int(os.environ.get("CLAIMREADY_N_CTX", "8192"))
|
| 63 |
+
|
| 64 |
+
model_path = hf_hub_download(repo, model_file)
|
| 65 |
+
mmproj_path = hf_hub_download(repo, mmproj_file)
|
| 66 |
+
handler = MTMDChatHandler(clip_model_path=mmproj_path)
|
| 67 |
+
_MODEL = Llama(
|
| 68 |
+
model_path=model_path,
|
| 69 |
+
chat_handler=handler,
|
| 70 |
+
n_ctx=n_ctx,
|
| 71 |
+
n_threads=os.cpu_count(),
|
| 72 |
+
verbose=False,
|
| 73 |
+
)
|
| 74 |
+
return _MODEL
|
| 75 |
+
except Exception as e: # surface a clear message to the UI
|
| 76 |
+
raise LLMConfigError(
|
| 77 |
+
f"Could not load the local model ({type(e).__name__}: {e}). "
|
| 78 |
+
"Check the CLAIMREADY_GGUF_* settings and that the model can be downloaded."
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
# --- File -> image content parts ----------------------------------------------
|
| 83 |
+
|
| 84 |
+
def _img_to_data_url(img):
|
| 85 |
+
"""Downscale a PIL image to one vision tile and return a JPEG data URL."""
|
| 86 |
+
img = img.convert("RGB")
|
| 87 |
+
w, h = img.size
|
| 88 |
+
scale = min(1.0, _MAX_IMAGE_SIDE / max(w, h))
|
| 89 |
+
if scale < 1.0:
|
| 90 |
+
img = img.resize((max(1, int(w * scale)), max(1, int(h * scale))))
|
| 91 |
+
buf = io.BytesIO()
|
| 92 |
+
img.save(buf, format="JPEG", quality=90)
|
| 93 |
+
b64 = base64.b64encode(buf.getvalue()).decode("ascii")
|
| 94 |
+
return {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}}
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _file_to_parts(path):
|
| 98 |
+
"""Turn an uploaded file into a list of image content parts."""
|
| 99 |
+
ext = os.path.splitext(path)[1].lower()
|
| 100 |
+
if ext == ".pdf":
|
| 101 |
+
parts = []
|
| 102 |
+
with fitz.open(path) as doc:
|
| 103 |
+
for page in doc:
|
| 104 |
+
pix = page.get_pixmap(dpi=_PDF_DPI)
|
| 105 |
+
img = Image.open(io.BytesIO(pix.tobytes("png")))
|
| 106 |
+
parts.append(_img_to_data_url(img))
|
| 107 |
+
return parts
|
| 108 |
+
if ext in _IMAGE_EXTS:
|
| 109 |
+
return [_img_to_data_url(Image.open(path))]
|
| 110 |
+
# Unknown extension: try to open as an image, else skip.
|
| 111 |
+
try:
|
| 112 |
+
return [_img_to_data_url(Image.open(path))]
|
| 113 |
+
except Exception:
|
| 114 |
+
return []
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
# --- Prompt -------------------------------------------------------------------
|
| 118 |
+
|
| 119 |
+
def _checklist_text(docs, rules):
|
| 120 |
+
lines = ["REQUIRED DOCUMENTS:"]
|
| 121 |
+
for d in docs:
|
| 122 |
+
lines.append(f"- id={d['id']} | {d['label']} — verify: {d['verify']}")
|
| 123 |
+
if rules:
|
| 124 |
+
lines.append("")
|
| 125 |
+
lines.append("CONTENT RULES (check against the text/values in the documents):")
|
| 126 |
+
for r in rules:
|
| 127 |
+
lines.append(f"- id={r['id']} | {r['check']}")
|
| 128 |
+
return "\n".join(lines)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def _system_prompt():
|
| 132 |
+
return (
|
| 133 |
+
"You are a PMJAY (Ayushman Bharat) claims processing doctor reviewing a "
|
| 134 |
+
"hospital's pre-authorization / claim document set against the Standard "
|
| 135 |
+
"Treatment Guidelines BEFORE it is submitted to the government portal.\n\n"
|
| 136 |
+
"You will be given: (a) the package + stage, (b) a checklist of required "
|
| 137 |
+
"documents, (c) content rules to confirm, and (d) the uploaded files as "
|
| 138 |
+
"images (PDF pages are also given as images). Read every image carefully "
|
| 139 |
+
"(OCR the text).\n\n"
|
| 140 |
+
"For EACH required document, decide whether it is present among the "
|
| 141 |
+
"uploaded files and whether its content matches what must be verified. "
|
| 142 |
+
"For EACH content rule, decide pass / fail / unclear based on the text or "
|
| 143 |
+
"values you can read. Do not invent evidence — if you cannot read it, say "
|
| 144 |
+
"so and mark it unclear.\n\n"
|
| 145 |
+
"STRICT RULE EVALUATION — apply these without exception:\n"
|
| 146 |
+
"1. NUMERIC THRESHOLDS: When a rule states a numeric limit (e.g. '>= 101°F', "
|
| 147 |
+
"'>= 38.3°C', '< 7 g/dl', '> 55 years', '> 1 cm'), find the actual value in "
|
| 148 |
+
"the documents and compare it EXACTLY. Do NOT round up, approximate, or give "
|
| 149 |
+
"the benefit of the doubt. A reading of 100.8°F does NOT satisfy a '>= 101°F' "
|
| 150 |
+
"rule — that is a 'fail'. A hemoglobin of 7.2 does NOT satisfy '< 7 g/dl'.\n"
|
| 151 |
+
"2. DURATION / FREQUENCY: When a rule includes a time span (e.g. 'for more "
|
| 152 |
+
"than 2 days'), you must find explicit dated evidence covering that span "
|
| 153 |
+
"(e.g. fever documented on 3+ separate dated entries). A single reading never "
|
| 154 |
+
"satisfies a multi-day requirement — mark it 'unclear' if duration is not "
|
| 155 |
+
"documented.\n"
|
| 156 |
+
"3. PASS ONLY ON FULL MATCH: Mark 'pass' only when you can cite a specific, "
|
| 157 |
+
"readable value that meets EVERY part of the criterion (the value AND the "
|
| 158 |
+
"threshold AND any duration). If a value is present but below/outside the "
|
| 159 |
+
"bar, mark 'fail'. If you cannot read or locate the value, mark 'unclear'. "
|
| 160 |
+
"Never 'pass' by assumption.\n"
|
| 161 |
+
"4. UNITS: Convert units when needed to compare (°F<->°C, etc.), but always "
|
| 162 |
+
"state the actual value you read.\n"
|
| 163 |
+
"5. EVIDENCE: In every rule's 'evidence', quote the exact value you read and "
|
| 164 |
+
"state explicitly whether it meets the threshold (e.g. \"Temp 100.8°F read; "
|
| 165 |
+
"below the 101°F threshold -> fail\"). Keep document 'present' judgements "
|
| 166 |
+
"consistent with the evidence you cite for related rules.\n"
|
| 167 |
+
"6. STATUS MUST MATCH EVIDENCE: The 'status' you assign to a rule (and the "
|
| 168 |
+
"'present' boolean for a document) MUST agree with the conclusion written in "
|
| 169 |
+
"its own 'evidence'. If the evidence says the value is below/outside the bar, "
|
| 170 |
+
"or ends in '-> fail', the status is 'fail' — NEVER 'pass'. If the evidence "
|
| 171 |
+
"says the value cannot be read or located, the status is 'unclear'. Before "
|
| 172 |
+
"finalizing, re-read each evidence string and confirm the label agrees with "
|
| 173 |
+
"it. A 'pass' whose evidence concludes 'not met' is a contradiction and is "
|
| 174 |
+
"forbidden.\n\n"
|
| 175 |
+
"Respond with ONLY a JSON object (no markdown fences, no prose) matching:\n"
|
| 176 |
+
"{\n"
|
| 177 |
+
' "documents": [{"id": str, "present": bool, "confidence": "high|medium|low", "evidence": str}],\n'
|
| 178 |
+
' "rules": [{"id": str, "status": "pass|fail|unclear", "evidence": str}],\n'
|
| 179 |
+
' "overall": "ready|not_ready|needs_review",\n'
|
| 180 |
+
' "summary": str\n'
|
| 181 |
+
"}\n"
|
| 182 |
+
"Use the exact id values given. 'evidence' is a short phrase citing which "
|
| 183 |
+
"file and what you saw. Keep 'summary' to 1-3 sentences."
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def _build_user_content(pkg, stage_label, docs, rules, files):
|
| 188 |
+
"""Build the user message: instructions + checklist text, then the images.
|
| 189 |
+
|
| 190 |
+
Gemma has no separate system role, so the system instructions are prepended
|
| 191 |
+
to the leading text block.
|
| 192 |
+
"""
|
| 193 |
+
intro = (
|
| 194 |
+
f"{_system_prompt()}\n\n"
|
| 195 |
+
f"PACKAGE: {pkg['code']} — {pkg['name']} ({pkg.get('procedure', '')})\n"
|
| 196 |
+
f"STAGE: {stage_label}\n\n"
|
| 197 |
+
f"{_checklist_text(docs, rules)}\n\n"
|
| 198 |
+
f"UPLOADED FILES: the documents follow as images."
|
| 199 |
+
)
|
| 200 |
+
content = [{"type": "text", "text": intro}]
|
| 201 |
+
|
| 202 |
+
n_images = 0
|
| 203 |
+
truncated = False
|
| 204 |
+
for path in files:
|
| 205 |
+
if n_images >= _MAX_IMAGES:
|
| 206 |
+
truncated = True
|
| 207 |
+
break
|
| 208 |
+
for part in _file_to_parts(path):
|
| 209 |
+
if n_images >= _MAX_IMAGES:
|
| 210 |
+
truncated = True
|
| 211 |
+
break
|
| 212 |
+
content.append(part)
|
| 213 |
+
n_images += 1
|
| 214 |
+
return content, n_images, truncated
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def _parse_json(text):
|
| 218 |
+
"""Best-effort parse of the model's JSON reply."""
|
| 219 |
+
text = (text or "").strip()
|
| 220 |
+
if text.startswith("```"):
|
| 221 |
+
text = text.strip("`")
|
| 222 |
+
if text.lower().startswith("json"):
|
| 223 |
+
text = text[4:]
|
| 224 |
+
text = text.strip()
|
| 225 |
+
try:
|
| 226 |
+
return json.loads(text)
|
| 227 |
+
except json.JSONDecodeError:
|
| 228 |
+
start, end = text.find("{"), text.rfind("}")
|
| 229 |
+
if start != -1 and end != -1 and end > start:
|
| 230 |
+
try:
|
| 231 |
+
return json.loads(text[start : end + 1])
|
| 232 |
+
except json.JSONDecodeError:
|
| 233 |
+
return None
|
| 234 |
+
return None
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def verify(pkg, stage_label, docs, rules, files):
|
| 238 |
+
"""Run local LLM verification. Returns a markdown report string."""
|
| 239 |
+
model = _get_model()
|
| 240 |
+
content, _n_images, _truncated = _build_user_content(pkg, stage_label, docs, rules, files)
|
| 241 |
+
|
| 242 |
+
resp = model.create_chat_completion(
|
| 243 |
+
messages=[{"role": "user", "content": content}],
|
| 244 |
+
temperature=0,
|
| 245 |
+
max_tokens=1500,
|
| 246 |
+
)
|
| 247 |
+
raw = resp["choices"][0]["message"]["content"]
|
| 248 |
+
parsed = _parse_json(raw)
|
| 249 |
+
return _render(pkg, stage_label, docs, rules, parsed, raw)
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
# --- Rendering -----------------------------------------------------------------
|
| 253 |
+
|
| 254 |
+
_DOC_ICON = {True: "✅", False: "❌"}
|
| 255 |
+
_RULE_ICON = {"pass": "✅", "fail": "❌", "unclear": "⚠️"}
|
| 256 |
+
_OVERALL = {
|
| 257 |
+
"ready": "✅ **Ready to submit** — no blocking issues found.",
|
| 258 |
+
"not_ready": "❌ **Not ready** — fix the issues below before submitting.",
|
| 259 |
+
"needs_review": "⚠️ **Needs manual review** — some items could not be confirmed.",
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def _render(pkg, stage_label, docs, rules, parsed, raw):
|
| 264 |
+
if parsed is None:
|
| 265 |
+
return (
|
| 266 |
+
"## ⚠️ Could not parse the model's response\n\n"
|
| 267 |
+
"The verification model replied, but not in the expected format. "
|
| 268 |
+
"Raw response below:\n\n"
|
| 269 |
+
f"```\n{(raw or '').strip()[:2000]}\n```"
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
rule_by_id = {r["id"]: r for r in rules}
|
| 273 |
+
res_docs = {d.get("id"): d for d in parsed.get("documents", []) if isinstance(d, dict)}
|
| 274 |
+
res_rules = {r.get("id"): r for r in parsed.get("rules", []) if isinstance(r, dict)}
|
| 275 |
+
|
| 276 |
+
lines = [
|
| 277 |
+
"## Verification result",
|
| 278 |
+
f"**{pkg['code']} — {pkg['name']}** · {stage_label}",
|
| 279 |
+
"",
|
| 280 |
+
_OVERALL.get(parsed.get("overall"), "⚠️ **Review the findings below.**"),
|
| 281 |
+
]
|
| 282 |
+
if parsed.get("summary"):
|
| 283 |
+
lines += ["", f"> {parsed['summary']}"]
|
| 284 |
+
|
| 285 |
+
lines += ["", "### Documents"]
|
| 286 |
+
for d in docs:
|
| 287 |
+
r = res_docs.get(d["id"], {})
|
| 288 |
+
present = bool(r.get("present"))
|
| 289 |
+
icon = _DOC_ICON[present]
|
| 290 |
+
conf = r.get("confidence")
|
| 291 |
+
conf_txt = f" _(confidence: {conf})_" if conf else ""
|
| 292 |
+
lines.append(f"- {icon} **{d['label']}**{conf_txt}")
|
| 293 |
+
if r.get("evidence"):
|
| 294 |
+
lines.append(f" - {r['evidence']}")
|
| 295 |
+
elif not r:
|
| 296 |
+
lines.append(" - _Not assessed by the model._")
|
| 297 |
+
|
| 298 |
+
if rules:
|
| 299 |
+
lines += ["", "### Content checks"]
|
| 300 |
+
for rule in rules:
|
| 301 |
+
r = res_rules.get(rule["id"], {})
|
| 302 |
+
status = r.get("status", "unclear")
|
| 303 |
+
icon = _RULE_ICON.get(status, "⚠️")
|
| 304 |
+
lines.append(f"- {icon} {rule['check']} — _{status}_")
|
| 305 |
+
if r.get("evidence"):
|
| 306 |
+
lines.append(f" - {r['evidence']}")
|
| 307 |
+
|
| 308 |
+
missing = [d["label"] for d in docs if not res_docs.get(d["id"], {}).get("present")]
|
| 309 |
+
failed = [
|
| 310 |
+
rule_by_id[rid]["check"]
|
| 311 |
+
for rid, r in res_rules.items()
|
| 312 |
+
if r.get("status") == "fail" and rid in rule_by_id
|
| 313 |
+
]
|
| 314 |
+
if missing or failed:
|
| 315 |
+
lines += ["", "### ⚠️ Action needed before submission"]
|
| 316 |
+
for m in missing:
|
| 317 |
+
lines.append(f"- Missing / unreadable document: **{m}**")
|
| 318 |
+
for f in failed:
|
| 319 |
+
lines.append(f"- Failed content check: {f}")
|
| 320 |
+
|
| 321 |
+
return "\n".join(lines)
|
packages.json
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"stages": {
|
| 3 |
+
"preauth": "Pre-Authorization (before treatment)",
|
| 4 |
+
"claim": "Claim Submission (after treatment)"
|
| 5 |
+
},
|
| 6 |
+
"packages": [
|
| 7 |
+
{
|
| 8 |
+
"code": "SG039A",
|
| 9 |
+
"name": "Cholecystectomy",
|
| 10 |
+
"procedure": "Without Exploration of CBD – Open",
|
| 11 |
+
"specialty": "General Surgery / Pediatric Surgery",
|
| 12 |
+
"price": "₹22,800",
|
| 13 |
+
"documents": {
|
| 14 |
+
"preauth": [
|
| 15 |
+
{
|
| 16 |
+
"id": "clinical_notes",
|
| 17 |
+
"label": "Clinical notes",
|
| 18 |
+
"verify": "Detailed history, signs & symptoms, and indication for the procedure"
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"id": "usg_abdomen",
|
| 22 |
+
"label": "USG Upper Abdomen",
|
| 23 |
+
"verify": "Confirms presence of gall stone. If CBD exploration is planned, MRCP/USG should show large (>1cm) or multiple CBD stones"
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"id": "lft",
|
| 27 |
+
"label": "LFT (Liver Function Test)",
|
| 28 |
+
"verify": "Report showing serum bilirubin, transaminase and alkaline phosphatase levels"
|
| 29 |
+
}
|
| 30 |
+
],
|
| 31 |
+
"claim": [
|
| 32 |
+
{
|
| 33 |
+
"id": "operative_notes",
|
| 34 |
+
"label": "Operative notes",
|
| 35 |
+
"verify": "Detailed operative notes with indications and outcomes of the procedure"
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"id": "pre_anesthesia",
|
| 39 |
+
"label": "Pre-anesthesia check-up report",
|
| 40 |
+
"verify": "Pre-anesthesia evaluation / fitness report"
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"id": "discharge_summary",
|
| 44 |
+
"label": "Detailed Discharge Summary",
|
| 45 |
+
"verify": "Discharge summary including follow-up advice at the time of discharge"
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"id": "intraop_photo",
|
| 49 |
+
"label": "Intra-operative photograph & gross specimen pictures",
|
| 50 |
+
"verify": "Intra-operative image and picture(s) of gross specimen removed. If CBD stones detected/suspected, photo/video of stones removed from CBD"
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"id": "histopathology",
|
| 54 |
+
"label": "Histopathology report",
|
| 55 |
+
"verify": "Histopathology report of the specimen removed",
|
| 56 |
+
"note": "Can be submitted within 7 days of discharge"
|
| 57 |
+
}
|
| 58 |
+
]
|
| 59 |
+
},
|
| 60 |
+
"rules": [
|
| 61 |
+
{ "id": "usg_calculi", "check": "USG shows presence of calculi in the gall bladder" },
|
| 62 |
+
{ "id": "pain_site", "check": "Patient has complaint of right hypochondrium or epigastric pain" },
|
| 63 |
+
{ "id": "no_prior_chole", "check": "No prior cholecystectomy done in the past" }
|
| 64 |
+
]
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"code": "SG039B",
|
| 68 |
+
"name": "Cholecystectomy",
|
| 69 |
+
"procedure": "With Exploration of CBD – Open",
|
| 70 |
+
"specialty": "General Surgery / Pediatric Surgery",
|
| 71 |
+
"price": "₹22,800",
|
| 72 |
+
"documents": {
|
| 73 |
+
"preauth": [
|
| 74 |
+
{
|
| 75 |
+
"id": "clinical_notes",
|
| 76 |
+
"label": "Clinical notes",
|
| 77 |
+
"verify": "Detailed history, signs & symptoms, and indication for the procedure"
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"id": "usg_abdomen",
|
| 81 |
+
"label": "USG Upper Abdomen",
|
| 82 |
+
"verify": "Confirms presence of gall stone. If CBD exploration is planned, MRCP/USG should show large (>1cm) or multiple CBD stones"
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"id": "lft",
|
| 86 |
+
"label": "LFT (Liver Function Test)",
|
| 87 |
+
"verify": "Report showing serum bilirubin, transaminase and alkaline phosphatase levels"
|
| 88 |
+
}
|
| 89 |
+
],
|
| 90 |
+
"claim": [
|
| 91 |
+
{
|
| 92 |
+
"id": "operative_notes",
|
| 93 |
+
"label": "Operative notes",
|
| 94 |
+
"verify": "Detailed operative notes with indications and outcomes of the procedure"
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
"id": "pre_anesthesia",
|
| 98 |
+
"label": "Pre-anesthesia check-up report",
|
| 99 |
+
"verify": "Pre-anesthesia evaluation / fitness report"
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
"id": "discharge_summary",
|
| 103 |
+
"label": "Detailed Discharge Summary",
|
| 104 |
+
"verify": "Discharge summary including follow-up advice at the time of discharge"
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"id": "intraop_photo",
|
| 108 |
+
"label": "Intra-operative photograph & gross specimen pictures",
|
| 109 |
+
"verify": "Intra-operative image and picture(s) of gross specimen removed. If CBD stones detected/suspected, photo/video of stones removed from CBD"
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
"id": "histopathology",
|
| 113 |
+
"label": "Histopathology report",
|
| 114 |
+
"verify": "Histopathology report of the specimen removed",
|
| 115 |
+
"note": "Can be submitted within 7 days of discharge"
|
| 116 |
+
}
|
| 117 |
+
]
|
| 118 |
+
},
|
| 119 |
+
"rules": [
|
| 120 |
+
{ "id": "usg_calculi", "check": "USG shows presence of calculi in the gall bladder" },
|
| 121 |
+
{ "id": "pain_site", "check": "Patient has complaint of right hypochondrium or epigastric pain" },
|
| 122 |
+
{ "id": "no_prior_chole", "check": "No prior cholecystectomy done in the past" }
|
| 123 |
+
]
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"code": "SG039C",
|
| 127 |
+
"name": "Cholecystectomy",
|
| 128 |
+
"procedure": "Without Exploration of CBD – Lap.",
|
| 129 |
+
"specialty": "General Surgery / Pediatric Surgery",
|
| 130 |
+
"price": "₹22,800",
|
| 131 |
+
"documents": {
|
| 132 |
+
"preauth": [
|
| 133 |
+
{
|
| 134 |
+
"id": "clinical_notes",
|
| 135 |
+
"label": "Clinical notes",
|
| 136 |
+
"verify": "Detailed history, signs & symptoms, and indication for the procedure"
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"id": "usg_abdomen",
|
| 140 |
+
"label": "USG Upper Abdomen",
|
| 141 |
+
"verify": "Confirms presence of gall stone. If CBD exploration is planned, MRCP/USG should show large (>1cm) or multiple CBD stones"
|
| 142 |
+
},
|
| 143 |
+
{
|
| 144 |
+
"id": "lft",
|
| 145 |
+
"label": "LFT (Liver Function Test)",
|
| 146 |
+
"verify": "Report showing serum bilirubin, transaminase and alkaline phosphatase levels"
|
| 147 |
+
}
|
| 148 |
+
],
|
| 149 |
+
"claim": [
|
| 150 |
+
{
|
| 151 |
+
"id": "operative_notes",
|
| 152 |
+
"label": "Operative notes",
|
| 153 |
+
"verify": "Detailed operative notes with indications and outcomes of the procedure"
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
"id": "pre_anesthesia",
|
| 157 |
+
"label": "Pre-anesthesia check-up report",
|
| 158 |
+
"verify": "Pre-anesthesia evaluation / fitness report"
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"id": "discharge_summary",
|
| 162 |
+
"label": "Detailed Discharge Summary",
|
| 163 |
+
"verify": "Discharge summary including follow-up advice at the time of discharge"
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
"id": "intraop_photo",
|
| 167 |
+
"label": "Intra-operative photograph & gross specimen pictures",
|
| 168 |
+
"verify": "Intra-operative image and picture(s) of gross specimen removed. If CBD stones detected/suspected, photo/video of stones removed from CBD"
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
"id": "histopathology",
|
| 172 |
+
"label": "Histopathology report",
|
| 173 |
+
"verify": "Histopathology report of the specimen removed",
|
| 174 |
+
"note": "Can be submitted within 7 days of discharge"
|
| 175 |
+
}
|
| 176 |
+
]
|
| 177 |
+
},
|
| 178 |
+
"rules": [
|
| 179 |
+
{ "id": "usg_calculi", "check": "USG shows presence of calculi in the gall bladder" },
|
| 180 |
+
{ "id": "pain_site", "check": "Patient has complaint of right hypochondrium or epigastric pain" },
|
| 181 |
+
{ "id": "no_prior_chole", "check": "No prior cholecystectomy done in the past" }
|
| 182 |
+
]
|
| 183 |
+
},
|
| 184 |
+
{
|
| 185 |
+
"code": "SG039D",
|
| 186 |
+
"name": "Cholecystectomy",
|
| 187 |
+
"procedure": "With Exploration of CBD – Lap.",
|
| 188 |
+
"specialty": "General Surgery / Pediatric Surgery",
|
| 189 |
+
"price": "₹22,800",
|
| 190 |
+
"documents": {
|
| 191 |
+
"preauth": [
|
| 192 |
+
{
|
| 193 |
+
"id": "clinical_notes",
|
| 194 |
+
"label": "Clinical notes",
|
| 195 |
+
"verify": "Detailed history, signs & symptoms, and indication for the procedure"
|
| 196 |
+
},
|
| 197 |
+
{
|
| 198 |
+
"id": "usg_abdomen",
|
| 199 |
+
"label": "USG Upper Abdomen",
|
| 200 |
+
"verify": "Confirms presence of gall stone. If CBD exploration is planned, MRCP/USG should show large (>1cm) or multiple CBD stones"
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"id": "lft",
|
| 204 |
+
"label": "LFT (Liver Function Test)",
|
| 205 |
+
"verify": "Report showing serum bilirubin, transaminase and alkaline phosphatase levels"
|
| 206 |
+
}
|
| 207 |
+
],
|
| 208 |
+
"claim": [
|
| 209 |
+
{
|
| 210 |
+
"id": "operative_notes",
|
| 211 |
+
"label": "Operative notes",
|
| 212 |
+
"verify": "Detailed operative notes with indications and outcomes of the procedure"
|
| 213 |
+
},
|
| 214 |
+
{
|
| 215 |
+
"id": "pre_anesthesia",
|
| 216 |
+
"label": "Pre-anesthesia check-up report",
|
| 217 |
+
"verify": "Pre-anesthesia evaluation / fitness report"
|
| 218 |
+
},
|
| 219 |
+
{
|
| 220 |
+
"id": "discharge_summary",
|
| 221 |
+
"label": "Detailed Discharge Summary",
|
| 222 |
+
"verify": "Discharge summary including follow-up advice at the time of discharge"
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
"id": "intraop_photo",
|
| 226 |
+
"label": "Intra-operative photograph & gross specimen pictures",
|
| 227 |
+
"verify": "Intra-operative image and picture(s) of gross specimen removed. If CBD stones detected/suspected, photo/video of stones removed from CBD"
|
| 228 |
+
},
|
| 229 |
+
{
|
| 230 |
+
"id": "histopathology",
|
| 231 |
+
"label": "Histopathology report",
|
| 232 |
+
"verify": "Histopathology report of the specimen removed",
|
| 233 |
+
"note": "Can be submitted within 7 days of discharge"
|
| 234 |
+
}
|
| 235 |
+
]
|
| 236 |
+
},
|
| 237 |
+
"rules": [
|
| 238 |
+
{ "id": "usg_calculi", "check": "USG shows presence of calculi in the gall bladder" },
|
| 239 |
+
{ "id": "pain_site", "check": "Patient has complaint of right hypochondrium or epigastric pain" },
|
| 240 |
+
{ "id": "no_prior_chole", "check": "No prior cholecystectomy done in the past" }
|
| 241 |
+
]
|
| 242 |
+
},
|
| 243 |
+
{
|
| 244 |
+
"code": "MG006A",
|
| 245 |
+
"name": "Enteric fever",
|
| 246 |
+
"procedure": "Enteric fever (medical management)",
|
| 247 |
+
"specialty": "General Medicine / Pediatric Medical Management",
|
| 248 |
+
"price": "₹1,800 (Gen) – ₹4,500 (ICU+vent)",
|
| 249 |
+
"documents": {
|
| 250 |
+
"preauth": [
|
| 251 |
+
{
|
| 252 |
+
"id": "clinical_notes",
|
| 253 |
+
"label": "Clinical notes with detailed history",
|
| 254 |
+
"verify": "Detailed clinical notes documenting history of fever and symptoms"
|
| 255 |
+
},
|
| 256 |
+
{
|
| 257 |
+
"id": "cbc_panel",
|
| 258 |
+
"label": "CBC, ESR, Peripheral smear, LFT",
|
| 259 |
+
"verify": "Baseline CBC, ESR, peripheral smear and LFT reports"
|
| 260 |
+
}
|
| 261 |
+
],
|
| 262 |
+
"claim": [
|
| 263 |
+
{
|
| 264 |
+
"id": "indoor_case_papers",
|
| 265 |
+
"label": "Detailed Indoor case papers & treatment details",
|
| 266 |
+
"verify": "Indoor case papers with vitals and full treatment details"
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
"id": "post_cbc_panel",
|
| 270 |
+
"label": "Post-treatment CBC, ESR, Peripheral smear, LFT",
|
| 271 |
+
"verify": "Repeat CBC, ESR, peripheral smear and LFT after treatment"
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"id": "discharge_summary",
|
| 275 |
+
"label": "Detailed Discharge Summary",
|
| 276 |
+
"verify": "Discharge summary including date of follow-up"
|
| 277 |
+
}
|
| 278 |
+
]
|
| 279 |
+
},
|
| 280 |
+
"rules": [
|
| 281 |
+
{ "id": "fever_threshold", "check": "Patient had fever >= 38.3°C / 101°F for more than 2 days (documented)" }
|
| 282 |
+
]
|
| 283 |
+
},
|
| 284 |
+
{
|
| 285 |
+
"code": "MG064A",
|
| 286 |
+
"name": "Severe Anemia",
|
| 287 |
+
"procedure": "Severe anemia (medical management)",
|
| 288 |
+
"specialty": "General Medicine / Pediatric Medical Management",
|
| 289 |
+
"price": "₹1,800 (Gen) – ₹4,500 (ICU+vent)",
|
| 290 |
+
"documents": {
|
| 291 |
+
"preauth": [
|
| 292 |
+
{
|
| 293 |
+
"id": "clinical_notes",
|
| 294 |
+
"label": "Clinical notes",
|
| 295 |
+
"verify": "Evaluation findings, indications for the procedure, and planned line of treatment"
|
| 296 |
+
},
|
| 297 |
+
{
|
| 298 |
+
"id": "cbc_hb",
|
| 299 |
+
"label": "CBC, Hb report",
|
| 300 |
+
"verify": "Complete blood count and hemoglobin level report"
|
| 301 |
+
}
|
| 302 |
+
],
|
| 303 |
+
"claim": [
|
| 304 |
+
{
|
| 305 |
+
"id": "indoor_case_papers",
|
| 306 |
+
"label": "Detailed Indoor case papers & treatment details",
|
| 307 |
+
"verify": "Indoor case papers with indications and treatment details. Should mention blood transfusion and ferrous sulphate injection"
|
| 308 |
+
},
|
| 309 |
+
{
|
| 310 |
+
"id": "post_cbc_hb",
|
| 311 |
+
"label": "Post-treatment CBC, Hb report",
|
| 312 |
+
"verify": "Repeat CBC / hemoglobin report after treatment"
|
| 313 |
+
},
|
| 314 |
+
{
|
| 315 |
+
"id": "discharge_summary",
|
| 316 |
+
"label": "Detailed Discharge Summary",
|
| 317 |
+
"verify": "Detailed discharge summary"
|
| 318 |
+
}
|
| 319 |
+
]
|
| 320 |
+
},
|
| 321 |
+
"rules": [
|
| 322 |
+
{ "id": "hb_below_7", "check": "Patient hemoglobin level was less than 7 g/dl" },
|
| 323 |
+
{ "id": "transfusion", "check": "Patient underwent blood transfusion treatment" }
|
| 324 |
+
]
|
| 325 |
+
},
|
| 326 |
+
{
|
| 327 |
+
"code": "SB039A",
|
| 328 |
+
"name": "Total Knee Replacement (Primary)",
|
| 329 |
+
"procedure": "Primary - Total Knee Replacement",
|
| 330 |
+
"specialty": "Orthopedics",
|
| 331 |
+
"price": "₹80,000",
|
| 332 |
+
"documents": {
|
| 333 |
+
"preauth": [
|
| 334 |
+
{
|
| 335 |
+
"id": "clinical_notes",
|
| 336 |
+
"label": "Clinical notes with indication for surgery",
|
| 337 |
+
"verify": "Clinical notes documenting the indication for knee replacement surgery"
|
| 338 |
+
},
|
| 339 |
+
{
|
| 340 |
+
"id": "knee_xray",
|
| 341 |
+
"label": "X-ray / CT of Knee (labelled)",
|
| 342 |
+
"verify": "X-ray or CT of the knee, labelled with patient ID, date and side (Left/Right)"
|
| 343 |
+
}
|
| 344 |
+
],
|
| 345 |
+
"claim": [
|
| 346 |
+
{
|
| 347 |
+
"id": "indoor_case_papers",
|
| 348 |
+
"label": "Indoor case papers",
|
| 349 |
+
"verify": "Indoor case papers for the admission"
|
| 350 |
+
},
|
| 351 |
+
{
|
| 352 |
+
"id": "postop_photo",
|
| 353 |
+
"label": "Post-op clinical photograph",
|
| 354 |
+
"verify": "Post-operative clinical photograph"
|
| 355 |
+
},
|
| 356 |
+
{
|
| 357 |
+
"id": "postop_xray",
|
| 358 |
+
"label": "Post-op X-ray showing implant",
|
| 359 |
+
"verify": "Post-op X-ray of the operated knee showing the implant, labelled with patient ID, date and side (Left/Right)"
|
| 360 |
+
},
|
| 361 |
+
{
|
| 362 |
+
"id": "implant_invoice",
|
| 363 |
+
"label": "Implant invoice / barcode",
|
| 364 |
+
"verify": "Invoice or bar code of the implant used"
|
| 365 |
+
},
|
| 366 |
+
{
|
| 367 |
+
"id": "operative_note",
|
| 368 |
+
"label": "Detailed operative / procedure note",
|
| 369 |
+
"verify": "Detailed operative note"
|
| 370 |
+
},
|
| 371 |
+
{
|
| 372 |
+
"id": "discharge_summary",
|
| 373 |
+
"label": "Discharge Summary",
|
| 374 |
+
"verify": "Discharge summary"
|
| 375 |
+
}
|
| 376 |
+
]
|
| 377 |
+
},
|
| 378 |
+
"rules": [
|
| 379 |
+
{ "id": "postop_implant", "check": "Post-op X-ray of the knee shows presence of the implant" },
|
| 380 |
+
{ "id": "primary_age", "check": "If indication is primary osteoarthritis (no trauma / systemic joint disease), patient age is > 55 years" }
|
| 381 |
+
]
|
| 382 |
+
},
|
| 383 |
+
{
|
| 384 |
+
"code": "SB039B",
|
| 385 |
+
"name": "Total Knee Replacement (Revision)",
|
| 386 |
+
"procedure": "Revision - Total Knee Replacement",
|
| 387 |
+
"specialty": "Orthopedics",
|
| 388 |
+
"price": "₹1,30,000",
|
| 389 |
+
"documents": {
|
| 390 |
+
"preauth": [
|
| 391 |
+
{
|
| 392 |
+
"id": "clinical_notes",
|
| 393 |
+
"label": "Clinical notes with indication for surgery",
|
| 394 |
+
"verify": "Clinical notes documenting the indication for revision knee replacement"
|
| 395 |
+
},
|
| 396 |
+
{
|
| 397 |
+
"id": "knee_xray",
|
| 398 |
+
"label": "X-ray / CT of Knee (labelled)",
|
| 399 |
+
"verify": "X-ray or CT of the knee, labelled with patient ID, date and side (Left/Right)"
|
| 400 |
+
},
|
| 401 |
+
{
|
| 402 |
+
"id": "preop_xray_implant",
|
| 403 |
+
"label": "Pre-op X-ray showing existing implant",
|
| 404 |
+
"verify": "Pre-op X-ray of the affected knee showing the existing implant (required for revision cases)"
|
| 405 |
+
}
|
| 406 |
+
],
|
| 407 |
+
"claim": [
|
| 408 |
+
{
|
| 409 |
+
"id": "indoor_case_papers",
|
| 410 |
+
"label": "Indoor case papers",
|
| 411 |
+
"verify": "Indoor case papers for the admission"
|
| 412 |
+
},
|
| 413 |
+
{
|
| 414 |
+
"id": "postop_photo",
|
| 415 |
+
"label": "Post-op clinical photograph",
|
| 416 |
+
"verify": "Post-operative clinical photograph"
|
| 417 |
+
},
|
| 418 |
+
{
|
| 419 |
+
"id": "postop_xray",
|
| 420 |
+
"label": "Post-op X-ray showing implant",
|
| 421 |
+
"verify": "Post-op X-ray of the operated knee showing the implant, labelled with patient ID, date and side (Left/Right)"
|
| 422 |
+
},
|
| 423 |
+
{
|
| 424 |
+
"id": "implant_invoice",
|
| 425 |
+
"label": "Implant invoice / barcode",
|
| 426 |
+
"verify": "Invoice or bar code of the implant used"
|
| 427 |
+
},
|
| 428 |
+
{
|
| 429 |
+
"id": "operative_note",
|
| 430 |
+
"label": "Detailed operative / procedure note",
|
| 431 |
+
"verify": "Detailed operative note"
|
| 432 |
+
},
|
| 433 |
+
{
|
| 434 |
+
"id": "discharge_summary",
|
| 435 |
+
"label": "Discharge Summary",
|
| 436 |
+
"verify": "Discharge summary"
|
| 437 |
+
}
|
| 438 |
+
]
|
| 439 |
+
},
|
| 440 |
+
"rules": [
|
| 441 |
+
{ "id": "postop_implant", "check": "Post-op X-ray of the knee shows presence of the implant" },
|
| 442 |
+
{ "id": "preop_implant", "check": "Pre-op X-ray shows presence of the existing implant" }
|
| 443 |
+
]
|
| 444 |
+
}
|
| 445 |
+
]
|
| 446 |
+
}
|
packages.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PMJAY package configuration — loaded from packages.json.
|
| 3 |
+
|
| 4 |
+
packages.json is the single source of truth: each package carries the mandatory
|
| 5 |
+
document checklist for both stages (pre-auth / claim) plus the "TMS rule" content
|
| 6 |
+
checks the STG asks processing doctors to verify. Edit the JSON to add packages,
|
| 7 |
+
documents, or rules — no code change required.
|
| 8 |
+
|
| 9 |
+
The verdict engine treats `documents` as a deterministic presence checklist, and
|
| 10 |
+
`rules` as content checks the LLM evaluates against OCR'd text.
|
| 11 |
+
|
| 12 |
+
Source PDFs: stg_guidelines/STGs_ps1/
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import json
|
| 16 |
+
import os
|
| 17 |
+
|
| 18 |
+
_DATA_PATH = os.path.join(os.path.dirname(__file__), "packages.json")
|
| 19 |
+
|
| 20 |
+
# Stage identifiers used across the app.
|
| 21 |
+
STAGE_PREAUTH = "preauth"
|
| 22 |
+
STAGE_CLAIM = "claim"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _load():
|
| 26 |
+
with open(_DATA_PATH, encoding="utf-8") as fh:
|
| 27 |
+
raw = json.load(fh)
|
| 28 |
+
|
| 29 |
+
stage_labels = raw.get("stages", {})
|
| 30 |
+
|
| 31 |
+
packages = {}
|
| 32 |
+
for pkg in raw.get("packages", []):
|
| 33 |
+
code = pkg["code"]
|
| 34 |
+
documents = {}
|
| 35 |
+
for stage, items in pkg.get("documents", {}).items():
|
| 36 |
+
documents[stage] = [
|
| 37 |
+
{
|
| 38 |
+
"id": d["id"],
|
| 39 |
+
"label": d["label"],
|
| 40 |
+
"verify": d["verify"],
|
| 41 |
+
"note": d.get("note", ""),
|
| 42 |
+
}
|
| 43 |
+
for d in items
|
| 44 |
+
]
|
| 45 |
+
rules = [
|
| 46 |
+
{
|
| 47 |
+
"id": r["id"],
|
| 48 |
+
"check": r["check"],
|
| 49 |
+
"expected": r.get("expected", "Yes"),
|
| 50 |
+
}
|
| 51 |
+
for r in pkg.get("rules", [])
|
| 52 |
+
]
|
| 53 |
+
packages[code] = {
|
| 54 |
+
"code": code,
|
| 55 |
+
"name": pkg["name"],
|
| 56 |
+
"procedure": pkg.get("procedure", ""),
|
| 57 |
+
"specialty": pkg.get("specialty", ""),
|
| 58 |
+
"price": pkg.get("price", ""),
|
| 59 |
+
"documents": documents,
|
| 60 |
+
"rules": rules,
|
| 61 |
+
}
|
| 62 |
+
return stage_labels, packages
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
STAGE_LABELS, PACKAGES = _load()
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def dropdown_choices():
|
| 69 |
+
"""Return [(label, code), ...] for the Gradio dropdown, grouped sensibly."""
|
| 70 |
+
# Count how many codes share each display name; only disambiguate with the
|
| 71 |
+
# procedure when a name is used by more than one package (e.g. the 4
|
| 72 |
+
# cholecystectomy variants).
|
| 73 |
+
name_counts = {}
|
| 74 |
+
for pkg in PACKAGES.values():
|
| 75 |
+
name_counts[pkg["name"]] = name_counts.get(pkg["name"], 0) + 1
|
| 76 |
+
|
| 77 |
+
choices = []
|
| 78 |
+
for code, pkg in PACKAGES.items():
|
| 79 |
+
label = f"{code} — {pkg['name']}"
|
| 80 |
+
if name_counts[pkg["name"]] > 1 and pkg.get("procedure"):
|
| 81 |
+
label += f" — {pkg['procedure']}"
|
| 82 |
+
choices.append((label, code))
|
| 83 |
+
return choices
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def get_package(code):
|
| 87 |
+
return PACKAGES.get(code)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def required_documents(code, stage):
|
| 91 |
+
pkg = PACKAGES.get(code)
|
| 92 |
+
if not pkg:
|
| 93 |
+
return []
|
| 94 |
+
return pkg["documents"].get(stage, [])
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
--extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu
|
| 2 |
+
gradio>=4.44.0
|
| 3 |
+
llama-cpp-python>=0.3.16
|
| 4 |
+
pymupdf>=1.24.0
|
| 5 |
+
pillow>=10.0.0
|
| 6 |
+
huggingface_hub>=0.25.0
|