vinaybabu commited on
Commit
bf76d9c
·
verified ·
1 Parent(s): d89aa52

Deploy ClaimReady: on-device Gemma 3 via llama.cpp

Browse files
Files changed (7) hide show
  1. .gitignore +4 -0
  2. README.md +18 -8
  3. app.py +164 -0
  4. llm.py +321 -0
  5. packages.json +446 -0
  6. packages.py +94 -0
  7. 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 Ready
3
- emoji: 🌍
4
- colorFrom: yellow
5
- colorTo: pink
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
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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