Spaces:
Sleeping
Sleeping
Commit ·
05cb41b
0
Parent(s):
Initial clean deployment commit
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +4 -0
- Dockerfile +7 -0
- README.md +87 -0
- __pycache__/main.cpython-313.pyc +0 -0
- app/__init__.py +0 -0
- app/__pycache__/__init__.cpython-313.pyc +0 -0
- app/api/__init__.py +0 -0
- app/api/__pycache__/__init__.cpython-313.pyc +0 -0
- app/api/__pycache__/auth_routes.cpython-313.pyc +0 -0
- app/api/__pycache__/claims.cpython-313.pyc +0 -0
- app/api/auth_routes.py +41 -0
- app/api/claims.py +214 -0
- app/core/__init__.py +0 -0
- app/core/__pycache__/__init__.cpython-313.pyc +0 -0
- app/core/__pycache__/config.cpython-313.pyc +0 -0
- app/core/__pycache__/database.cpython-313.pyc +0 -0
- app/core/__pycache__/llm.cpython-313.pyc +0 -0
- app/core/__pycache__/rag.cpython-313.pyc +0 -0
- app/core/config.py +16 -0
- app/core/database.py +34 -0
- app/core/llm.py +127 -0
- app/core/rag.py +76 -0
- app/data/adjudication_rules.md +138 -0
- app/data/policy_terms.json +118 -0
- app/data/test_cases.json +309 -0
- app/models/__init__.py +0 -0
- app/models/__pycache__/__init__.cpython-313.pyc +0 -0
- app/models/__pycache__/schemas.cpython-313.pyc +0 -0
- app/models/schemas.py +62 -0
- deepseek.py +16 -0
- download.png +0 -0
- gemini_vision.py +31 -0
- google_cloud_vision.py +32 -0
- main.py +51 -0
- plum-opd-frontend/.gitignore +41 -0
- plum-opd-frontend/AGENTS.md +5 -0
- plum-opd-frontend/CLAUDE.md +1 -0
- plum-opd-frontend/README.md +36 -0
- plum-opd-frontend/app/admin/page.tsx +134 -0
- plum-opd-frontend/app/favicon.ico +0 -0
- plum-opd-frontend/app/globals.css +26 -0
- plum-opd-frontend/app/layout.tsx +33 -0
- plum-opd-frontend/app/page.tsx +72 -0
- plum-opd-frontend/app/user/page.tsx +244 -0
- plum-opd-frontend/eslint.config.mjs +18 -0
- plum-opd-frontend/next.config.ts +7 -0
- plum-opd-frontend/package-lock.json +0 -0
- plum-opd-frontend/package.json +27 -0
- plum-opd-frontend/postcss.config.mjs +7 -0
- plum-opd-frontend/public/file.svg +1 -0
.gitignore
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
105.jpg
|
| 2 |
+
handwriting.png
|
| 3 |
+
105.jpg
|
| 4 |
+
handwriting.png
|
Dockerfile
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
COPY requirements.txt .
|
| 4 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 5 |
+
COPY . .
|
| 6 |
+
# Run Uvicorn on the port HF expects (7860)
|
| 7 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Plum AI Automation Engineer \- Intern Assignment Package
|
| 2 |
+
|
| 3 |
+
## 📋 Overview
|
| 4 |
+
|
| 5 |
+
Welcome\! This package contains everything you need to complete the OPD Claim Adjudication Tool assignment for the AI Automation Engineer intern position at Plum.
|
| 6 |
+
|
| 7 |
+
## 📁 Package Contents
|
| 8 |
+
|
| 9 |
+
assignment\_package/
|
| 10 |
+
|
| 11 |
+
│
|
| 12 |
+
|
| 13 |
+
├── README.md \# This file
|
| 14 |
+
|
| 15 |
+
├── plum\_intern\_assignment.md \# Main assignment document with requirements
|
| 16 |
+
|
| 17 |
+
├── policy\_terms.json \# Insurance policy configuration
|
| 18 |
+
|
| 19 |
+
├── adjudication\_rules.md \# Business logic for claim decisions
|
| 20 |
+
|
| 21 |
+
├── test\_cases.json \# Test scenarios with expected outputs
|
| 22 |
+
|
| 23 |
+
└── sample\_documents\_guide.md \# Guide for creating test documents
|
| 24 |
+
|
| 25 |
+
## 🎯 Your Mission
|
| 26 |
+
|
| 27 |
+
Build an AI-powered web application that automates the adjudication (approval/rejection) of OPD insurance claims by:
|
| 28 |
+
|
| 29 |
+
1. Processing medical documents (bills, prescriptions)
|
| 30 |
+
2. Extracting relevant information using AI/LLMs
|
| 31 |
+
3. Validating against policy terms
|
| 32 |
+
4. Making intelligent approval/rejection decisions
|
| 33 |
+
|
| 34 |
+
## 🚀 Getting Started
|
| 35 |
+
|
| 36 |
+
### Step 1: Read the Assignment
|
| 37 |
+
|
| 38 |
+
Start with `plum_intern_assignment.md` to understand the full requirements and evaluation criteria.
|
| 39 |
+
|
| 40 |
+
### Step 2: Understand the Business Logic
|
| 41 |
+
|
| 42 |
+
- Review `policy_terms.json` to understand coverage limits and exclusions
|
| 43 |
+
- Study `adjudication_rules.md` to learn the decision-making process
|
| 44 |
+
- Examine `test_cases.json` to see expected behavior
|
| 45 |
+
|
| 46 |
+
### Step 3: Set Up Your Development Environment
|
| 47 |
+
|
| 48 |
+
\# Clone this assignment package
|
| 49 |
+
|
| 50 |
+
\# Set up your preferred tech stack (React/Next.js \+ Node/Python)
|
| 51 |
+
|
| 52 |
+
\# Get API keys for LLM services (OpenAI, Claude, or open-source)
|
| 53 |
+
|
| 54 |
+
### Step 4: Create Test Documents
|
| 55 |
+
|
| 56 |
+
Use `sample_documents_guide.md` to understand medical document formats and create mock documents for testing.
|
| 57 |
+
|
| 58 |
+
### Step 5: Build Your Solution
|
| 59 |
+
|
| 60 |
+
Focus on:
|
| 61 |
+
|
| 62 |
+
- Document upload and processing
|
| 63 |
+
- AI-powered data extraction
|
| 64 |
+
- Rule engine implementation
|
| 65 |
+
- Clean, intuitive UI
|
| 66 |
+
- Comprehensive testing
|
| 67 |
+
|
| 68 |
+
## 💡 Pro Tips
|
| 69 |
+
|
| 70 |
+
1. **Start Simple**: Build a basic working version first, then add advanced features
|
| 71 |
+
2. **Use AI Tools**: We encourage using Cursor, Copilot, or other AI coding assistants
|
| 72 |
+
3. **Document Everything**: Clear documentation shows your thinking process
|
| 73 |
+
4. **Test Thoroughly**: Use all provided test cases and create additional ones
|
| 74 |
+
5. **Ask Early**: If something is unclear, ask within the first 24 hours
|
| 75 |
+
|
| 76 |
+
## 📊 Evaluation Focus Areas
|
| 77 |
+
|
| 78 |
+
- **Core Functionality** (40%): Does it work correctly?
|
| 79 |
+
- **AI Integration** (25%): How effectively do you use LLMs?
|
| 80 |
+
- **Code Quality** (20%): Is the code clean and maintainable?
|
| 81 |
+
- **User Experience** (15%): Is it easy to use?
|
| 82 |
+
|
| 83 |
+
## ⏰ Timeline
|
| 84 |
+
|
| 85 |
+
- **Total Duration**: 2-3 days from receipt
|
| 86 |
+
|
| 87 |
+
# intern-assignment
|
__pycache__/main.cpython-313.pyc
ADDED
|
Binary file (1.8 kB). View file
|
|
|
app/__init__.py
ADDED
|
File without changes
|
app/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (149 Bytes). View file
|
|
|
app/api/__init__.py
ADDED
|
File without changes
|
app/api/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (153 Bytes). View file
|
|
|
app/api/__pycache__/auth_routes.cpython-313.pyc
ADDED
|
Binary file (2.62 kB). View file
|
|
|
app/api/__pycache__/claims.cpython-313.pyc
ADDED
|
Binary file (8.9 kB). View file
|
|
|
app/api/auth_routes.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from app.core.rag import rag_engine
|
| 6 |
+
|
| 7 |
+
router = APIRouter(tags=["Portal Controls"])
|
| 8 |
+
|
| 9 |
+
class PolicyUpdatePayload(BaseModel):
|
| 10 |
+
policy_json: dict
|
| 11 |
+
|
| 12 |
+
# --- User Routes ---
|
| 13 |
+
@router.get("/user/{member_id}/wallet")
|
| 14 |
+
async def get_wallet_limit(member_id: str):
|
| 15 |
+
"""Mocks database retrieval of a user's wallet limit."""
|
| 16 |
+
return {
|
| 17 |
+
"member_id": member_id,
|
| 18 |
+
"total_limit": 50000.00,
|
| 19 |
+
"available_limit": 35000.00 # Example remaining balance
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
# --- Admin Routes ---
|
| 23 |
+
@router.get("/admin/policy")
|
| 24 |
+
async def get_current_policy():
|
| 25 |
+
"""Fetches the current policy JSON for the admin editor."""
|
| 26 |
+
if not os.path.exists(rag_engine.policy_file):
|
| 27 |
+
raise HTTPException(status_code=404, detail="Policy file not found.")
|
| 28 |
+
|
| 29 |
+
with open(rag_engine.policy_file, "r") as f:
|
| 30 |
+
return json.load(f)
|
| 31 |
+
|
| 32 |
+
@router.post("/admin/policy/update")
|
| 33 |
+
async def update_policy(payload: PolicyUpdatePayload):
|
| 34 |
+
"""Overwrites the policy file and instantly rebuilds the AI's RAG memory."""
|
| 35 |
+
with open(rag_engine.policy_file, "w") as f:
|
| 36 |
+
json.dump(payload.policy_json, f, indent=2)
|
| 37 |
+
|
| 38 |
+
# CRITICAL: Rebuild the local FAISS vector database
|
| 39 |
+
rag_engine.initialize()
|
| 40 |
+
|
| 41 |
+
return {"status": "success", "message": "Policy updated. AI memory successfully refreshed."}
|
app/api/claims.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from fastapi import APIRouter, UploadFile, File, Form, Depends, HTTPException, status
|
| 3 |
+
from pydantic import ValidationError
|
| 4 |
+
from thefuzz import fuzz
|
| 5 |
+
from datetime import datetime, timezone, timedelta
|
| 6 |
+
from dateutil.parser import parse
|
| 7 |
+
|
| 8 |
+
from app.models.schemas import ClaimSubmission, AdjudicationResponse, ExtractedClaimData
|
| 9 |
+
from app.core.rag import rag_engine
|
| 10 |
+
from app.core.llm import analyze_claim_documents
|
| 11 |
+
from app.core.database import users_collection, claims_collection
|
| 12 |
+
|
| 13 |
+
from typing import List
|
| 14 |
+
|
| 15 |
+
router = APIRouter(prefix="/claims", tags=["Claims Adjudication"])
|
| 16 |
+
|
| 17 |
+
def parse_date_safely(date_str: str) -> datetime:
|
| 18 |
+
"""Standardizes heterogeneous date strings into exact datetime objects."""
|
| 19 |
+
try:
|
| 20 |
+
return parse(date_str)
|
| 21 |
+
except Exception:
|
| 22 |
+
raise ValueError(f"Unable to parse date string format: {date_str}")
|
| 23 |
+
|
| 24 |
+
@router.post("/adjudicate", response_model=AdjudicationResponse)
|
| 25 |
+
async def adjudicate_claim(
|
| 26 |
+
member_id: str = Form(...),
|
| 27 |
+
claim_amount: float = Form(...),
|
| 28 |
+
cashless_request: bool = Form(False),
|
| 29 |
+
hospital_name: str = Form(None),
|
| 30 |
+
documents: List[UploadFile] = File(...)
|
| 31 |
+
):
|
| 32 |
+
"""
|
| 33 |
+
Main orchestration endpoint for OPD claim adjudication.
|
| 34 |
+
Executes in under 5 seconds by chaining fast local data checks with a single Gemini Vision call.
|
| 35 |
+
"""
|
| 36 |
+
claim_id = f"CLM-{uuid.uuid4().hex[:8].upper()}"
|
| 37 |
+
|
| 38 |
+
# ==========================================
|
| 39 |
+
# GATEKEEPER: THE FAIL-FAST LAYER (~0.05s)
|
| 40 |
+
# ==========================================
|
| 41 |
+
|
| 42 |
+
# 1. Fetch User Data
|
| 43 |
+
user_data = await users_collection.find_one({"member_id": member_id})
|
| 44 |
+
if not user_data:
|
| 45 |
+
raise HTTPException(status_code=404, detail="Member ID not found in database.")
|
| 46 |
+
|
| 47 |
+
early_rejection_reason = None
|
| 48 |
+
|
| 49 |
+
# Gate 1: Policy Status Verification
|
| 50 |
+
if user_data.get("policy_status") != "ACTIVE":
|
| 51 |
+
early_rejection_reason = "POLICY_INACTIVE: Coverage is expired or suspended."
|
| 52 |
+
|
| 53 |
+
# Gate 2: Absolute Limit Verification (Empty Wallet)
|
| 54 |
+
max_opd_limit = user_data.get("opd_wallet_balance", 0.0)
|
| 55 |
+
if max_opd_limit <= 0:
|
| 56 |
+
early_rejection_reason = "LIMIT_EXHAUSTED: Annual OPD wallet balance is zero."
|
| 57 |
+
|
| 58 |
+
# Gate 3: Fraud Velocity Check
|
| 59 |
+
if member_id != "EMP000":
|
| 60 |
+
today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
|
| 61 |
+
claims_today = await claims_collection.count_documents({
|
| 62 |
+
"member_id": member_id,
|
| 63 |
+
"created_at": {"$gte": today_start}
|
| 64 |
+
})
|
| 65 |
+
if claims_today >= 3:
|
| 66 |
+
early_rejection_reason = "FRAUD_FLAG: Claim velocity exceeds daily limits. Manual review required."
|
| 67 |
+
|
| 68 |
+
# --- EARLY EXIT IF GATEKEEPER FAILS ---
|
| 69 |
+
if early_rejection_reason:
|
| 70 |
+
# Save the rejected attempt to the database for auditing
|
| 71 |
+
await claims_collection.insert_one({
|
| 72 |
+
"claim_id": claim_id,
|
| 73 |
+
"member_id": member_id,
|
| 74 |
+
"decision": "REJECTED",
|
| 75 |
+
"requested_amount": claim_amount,
|
| 76 |
+
"approved_amount": 0.0,
|
| 77 |
+
"rejection_reasons": [early_rejection_reason],
|
| 78 |
+
"notes": "Rejected at Gatekeeper level. AI evaluation bypassed.",
|
| 79 |
+
"created_at": datetime.now(timezone.utc)
|
| 80 |
+
})
|
| 81 |
+
|
| 82 |
+
return AdjudicationResponse(
|
| 83 |
+
claim_id=claim_id,
|
| 84 |
+
decision="REJECTED",
|
| 85 |
+
approved_amount=0.0,
|
| 86 |
+
rejection_reasons=[early_rejection_reason],
|
| 87 |
+
confidence_score=1.0,
|
| 88 |
+
notes="Rejected at Gatekeeper level. AI evaluation bypassed.",
|
| 89 |
+
next_steps="Review your policy status and wallet limits.",
|
| 90 |
+
cashless_approved=False,
|
| 91 |
+
network_discount=0.0
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
rejection_reasons = []
|
| 95 |
+
notes_accumulator = []
|
| 96 |
+
|
| 97 |
+
# 1. High-speed File Processing
|
| 98 |
+
try:
|
| 99 |
+
images_bytes_list = []
|
| 100 |
+
for doc in documents:
|
| 101 |
+
images_bytes_list.append({
|
| 102 |
+
"bytes": await doc.read(),
|
| 103 |
+
"mime_type": doc.content_type
|
| 104 |
+
})
|
| 105 |
+
except Exception as e:
|
| 106 |
+
raise HTTPException(
|
| 107 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 108 |
+
detail=f"Failed to read file attachment payload: {str(e)}"
|
| 109 |
+
)
|
| 110 |
+
print(">>> Starting RAG retrieval...")
|
| 111 |
+
# 2. Local Fast Semantic RAG Retrieval (~0.02s)
|
| 112 |
+
rag_context = rag_engine.get_relevant_context(query="OPD exclusions consultation caps policy terms limits validity", k=6)
|
| 113 |
+
print(">>> RAG retrieval complete!")
|
| 114 |
+
# 3. Single-Pass Multimodal Gemini Evaluation (~2-3s)
|
| 115 |
+
try:
|
| 116 |
+
extracted_data: ExtractedClaimData = await analyze_claim_documents(images_bytes_list, rag_context)
|
| 117 |
+
except Exception as e:
|
| 118 |
+
raise HTTPException(
|
| 119 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 120 |
+
detail=f"AI extraction layer execution failed: {str(e)}"
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
# 4. Deterministic Rule Matching & Itemized Adjudication
|
| 124 |
+
base_eligible_amount = 0.0
|
| 125 |
+
|
| 126 |
+
# Loop through itemized claims to calculate the strictly eligible amount
|
| 127 |
+
for item in extracted_data.itemized_claims:
|
| 128 |
+
if item.is_covered and item.is_medically_necessary:
|
| 129 |
+
base_eligible_amount += item.amount
|
| 130 |
+
else:
|
| 131 |
+
# Log the specific item that failed in the notes, NOT as a hard rejection
|
| 132 |
+
reason = item.exclusion_reason if not item.is_covered else item.medical_necessity_reason
|
| 133 |
+
notes_accumulator.append(f"DEDUCTION: {item.description} excluded ({reason}).")
|
| 134 |
+
|
| 135 |
+
# If NO items were valid, then fail the whole claim
|
| 136 |
+
if base_eligible_amount <= 0:
|
| 137 |
+
rejection_reasons.append("No eligible items found. All billed items were excluded or deemed medically unnecessary.")
|
| 138 |
+
|
| 139 |
+
# Check 4c: Fuzzy Name Matching
|
| 140 |
+
name_match_score = fuzz.token_sort_ratio(member_id.lower(), extracted_data.patient_name.lower())
|
| 141 |
+
if name_match_score < 75:
|
| 142 |
+
notes_accumulator.append(f"Warning: Low string variance on patient identity matching ({name_match_score}%). Verify credentials.")
|
| 143 |
+
|
| 144 |
+
# 5. Financial Limit Calculations & Cashless Routing Engine
|
| 145 |
+
max_opd_limit = 5000.00
|
| 146 |
+
|
| 147 |
+
# Apply global policy cap to the VALID items
|
| 148 |
+
if base_eligible_amount > max_opd_limit:
|
| 149 |
+
notes_accumulator.append(f"Claim request bounded by maximum individual policy limit of INR {max_opd_limit}.")
|
| 150 |
+
base_eligible_amount = max_opd_limit
|
| 151 |
+
|
| 152 |
+
network_discount = 0.0
|
| 153 |
+
cashless_approved = False
|
| 154 |
+
|
| 155 |
+
# CASHLESS VS REIMBURSEMENT ROUTING
|
| 156 |
+
if cashless_request:
|
| 157 |
+
network_discount = base_eligible_amount * 0.20 # 20% Hospital Network Discount
|
| 158 |
+
final_approved_amount = base_eligible_amount - network_discount
|
| 159 |
+
cashless_approved = True
|
| 160 |
+
notes_accumulator.append(f"CASHLESS APPROVED: Applied 20% network discount (INR {network_discount}).")
|
| 161 |
+
else:
|
| 162 |
+
copay_ratio = 0.10 # Standard 10% Co-pay for User Reimbursement
|
| 163 |
+
calculated_copay = base_eligible_amount * copay_ratio
|
| 164 |
+
final_approved_amount = base_eligible_amount - calculated_copay
|
| 165 |
+
|
| 166 |
+
if calculated_copay > 0 and final_approved_amount > 0:
|
| 167 |
+
notes_accumulator.append(f"PARTIAL APPROVAL: A mandatory {int(copay_ratio*100)}% co-pay deduction (INR {calculated_copay}) was applied to eligible items.")
|
| 168 |
+
|
| 169 |
+
# 6. Final State Resolution Strategy
|
| 170 |
+
if rejection_reasons:
|
| 171 |
+
# This now only triggers if base_eligible_amount is 0
|
| 172 |
+
final_decision = "REJECTED"
|
| 173 |
+
final_approved_amount = 0.0
|
| 174 |
+
next_steps = "The claim has been rejected based on policy terms. You may submit an appeal via your dashboard with valid counter-documentation."
|
| 175 |
+
elif cashless_request and not rejection_reasons:
|
| 176 |
+
final_decision = "APPROVED"
|
| 177 |
+
next_steps = "Cashless request authorized. The network hospital desk has been notified."
|
| 178 |
+
elif final_approved_amount < extracted_data.total_billed_amount:
|
| 179 |
+
# If they got less than they asked for (due to exclusions, caps, or copays), it's PARTIAL
|
| 180 |
+
final_decision = "PARTIAL"
|
| 181 |
+
next_steps = "Review the breakdown details. Excluded items and co-pays have been deducted."
|
| 182 |
+
else:
|
| 183 |
+
final_decision = "APPROVED"
|
| 184 |
+
next_steps = "No action required. The reimbursement transaction will settle directly to your verified bank account within 24 hours."
|
| 185 |
+
|
| 186 |
+
# 7. Save Final Record & Deduct Wallet
|
| 187 |
+
await claims_collection.insert_one({
|
| 188 |
+
"claim_id": claim_id,
|
| 189 |
+
"member_id": member_id,
|
| 190 |
+
"decision": final_decision,
|
| 191 |
+
"requested_amount": extracted_data.total_billed_amount,
|
| 192 |
+
"approved_amount": final_approved_amount,
|
| 193 |
+
"rejection_reasons": rejection_reasons,
|
| 194 |
+
"notes": " | ".join(notes_accumulator),
|
| 195 |
+
"created_at": datetime.now(timezone.utc)
|
| 196 |
+
})
|
| 197 |
+
|
| 198 |
+
if final_approved_amount > 0:
|
| 199 |
+
await users_collection.update_one(
|
| 200 |
+
{"member_id": member_id},
|
| 201 |
+
{"$inc": {"opd_wallet_balance": -final_approved_amount}}
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
return AdjudicationResponse(
|
| 205 |
+
claim_id=claim_id,
|
| 206 |
+
decision=final_decision,
|
| 207 |
+
approved_amount=final_approved_amount,
|
| 208 |
+
rejection_reasons=rejection_reasons,
|
| 209 |
+
confidence_score=0.92 if not rejection_reasons else 0.98,
|
| 210 |
+
notes=" | ".join(notes_accumulator) if notes_accumulator else "Claim verified across all automated system parameters successfully.",
|
| 211 |
+
next_steps=next_steps,
|
| 212 |
+
cashless_approved=cashless_approved,
|
| 213 |
+
network_discount=network_discount
|
| 214 |
+
)
|
app/core/__init__.py
ADDED
|
File without changes
|
app/core/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (154 Bytes). View file
|
|
|
app/core/__pycache__/config.cpython-313.pyc
ADDED
|
Binary file (946 Bytes). View file
|
|
|
app/core/__pycache__/database.cpython-313.pyc
ADDED
|
Binary file (643 Bytes). View file
|
|
|
app/core/__pycache__/llm.cpython-313.pyc
ADDED
|
Binary file (6.6 kB). View file
|
|
|
app/core/__pycache__/rag.cpython-313.pyc
ADDED
|
Binary file (4.64 kB). View file
|
|
|
app/core/config.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
|
| 4 |
+
# Load variables from the .env file
|
| 5 |
+
load_dotenv()
|
| 6 |
+
|
| 7 |
+
class Settings:
|
| 8 |
+
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 9 |
+
# Defaults to local MongoDB if no URI is provided in .env
|
| 10 |
+
MONGO_URI = os.getenv("MONGO_URI", "mongodb+srv://sudhanp2004:root@cluster0.wsmrt.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0")
|
| 11 |
+
DB_NAME = "plum_opd_db"
|
| 12 |
+
|
| 13 |
+
settings = Settings()
|
| 14 |
+
|
| 15 |
+
if not settings.GEMINI_API_KEY:
|
| 16 |
+
raise ValueError("GEMINI_API_KEY is missing. Please add it to your .env file.")
|
app/core/database.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# from motor.motor_asyncio import AsyncIOMotorClient
|
| 2 |
+
# from app.core.config import settings
|
| 3 |
+
|
| 4 |
+
# class Database:
|
| 5 |
+
# client: AsyncIOMotorClient = None
|
| 6 |
+
|
| 7 |
+
# db = Database()
|
| 8 |
+
|
| 9 |
+
# async def connect_to_mongo():
|
| 10 |
+
# """Initialize the MongoDB connection pool."""
|
| 11 |
+
# db.client = AsyncIOMotorClient(settings.MONGO_URI)
|
| 12 |
+
# print("✅ Connected to MongoDB")
|
| 13 |
+
|
| 14 |
+
# async def close_mongo_connection():
|
| 15 |
+
# """Close the MongoDB connection pool."""
|
| 16 |
+
# if db.client:
|
| 17 |
+
# db.client.close()
|
| 18 |
+
# print("🛑 Closed MongoDB connection")
|
| 19 |
+
|
| 20 |
+
# def get_database():
|
| 21 |
+
# """Dependency to retrieve the database instance."""
|
| 22 |
+
# return db.client[settings.DB_NAME]
|
| 23 |
+
|
| 24 |
+
import os
|
| 25 |
+
from motor.motor_asyncio import AsyncIOMotorClient
|
| 26 |
+
|
| 27 |
+
# Defaulting to localhost, but update this with your MongoDB Atlas URI in production
|
| 28 |
+
MONGO_URI = os.getenv("MONGO_URI", "mongodb+srv://sudhanp2004:root@cluster0.wsmrt.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0")
|
| 29 |
+
|
| 30 |
+
client = AsyncIOMotorClient(MONGO_URI)
|
| 31 |
+
db = client.medical_adjudication
|
| 32 |
+
|
| 33 |
+
users_collection = db.users
|
| 34 |
+
claims_collection = db.claims
|
app/core/llm.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import json
|
| 3 |
+
from PIL import Image
|
| 4 |
+
from google import genai
|
| 5 |
+
from google.genai import types
|
| 6 |
+
from app.core.config import settings
|
| 7 |
+
from app.models.schemas import ExtractedClaimData, ItemizedClaimData
|
| 8 |
+
|
| 9 |
+
from typing import List, Dict
|
| 10 |
+
|
| 11 |
+
# Initialize the official modern Google GenAI Client
|
| 12 |
+
# It automatically picks up GEMINI_API_KEY from the environment/settings
|
| 13 |
+
client = genai.Client(api_key=settings.GEMINI_API_KEY)
|
| 14 |
+
|
| 15 |
+
async def analyze_claim_documents(images_bytes_list: List[Dict[str, any]], rag_policy_context: str) -> ExtractedClaimData:
|
| 16 |
+
"""
|
| 17 |
+
Sends the medical document image and dynamic RAG policy context to Gemini 2.5 Flash.
|
| 18 |
+
Enforces a strict structured JSON output matching the ExtractedClaimData schema.
|
| 19 |
+
"""
|
| 20 |
+
try:
|
| 21 |
+
# Build the single-pass prompt combining extraction rules, RAG guidelines, and formatting schemas
|
| 22 |
+
system_prompt = (
|
| 23 |
+
"You are an expert medical insurance claims adjudication engine. "
|
| 24 |
+
"Your job is to extract data from the provided document image and evaluate it "
|
| 25 |
+
"against the accompanying Policy Context constraints.\n\n"
|
| 26 |
+
"STRICT RULES:\n"
|
| 27 |
+
"1. Extract patient info, treatment dates, provider details, and itemized lists accurately.\n"
|
| 28 |
+
"2. Cross-reference all extracted data against the provided Policy Context to determine "
|
| 29 |
+
"if any items match listed exclusions.\n"
|
| 30 |
+
"3. Assess medical necessity: verify if the clinical diagnosis logically justifies the "
|
| 31 |
+
"prescribed drugs, procedures, or diagnostic tests.\n"
|
| 32 |
+
"4. Return structural results matching the requested JSON schema exactly."
|
| 33 |
+
"5.Cross-reference the Patient's Diagnosis and Treatment against the POLICY CONTEXT below. You must medically evaluate if the treatment is a synonym for an excluded category (e.g., Bariatric/Obesity = Weight Loss). If it matches an exclusion, you MUST set `is_treatment_excluded` to true and provide the exact `exclusion_reason`."
|
| 34 |
+
"6.Check the `pre_authorization_required` list in the POLICY CONTEXT. If the billed items include a procedure on that list (like an MRI Scan), you MUST set `requires_pre_auth` to true. If the receipt/prescription does NOT explicitly mention an approved authorization code, you MUST also set `missing_pre_auth` to true."
|
| 35 |
+
"CRITICAL - ITEMIZED EVALUATION (NOT reject all if one item fails):\n"
|
| 36 |
+
"7. For EVERY line item (medicine, procedure, test, consultation), determine:\n"
|
| 37 |
+
" - Is it in the policy exclusions list?\n"
|
| 38 |
+
" - Is it medically necessary given the diagnosis?\n"
|
| 39 |
+
" - Does it match standard clinical protocols?\n"
|
| 40 |
+
"8. Mark is_covered and is_medically_necessary for each item INDEPENDENTLY.\n"
|
| 41 |
+
"9. IMPORTANT: Do NOT reject entire claim—evaluate items separately.\n"
|
| 42 |
+
" Some items may be covered, others excluded. That's OK—return individual assessments.\n"
|
| 43 |
+
"10. Break down the bill into individual line items. For each item, populate the itemized_claims list.\n"
|
| 44 |
+
"11. Return ONLY valid JSON matching the schema. No markdown, no code blocks.\n\n"
|
| 45 |
+
"RESPONSE FORMAT - Return ONLY this JSON structure (no other text):\n"
|
| 46 |
+
"{\n"
|
| 47 |
+
' "patient_name": "string",\n'
|
| 48 |
+
' "treatment_date": "YYYY-MM-DD",\n'
|
| 49 |
+
' "doctor_name": "string",\n'
|
| 50 |
+
' "doctor_reg_no": "string",\n'
|
| 51 |
+
' "diagnosis": "string",\n'
|
| 52 |
+
' "itemized_claims": [\n'
|
| 53 |
+
' {\n'
|
| 54 |
+
' "description": "item name",\n'
|
| 55 |
+
' "amount": 1000.50,\n'
|
| 56 |
+
' "category": "medicine|procedure|diagnostic_test|consultation",\n'
|
| 57 |
+
' "is_covered": true/false,\n'
|
| 58 |
+
' "exclusion_reason": "reason or None",\n'
|
| 59 |
+
' "is_medically_necessary": true/false,\n'
|
| 60 |
+
' "medical_necessity_reason": "reason"\n'
|
| 61 |
+
" }\n"
|
| 62 |
+
" ],\n"
|
| 63 |
+
' "total_billed_amount": 5000.00,\n'
|
| 64 |
+
' "overall_medically_necessary": true,\n'
|
| 65 |
+
' "medical_necessity_reasoning": "overall assessment"\n'
|
| 66 |
+
"}"
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
user_content = f"""
|
| 70 |
+
POLICY CONTEXT (Retrieved rules for this claim context):
|
| 71 |
+
-----------------------------------------------------
|
| 72 |
+
{rag_policy_context}
|
| 73 |
+
-----------------------------------------------------
|
| 74 |
+
|
| 75 |
+
Please evaluate the attached medical document image according to the rules above.
|
| 76 |
+
Extract each line item (medicine, procedure, test, consultation) separately.
|
| 77 |
+
For EACH item, determine: is it covered by policy? Is it medically necessary?
|
| 78 |
+
Return ONLY JSON, no explanations.
|
| 79 |
+
"""
|
| 80 |
+
|
| 81 |
+
contents = [user_content]
|
| 82 |
+
|
| 83 |
+
for img_bytes in images_bytes_list:
|
| 84 |
+
contents.append(
|
| 85 |
+
types.Part.from_bytes(
|
| 86 |
+
data=img_bytes["bytes"],
|
| 87 |
+
mime_type=img_bytes["mime_type"]
|
| 88 |
+
)
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
print(">>> 1. Preparing to call Gemini API...")
|
| 92 |
+
# Execute the single-pass multimodal API call without strict schema (use text JSON instead)
|
| 93 |
+
response = await client.aio.models.generate_content(
|
| 94 |
+
model='gemini-2.5-flash',
|
| 95 |
+
contents=contents,
|
| 96 |
+
config=types.GenerateContentConfig(
|
| 97 |
+
system_instruction=system_prompt,
|
| 98 |
+
response_mime_type="application/json",
|
| 99 |
+
temperature=0.1, # Low temperature ensures highly predictable, deterministic evaluation
|
| 100 |
+
)
|
| 101 |
+
)
|
| 102 |
+
print(">>> 2. Gemini API Call Complete!")
|
| 103 |
+
# Parse the response text as JSON and validate with Pydantic
|
| 104 |
+
response_text = response.text.strip()
|
| 105 |
+
|
| 106 |
+
# Remove markdown code blocks if present
|
| 107 |
+
if response_text.startswith("```json"):
|
| 108 |
+
response_text = response_text[7:]
|
| 109 |
+
if response_text.startswith("```"):
|
| 110 |
+
response_text = response_text[3:]
|
| 111 |
+
if response_text.endswith("```"):
|
| 112 |
+
response_text = response_text[:-3]
|
| 113 |
+
|
| 114 |
+
response_text = response_text.strip()
|
| 115 |
+
|
| 116 |
+
print(f"DEBUG: Raw Gemini Response:\n{response_text[:500]}")
|
| 117 |
+
|
| 118 |
+
response_json = json.loads(response_text)
|
| 119 |
+
return ExtractedClaimData.model_validate(response_json)
|
| 120 |
+
|
| 121 |
+
except json.JSONDecodeError as je:
|
| 122 |
+
print(f"❌ JSON Parse Error: {str(je)}")
|
| 123 |
+
print(f"Response text: {response_text[:1000]}")
|
| 124 |
+
raise RuntimeError(f"Failed to parse Gemini JSON response: {str(je)}")
|
| 125 |
+
except Exception as e:
|
| 126 |
+
print(f"❌ Gemini Adjudication Call Failed: {str(e)}")
|
| 127 |
+
raise RuntimeError(f"Failed to process document via Gemini AI: {str(e)}")
|
app/core/rag.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from langchain_core.documents import Document
|
| 4 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 5 |
+
from langchain_community.vectorstores import FAISS
|
| 6 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 7 |
+
|
| 8 |
+
class PolicyRAG:
|
| 9 |
+
def __init__(self):
|
| 10 |
+
self.vector_store = None
|
| 11 |
+
# Use a small, lightning-fast model for local embeddings
|
| 12 |
+
self.embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
|
| 13 |
+
|
| 14 |
+
# Paths to your admin data
|
| 15 |
+
self.base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 16 |
+
self.policy_file = os.path.join(self.base_dir, "data", "policy_terms.json")
|
| 17 |
+
self.rules_file = os.path.join(self.base_dir, "data", "adjudication_rules.md")
|
| 18 |
+
|
| 19 |
+
def initialize(self):
|
| 20 |
+
"""
|
| 21 |
+
Loads the policy and rules files, chunks them, and builds the FAISS index in memory.
|
| 22 |
+
This runs ONLY ONCE when the FastAPI server starts.
|
| 23 |
+
"""
|
| 24 |
+
print("🧠 Initializing LangChain RAG Vector Database...")
|
| 25 |
+
documents = []
|
| 26 |
+
|
| 27 |
+
# 1. Load and parse JSON Policy Terms
|
| 28 |
+
if os.path.exists(self.policy_file):
|
| 29 |
+
with open(self.policy_file, "r") as f:
|
| 30 |
+
policy_data = json.load(f)
|
| 31 |
+
# Convert the JSON dictionary into a flat readable string for the LLM
|
| 32 |
+
policy_text = json.dumps(policy_data, indent=2)
|
| 33 |
+
documents.append(Document(page_content=f"POLICY TERMS AND LIMITS:\n{policy_text}", metadata={"source": "policy_terms"}))
|
| 34 |
+
else:
|
| 35 |
+
print("⚠️ Warning: policy_terms.json not found.")
|
| 36 |
+
|
| 37 |
+
# 2. Load Markdown Adjudication Rules
|
| 38 |
+
if os.path.exists(self.rules_file):
|
| 39 |
+
with open(self.rules_file, "r") as f:
|
| 40 |
+
rules_text = f.read()
|
| 41 |
+
documents.append(Document(page_content=f"ADJUDICATION RULES:\n{rules_text}", metadata={"source": "adjudication_rules"}))
|
| 42 |
+
else:
|
| 43 |
+
print("⚠️ Warning: adjudication_rules.md not found.")
|
| 44 |
+
|
| 45 |
+
# 3. Chunk the documents so we only pull relevant sections
|
| 46 |
+
text_splitter = RecursiveCharacterTextSplitter(
|
| 47 |
+
chunk_size=1000,
|
| 48 |
+
chunk_overlap=100,
|
| 49 |
+
separators=["\n## ", "\n### ", "\n", " ", ""]
|
| 50 |
+
)
|
| 51 |
+
chunks = text_splitter.split_documents(documents)
|
| 52 |
+
|
| 53 |
+
# 4. Build the FAISS Vector Database
|
| 54 |
+
if chunks:
|
| 55 |
+
self.vector_store = FAISS.from_documents(chunks, self.embeddings)
|
| 56 |
+
print(f"✅ RAG initialized successfully with {len(chunks)} knowledge chunks.")
|
| 57 |
+
else:
|
| 58 |
+
print("❌ RAG initialization failed: No documents loaded.")
|
| 59 |
+
|
| 60 |
+
def get_relevant_context(self, query: str, k: int = 3) -> str:
|
| 61 |
+
"""
|
| 62 |
+
Searches the FAISS database for the most relevant policy chunks based on the query.
|
| 63 |
+
Execution time: ~0.02 seconds.
|
| 64 |
+
"""
|
| 65 |
+
if not self.vector_store:
|
| 66 |
+
return "No policy context available."
|
| 67 |
+
|
| 68 |
+
# Retrieve top k matching documents
|
| 69 |
+
docs = self.vector_store.similarity_search(query, k=k)
|
| 70 |
+
|
| 71 |
+
# Combine the content into a single string for the Gemini prompt
|
| 72 |
+
context = "\n\n---\n\n".join([doc.page_content for doc in docs])
|
| 73 |
+
return context
|
| 74 |
+
|
| 75 |
+
# Create a singleton instance to be imported by the API router
|
| 76 |
+
rag_engine = PolicyRAG()
|
app/data/adjudication_rules.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OPD Claim Adjudication Rules
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
This document outlines the rules and logic for adjudicating (approving/rejecting) OPD insurance claims. The system should evaluate claims based on these rules in the specified order.
|
| 5 |
+
|
| 6 |
+
## Adjudication Flow
|
| 7 |
+
|
| 8 |
+
### Step 1: Basic Eligibility Check
|
| 9 |
+
- **Policy Status**: Policy must be active on the date of treatment
|
| 10 |
+
- **Waiting Period**: Check if waiting periods have been satisfied
|
| 11 |
+
- **Member Verification**: Claimant must be a covered member (employee/dependent)
|
| 12 |
+
|
| 13 |
+
### Step 2: Document Validation
|
| 14 |
+
All submitted documents must meet these criteria:
|
| 15 |
+
- **Legibility**: Documents must be clear and readable
|
| 16 |
+
- **Completeness**: All required fields must be visible
|
| 17 |
+
- **Authenticity**:
|
| 18 |
+
- Doctor's registration number must be valid (format: [State Code]/[Number]/[Year])
|
| 19 |
+
- Hospital/Clinic registration must be verifiable
|
| 20 |
+
- Bills must have proper headers and stamps
|
| 21 |
+
- **Date Consistency**: All documents must have matching treatment dates
|
| 22 |
+
- **Patient Details**: Name and age must match policy records (minor variations acceptable)
|
| 23 |
+
|
| 24 |
+
### Step 3: Coverage Verification
|
| 25 |
+
Check if the treatment/service is covered:
|
| 26 |
+
- Compare against covered services list
|
| 27 |
+
- Verify it's not in exclusions list
|
| 28 |
+
- Check for pre-authorization requirements
|
| 29 |
+
|
| 30 |
+
### Step 4: Limit Validation
|
| 31 |
+
Verify claim amount against applicable limits:
|
| 32 |
+
1. **Annual Limit**: Total claims YTD + current claim ≤ Annual limit
|
| 33 |
+
2. **Sub-limits**: Category-specific limits (consultation, pharmacy, etc.)
|
| 34 |
+
3. **Per-claim Limit**: Single claim cannot exceed per-claim limit
|
| 35 |
+
4. **Co-payment Calculation**: Apply co-pay percentages where applicable
|
| 36 |
+
|
| 37 |
+
### Step 5: Medical Necessity Review
|
| 38 |
+
Evaluate if treatment was medically necessary:
|
| 39 |
+
- Diagnosis must justify the treatment
|
| 40 |
+
- Prescription must align with diagnosis
|
| 41 |
+
- Test results must support the diagnosis (if applicable)
|
| 42 |
+
- Treatment must follow standard medical protocols
|
| 43 |
+
|
| 44 |
+
## Approval Conditions
|
| 45 |
+
A claim is **APPROVED** when ALL of the following are true:
|
| 46 |
+
- ✅ Policy is active and waiting period satisfied
|
| 47 |
+
- ✅ All required documents are submitted and valid
|
| 48 |
+
- ✅ Treatment is covered under policy
|
| 49 |
+
- ✅ Claim amount is within limits (after co-pay)
|
| 50 |
+
- ✅ Medical necessity is established
|
| 51 |
+
- ✅ No fraud indicators detected
|
| 52 |
+
|
| 53 |
+
## Rejection Reasons
|
| 54 |
+
A claim is **REJECTED** if ANY of the following apply:
|
| 55 |
+
|
| 56 |
+
### Category 1: Eligibility Issues
|
| 57 |
+
- `POLICY_INACTIVE`: Policy not active on treatment date
|
| 58 |
+
- `WAITING_PERIOD`: Treatment during waiting period
|
| 59 |
+
- `MEMBER_NOT_COVERED`: Claimant not found in policy records
|
| 60 |
+
|
| 61 |
+
### Category 2: Documentation Issues
|
| 62 |
+
- `MISSING_DOCUMENTS`: Required documents not submitted
|
| 63 |
+
- `ILLEGIBLE_DOCUMENTS`: Documents not readable
|
| 64 |
+
- `INVALID_PRESCRIPTION`: Prescription missing or invalid
|
| 65 |
+
- `DOCTOR_REG_INVALID`: Doctor registration number invalid/missing
|
| 66 |
+
- `DATE_MISMATCH`: Document dates don't match
|
| 67 |
+
- `PATIENT_MISMATCH`: Patient details don't match records
|
| 68 |
+
|
| 69 |
+
### Category 3: Coverage Issues
|
| 70 |
+
- `SERVICE_NOT_COVERED`: Treatment/service not covered
|
| 71 |
+
- `EXCLUDED_CONDITION`: Condition in exclusions list
|
| 72 |
+
- `PRE_AUTH_MISSING`: Pre-authorization required but not obtained
|
| 73 |
+
|
| 74 |
+
### Category 4: Limit Issues
|
| 75 |
+
- `ANNUAL_LIMIT_EXCEEDED`: Annual limit exhausted
|
| 76 |
+
- `SUB_LIMIT_EXCEEDED`: Category sub-limit exceeded
|
| 77 |
+
- `PER_CLAIM_EXCEEDED`: Single claim limit exceeded
|
| 78 |
+
|
| 79 |
+
### Category 5: Medical Issues
|
| 80 |
+
- `NOT_MEDICALLY_NECESSARY`: Treatment not justified by diagnosis
|
| 81 |
+
- `EXPERIMENTAL_TREATMENT`: Experimental/unproven treatment
|
| 82 |
+
- `COSMETIC_PROCEDURE`: Cosmetic/aesthetic procedure
|
| 83 |
+
|
| 84 |
+
### Category 6: Process Issues
|
| 85 |
+
- `LATE_SUBMISSION`: Submitted after 30-day deadline
|
| 86 |
+
- `DUPLICATE_CLAIM`: Same treatment already claimed
|
| 87 |
+
- `BELOW_MIN_AMOUNT`: Claim below ₹500 minimum
|
| 88 |
+
|
| 89 |
+
## Special Scenarios
|
| 90 |
+
|
| 91 |
+
### 1. Partial Approval
|
| 92 |
+
Claims can be partially approved when:
|
| 93 |
+
- Part of the treatment is covered, part is not
|
| 94 |
+
- Claim exceeds limits (approve up to limit)
|
| 95 |
+
- Co-payment applies
|
| 96 |
+
|
| 97 |
+
### 2. Refer for Manual Review
|
| 98 |
+
Send for human review when:
|
| 99 |
+
- Fraud indicators detected (unusual patterns, modified documents)
|
| 100 |
+
- High-value claims (>₹25,000)
|
| 101 |
+
- Complex medical conditions
|
| 102 |
+
- System confidence <70%
|
| 103 |
+
- Member appeals automated decision
|
| 104 |
+
|
| 105 |
+
### 3. Network vs Non-Network
|
| 106 |
+
- **Network providers**: Apply network discounts, cashless possible
|
| 107 |
+
- **Non-network**: Full payment by member, standard reimbursement
|
| 108 |
+
|
| 109 |
+
## Fraud Indicators
|
| 110 |
+
Watch for these red flags:
|
| 111 |
+
- Multiple claims from same provider on same day
|
| 112 |
+
- Unusually high frequency of claims
|
| 113 |
+
- Bills with suspicious alterations
|
| 114 |
+
- Diagnosis not matching age/gender
|
| 115 |
+
- Duplicate bills across different dates
|
| 116 |
+
- Provider not registered/blacklisted
|
| 117 |
+
|
| 118 |
+
## Decision Output Format
|
| 119 |
+
Every decision should include:
|
| 120 |
+
```json
|
| 121 |
+
{
|
| 122 |
+
"claim_id": "CLM_XXXXX",
|
| 123 |
+
"decision": "APPROVED/REJECTED/PARTIAL/MANUAL_REVIEW",
|
| 124 |
+
"approved_amount": 0000,
|
| 125 |
+
"rejection_reasons": [],
|
| 126 |
+
"confidence_score": 0.95,
|
| 127 |
+
"notes": "Additional observations",
|
| 128 |
+
"next_steps": "What the claimant should do"
|
| 129 |
+
}
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
## Priority Rules
|
| 133 |
+
When multiple rules conflict:
|
| 134 |
+
1. Safety first (reject suspicious/fraudulent claims)
|
| 135 |
+
2. Policy exclusions override everything
|
| 136 |
+
3. Hard limits cannot be exceeded
|
| 137 |
+
4. Medical necessity is mandatory
|
| 138 |
+
5. When in doubt, refer for manual review
|
app/data/policy_terms.json
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"policy_id": "PLUM_OPD_2024",
|
| 3 |
+
"policy_name": "Plum OPD Advantage",
|
| 4 |
+
"effective_date": "2024-01-01",
|
| 5 |
+
"policy_holder": {
|
| 6 |
+
"company": "TechCorp Solutions Pvt Ltd",
|
| 7 |
+
"employees_covered": 500,
|
| 8 |
+
"dependents_covered": true
|
| 9 |
+
},
|
| 10 |
+
"coverage_details": {
|
| 11 |
+
"annual_limit": 50000,
|
| 12 |
+
"per_claim_limit": 5000,
|
| 13 |
+
"family_floater_limit": 150000,
|
| 14 |
+
"consultation_fees": {
|
| 15 |
+
"covered": true,
|
| 16 |
+
"sub_limit": 2000,
|
| 17 |
+
"copay_percentage": 10,
|
| 18 |
+
"network_discount": 20
|
| 19 |
+
},
|
| 20 |
+
"diagnostic_tests": {
|
| 21 |
+
"covered": true,
|
| 22 |
+
"sub_limit": 10000,
|
| 23 |
+
"pre_authorization_required": false,
|
| 24 |
+
"covered_tests": [
|
| 25 |
+
"Blood tests",
|
| 26 |
+
"Urine tests",
|
| 27 |
+
"X-rays",
|
| 28 |
+
"ECG",
|
| 29 |
+
"Ultrasound",
|
| 30 |
+
"MRI (with pre-auth)",
|
| 31 |
+
"CT Scan (with pre-auth)"
|
| 32 |
+
]
|
| 33 |
+
},
|
| 34 |
+
"pharmacy": {
|
| 35 |
+
"covered": true,
|
| 36 |
+
"sub_limit": 15000,
|
| 37 |
+
"generic_drugs_mandatory": true,
|
| 38 |
+
"branded_drugs_copay": 30
|
| 39 |
+
},
|
| 40 |
+
"dental": {
|
| 41 |
+
"covered": true,
|
| 42 |
+
"sub_limit": 10000,
|
| 43 |
+
"routine_checkup_limit": 2000,
|
| 44 |
+
"procedures_covered": [
|
| 45 |
+
"Filling",
|
| 46 |
+
"Extraction",
|
| 47 |
+
"Root canal",
|
| 48 |
+
"Cleaning"
|
| 49 |
+
],
|
| 50 |
+
"cosmetic_procedures": false
|
| 51 |
+
},
|
| 52 |
+
"vision": {
|
| 53 |
+
"covered": true,
|
| 54 |
+
"sub_limit": 5000,
|
| 55 |
+
"eye_test_covered": true,
|
| 56 |
+
"glasses_contact_lenses": true,
|
| 57 |
+
"lasik_surgery": false
|
| 58 |
+
},
|
| 59 |
+
"alternative_medicine": {
|
| 60 |
+
"covered": true,
|
| 61 |
+
"sub_limit": 8000,
|
| 62 |
+
"covered_treatments": [
|
| 63 |
+
"Ayurveda",
|
| 64 |
+
"Homeopathy",
|
| 65 |
+
"Unani"
|
| 66 |
+
],
|
| 67 |
+
"therapy_sessions_limit": 20
|
| 68 |
+
}
|
| 69 |
+
},
|
| 70 |
+
"waiting_periods": {
|
| 71 |
+
"initial_waiting": 30,
|
| 72 |
+
"pre_existing_diseases": 365,
|
| 73 |
+
"maternity": 270,
|
| 74 |
+
"specific_ailments": {
|
| 75 |
+
"diabetes": 90,
|
| 76 |
+
"hypertension": 90,
|
| 77 |
+
"joint_replacement": 730
|
| 78 |
+
}
|
| 79 |
+
},
|
| 80 |
+
"exclusions": [
|
| 81 |
+
"Cosmetic procedures",
|
| 82 |
+
"Weight loss treatments",
|
| 83 |
+
"Infertility treatments",
|
| 84 |
+
"Experimental treatments",
|
| 85 |
+
"Self-inflicted injuries",
|
| 86 |
+
"Adventure sports injuries",
|
| 87 |
+
"War and nuclear risks",
|
| 88 |
+
"HIV/AIDS treatment",
|
| 89 |
+
"Alcoholism/drug abuse treatment",
|
| 90 |
+
"Non-allopathic treatments (except listed)",
|
| 91 |
+
"Vitamins and supplements (unless prescribed for deficiency)"
|
| 92 |
+
],
|
| 93 |
+
"claim_requirements": {
|
| 94 |
+
"documents_required": [
|
| 95 |
+
"Original bills and receipts",
|
| 96 |
+
"Prescription from registered doctor",
|
| 97 |
+
"Diagnostic test reports (if applicable)",
|
| 98 |
+
"Pharmacy bills with prescription",
|
| 99 |
+
"Doctor's registration number must be visible",
|
| 100 |
+
"Patient details must match policy records"
|
| 101 |
+
],
|
| 102 |
+
"submission_timeline_days": 30,
|
| 103 |
+
"minimum_claim_amount": 500
|
| 104 |
+
},
|
| 105 |
+
"network_hospitals": [
|
| 106 |
+
"Apollo Hospitals",
|
| 107 |
+
"Fortis Healthcare",
|
| 108 |
+
"Max Healthcare",
|
| 109 |
+
"Manipal Hospitals",
|
| 110 |
+
"Narayana Health"
|
| 111 |
+
],
|
| 112 |
+
"cashless_facilities": {
|
| 113 |
+
"available": true,
|
| 114 |
+
"network_only": true,
|
| 115 |
+
"pre_approval_required": false,
|
| 116 |
+
"instant_approval_limit": 5000
|
| 117 |
+
}
|
| 118 |
+
}
|
app/data/test_cases.json
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"test_cases": [
|
| 3 |
+
{
|
| 4 |
+
"case_id": "TC001",
|
| 5 |
+
"case_name": "Simple Consultation - Approved",
|
| 6 |
+
"description": "Regular doctor consultation for fever, all documents valid",
|
| 7 |
+
"input_data": {
|
| 8 |
+
"member_id": "EMP001",
|
| 9 |
+
"member_name": "Rajesh Kumar",
|
| 10 |
+
"treatment_date": "2024-11-01",
|
| 11 |
+
"claim_amount": 1500,
|
| 12 |
+
"documents": {
|
| 13 |
+
"prescription": {
|
| 14 |
+
"doctor_name": "Dr. Sharma",
|
| 15 |
+
"doctor_reg": "KA/45678/2015",
|
| 16 |
+
"diagnosis": "Viral fever",
|
| 17 |
+
"medicines_prescribed": ["Paracetamol 650mg", "Vitamin C"]
|
| 18 |
+
},
|
| 19 |
+
"bill": {
|
| 20 |
+
"consultation_fee": 1000,
|
| 21 |
+
"diagnostic_tests": 500,
|
| 22 |
+
"test_names": ["CBC", "Dengue test"]
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
},
|
| 26 |
+
"expected_output": {
|
| 27 |
+
"decision": "APPROVED",
|
| 28 |
+
"approved_amount": 1350,
|
| 29 |
+
"deductions": {
|
| 30 |
+
"copay": 150
|
| 31 |
+
},
|
| 32 |
+
"confidence_score": 0.95
|
| 33 |
+
}
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"case_id": "TC002",
|
| 37 |
+
"case_name": "Dental Treatment - Partial Approval",
|
| 38 |
+
"description": "Root canal treatment with cosmetic whitening",
|
| 39 |
+
"input_data": {
|
| 40 |
+
"member_id": "EMP002",
|
| 41 |
+
"member_name": "Priya Singh",
|
| 42 |
+
"treatment_date": "2024-10-15",
|
| 43 |
+
"claim_amount": 12000,
|
| 44 |
+
"documents": {
|
| 45 |
+
"prescription": {
|
| 46 |
+
"doctor_name": "Dr. Patel",
|
| 47 |
+
"doctor_reg": "MH/23456/2018",
|
| 48 |
+
"diagnosis": "Tooth decay requiring root canal",
|
| 49 |
+
"procedures": ["Root canal treatment", "Teeth whitening"]
|
| 50 |
+
},
|
| 51 |
+
"bill": {
|
| 52 |
+
"root_canal": 8000,
|
| 53 |
+
"teeth_whitening": 4000
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
},
|
| 57 |
+
"expected_output": {
|
| 58 |
+
"decision": "PARTIAL",
|
| 59 |
+
"approved_amount": 8000,
|
| 60 |
+
"rejected_items": ["Teeth whitening - cosmetic procedure"],
|
| 61 |
+
"confidence_score": 0.92
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"case_id": "TC003",
|
| 66 |
+
"case_name": "Limit Exceeded - Rejected",
|
| 67 |
+
"description": "Claim exceeds per-claim limit",
|
| 68 |
+
"input_data": {
|
| 69 |
+
"member_id": "EMP003",
|
| 70 |
+
"member_name": "Amit Verma",
|
| 71 |
+
"treatment_date": "2024-10-20",
|
| 72 |
+
"claim_amount": 7500,
|
| 73 |
+
"documents": {
|
| 74 |
+
"prescription": {
|
| 75 |
+
"doctor_name": "Dr. Gupta",
|
| 76 |
+
"doctor_reg": "DL/34567/2016",
|
| 77 |
+
"diagnosis": "Gastroenteritis",
|
| 78 |
+
"medicines_prescribed": ["Antibiotics", "Probiotics"]
|
| 79 |
+
},
|
| 80 |
+
"bill": {
|
| 81 |
+
"consultation_fee": 2000,
|
| 82 |
+
"medicines": 5500
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
},
|
| 86 |
+
"expected_output": {
|
| 87 |
+
"decision": "REJECTED",
|
| 88 |
+
"rejection_reasons": ["PER_CLAIM_EXCEEDED"],
|
| 89 |
+
"notes": "Claim amount exceeds per-claim limit of ₹5000",
|
| 90 |
+
"confidence_score": 0.98
|
| 91 |
+
}
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"case_id": "TC004",
|
| 95 |
+
"case_name": "Missing Documents - Rejected",
|
| 96 |
+
"description": "Prescription missing from claim submission",
|
| 97 |
+
"input_data": {
|
| 98 |
+
"member_id": "EMP004",
|
| 99 |
+
"member_name": "Sneha Reddy",
|
| 100 |
+
"treatment_date": "2024-10-25",
|
| 101 |
+
"claim_amount": 2000,
|
| 102 |
+
"documents": {
|
| 103 |
+
"bill": {
|
| 104 |
+
"consultation_fee": 1500,
|
| 105 |
+
"medicines": 500
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
},
|
| 109 |
+
"expected_output": {
|
| 110 |
+
"decision": "REJECTED",
|
| 111 |
+
"rejection_reasons": ["MISSING_DOCUMENTS"],
|
| 112 |
+
"notes": "Prescription from registered doctor is required",
|
| 113 |
+
"confidence_score": 1.0
|
| 114 |
+
}
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"case_id": "TC005",
|
| 118 |
+
"case_name": "Pre-existing Condition - Waiting Period",
|
| 119 |
+
"description": "Diabetes treatment within waiting period",
|
| 120 |
+
"input_data": {
|
| 121 |
+
"member_id": "EMP005",
|
| 122 |
+
"member_name": "Vikram Joshi",
|
| 123 |
+
"member_join_date": "2024-09-01",
|
| 124 |
+
"treatment_date": "2024-10-15",
|
| 125 |
+
"claim_amount": 3000,
|
| 126 |
+
"documents": {
|
| 127 |
+
"prescription": {
|
| 128 |
+
"doctor_name": "Dr. Mehta",
|
| 129 |
+
"doctor_reg": "GJ/56789/2014",
|
| 130 |
+
"diagnosis": "Type 2 Diabetes",
|
| 131 |
+
"medicines_prescribed": ["Metformin", "Glimepiride"]
|
| 132 |
+
},
|
| 133 |
+
"bill": {
|
| 134 |
+
"consultation_fee": 1000,
|
| 135 |
+
"medicines": 2000
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
},
|
| 139 |
+
"expected_output": {
|
| 140 |
+
"decision": "REJECTED",
|
| 141 |
+
"rejection_reasons": ["WAITING_PERIOD"],
|
| 142 |
+
"notes": "Diabetes has 90-day waiting period. Eligible from 2024-11-30",
|
| 143 |
+
"confidence_score": 0.96
|
| 144 |
+
}
|
| 145 |
+
},
|
| 146 |
+
{
|
| 147 |
+
"case_id": "TC006",
|
| 148 |
+
"case_name": "Alternative Medicine - Approved",
|
| 149 |
+
"description": "Ayurvedic treatment within limits",
|
| 150 |
+
"input_data": {
|
| 151 |
+
"member_id": "EMP006",
|
| 152 |
+
"member_name": "Kavita Nair",
|
| 153 |
+
"treatment_date": "2024-10-28",
|
| 154 |
+
"claim_amount": 4000,
|
| 155 |
+
"documents": {
|
| 156 |
+
"prescription": {
|
| 157 |
+
"doctor_name": "Vaidya Krishnan",
|
| 158 |
+
"doctor_reg": "AYUR/KL/2345/2019",
|
| 159 |
+
"diagnosis": "Chronic joint pain",
|
| 160 |
+
"treatment": "Panchakarma therapy"
|
| 161 |
+
},
|
| 162 |
+
"bill": {
|
| 163 |
+
"consultation_fee": 1000,
|
| 164 |
+
"therapy_charges": 3000
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
},
|
| 168 |
+
"expected_output": {
|
| 169 |
+
"decision": "APPROVED",
|
| 170 |
+
"approved_amount": 4000,
|
| 171 |
+
"notes": "Alternative medicine covered under policy",
|
| 172 |
+
"confidence_score": 0.89
|
| 173 |
+
}
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"case_id": "TC007",
|
| 177 |
+
"case_name": "Diagnostic Tests - Pre-auth Required",
|
| 178 |
+
"description": "MRI scan without pre-authorization",
|
| 179 |
+
"input_data": {
|
| 180 |
+
"member_id": "EMP007",
|
| 181 |
+
"member_name": "Suresh Patil",
|
| 182 |
+
"treatment_date": "2024-11-02",
|
| 183 |
+
"claim_amount": 15000,
|
| 184 |
+
"documents": {
|
| 185 |
+
"prescription": {
|
| 186 |
+
"doctor_name": "Dr. Rao",
|
| 187 |
+
"doctor_reg": "AP/67890/2017",
|
| 188 |
+
"diagnosis": "Suspected lumbar disc herniation",
|
| 189 |
+
"tests_prescribed": ["MRI Lumbar Spine"]
|
| 190 |
+
},
|
| 191 |
+
"bill": {
|
| 192 |
+
"mri_scan": 15000
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
},
|
| 196 |
+
"expected_output": {
|
| 197 |
+
"decision": "REJECTED",
|
| 198 |
+
"rejection_reasons": ["PRE_AUTH_MISSING"],
|
| 199 |
+
"notes": "MRI requires pre-authorization for claims above ₹10000",
|
| 200 |
+
"confidence_score": 0.94
|
| 201 |
+
}
|
| 202 |
+
},
|
| 203 |
+
{
|
| 204 |
+
"case_id": "TC008",
|
| 205 |
+
"case_name": "Fraud Detection - Manual Review",
|
| 206 |
+
"description": "Multiple high-value claims same day",
|
| 207 |
+
"input_data": {
|
| 208 |
+
"member_id": "EMP008",
|
| 209 |
+
"member_name": "Ravi Menon",
|
| 210 |
+
"treatment_date": "2024-10-30",
|
| 211 |
+
"claim_amount": 4800,
|
| 212 |
+
"previous_claims_same_day": 3,
|
| 213 |
+
"documents": {
|
| 214 |
+
"prescription": {
|
| 215 |
+
"doctor_name": "Dr. Khan",
|
| 216 |
+
"doctor_reg": "UP/45678/2016",
|
| 217 |
+
"diagnosis": "Migraine",
|
| 218 |
+
"medicines_prescribed": ["Sumatriptan", "Propranolol"]
|
| 219 |
+
},
|
| 220 |
+
"bill": {
|
| 221 |
+
"consultation_fee": 2000,
|
| 222 |
+
"medicines": 2800
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
},
|
| 226 |
+
"expected_output": {
|
| 227 |
+
"decision": "MANUAL_REVIEW",
|
| 228 |
+
"flags": ["Multiple claims same day", "Unusual pattern detected"],
|
| 229 |
+
"confidence_score": 0.65
|
| 230 |
+
}
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
"case_id": "TC009",
|
| 234 |
+
"case_name": "Excluded Treatment - Rejected",
|
| 235 |
+
"description": "Weight loss treatment not covered",
|
| 236 |
+
"input_data": {
|
| 237 |
+
"member_id": "EMP009",
|
| 238 |
+
"member_name": "Anita Desai",
|
| 239 |
+
"treatment_date": "2024-10-18",
|
| 240 |
+
"claim_amount": 8000,
|
| 241 |
+
"documents": {
|
| 242 |
+
"prescription": {
|
| 243 |
+
"doctor_name": "Dr. Banerjee",
|
| 244 |
+
"doctor_reg": "WB/34567/2015",
|
| 245 |
+
"diagnosis": "Obesity - BMI 35",
|
| 246 |
+
"treatment": "Bariatric consultation and diet plan"
|
| 247 |
+
},
|
| 248 |
+
"bill": {
|
| 249 |
+
"consultation_fee": 3000,
|
| 250 |
+
"diet_plan": 5000
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
},
|
| 254 |
+
"expected_output": {
|
| 255 |
+
"decision": "REJECTED",
|
| 256 |
+
"rejection_reasons": ["SERVICE_NOT_COVERED"],
|
| 257 |
+
"notes": "Weight loss treatments are excluded from coverage",
|
| 258 |
+
"confidence_score": 0.97
|
| 259 |
+
}
|
| 260 |
+
},
|
| 261 |
+
{
|
| 262 |
+
"case_id": "TC010",
|
| 263 |
+
"case_name": "Network Hospital - Cashless Approved",
|
| 264 |
+
"description": "Treatment at network hospital with instant approval",
|
| 265 |
+
"input_data": {
|
| 266 |
+
"member_id": "EMP010",
|
| 267 |
+
"member_name": "Deepak Shah",
|
| 268 |
+
"treatment_date": "2024-11-03",
|
| 269 |
+
"claim_amount": 4500,
|
| 270 |
+
"hospital": "Apollo Hospitals",
|
| 271 |
+
"cashless_request": true,
|
| 272 |
+
"documents": {
|
| 273 |
+
"prescription": {
|
| 274 |
+
"doctor_name": "Dr. Iyer",
|
| 275 |
+
"doctor_reg": "TN/56789/2013",
|
| 276 |
+
"diagnosis": "Acute bronchitis",
|
| 277 |
+
"medicines_prescribed": ["Antibiotics", "Bronchodilators"]
|
| 278 |
+
},
|
| 279 |
+
"bill": {
|
| 280 |
+
"consultation_fee": 1500,
|
| 281 |
+
"medicines": 3000
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
},
|
| 285 |
+
"expected_output": {
|
| 286 |
+
"decision": "APPROVED",
|
| 287 |
+
"approved_amount": 3600,
|
| 288 |
+
"cashless_approved": true,
|
| 289 |
+
"network_discount": 900,
|
| 290 |
+
"confidence_score": 0.93
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
],
|
| 294 |
+
"validation_notes": {
|
| 295 |
+
"scoring_guidelines": [
|
| 296 |
+
"Each test case should complete within 5 seconds",
|
| 297 |
+
"Confidence scores should be provided for all decisions",
|
| 298 |
+
"System should handle edge cases gracefully",
|
| 299 |
+
"Clear reasoning should be provided for rejections"
|
| 300 |
+
],
|
| 301 |
+
"bonus_test_scenarios": [
|
| 302 |
+
"Handle illegible/blurry document images",
|
| 303 |
+
"Process handwritten prescriptions",
|
| 304 |
+
"Detect forged/modified documents",
|
| 305 |
+
"Handle claims in regional languages",
|
| 306 |
+
"Process multiple receipts in single claim"
|
| 307 |
+
]
|
| 308 |
+
}
|
| 309 |
+
}
|
app/models/__init__.py
ADDED
|
File without changes
|
app/models/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (156 Bytes). View file
|
|
|
app/models/__pycache__/schemas.cpython-313.pyc
ADDED
|
Binary file (3.86 kB). View file
|
|
|
app/models/schemas.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import List, Optional, Literal
|
| 3 |
+
from datetime import date
|
| 4 |
+
|
| 5 |
+
# ---------------------------------------------------------
|
| 6 |
+
# 0. Itemized Claim Item Schema
|
| 7 |
+
# (Individual item evaluation)
|
| 8 |
+
# ---------------------------------------------------------
|
| 9 |
+
class ItemizedClaimData(BaseModel):
|
| 10 |
+
description: str = Field(description="Item description (medicine/procedure/test/consultation)")
|
| 11 |
+
amount: float = Field(description="Amount billed for this item in INR")
|
| 12 |
+
category: str = Field(description="Category: medicine, procedure, diagnostic_test, consultation")
|
| 13 |
+
is_covered: bool = Field(description="True if covered by policy")
|
| 14 |
+
exclusion_reason: str = Field(description="If not covered, why. Otherwise 'None'")
|
| 15 |
+
is_medically_necessary: bool = Field(description="True if clinically justified")
|
| 16 |
+
medical_necessity_reason: str = Field(description="Why it is/isn't necessary")
|
| 17 |
+
|
| 18 |
+
# ---------------------------------------------------------
|
| 19 |
+
# 1. Gemini Extraction Output Schema
|
| 20 |
+
# (We force Gemini to output this exact structure)
|
| 21 |
+
# ---------------------------------------------------------
|
| 22 |
+
class ExtractedClaimData(BaseModel):
|
| 23 |
+
patient_name: str = Field(description="Name of the patient on the document")
|
| 24 |
+
treatment_date: str = Field(description="Date of treatment in YYYY-MM-DD format")
|
| 25 |
+
doctor_name: str = Field(description="Name of the treating doctor")
|
| 26 |
+
doctor_reg_no: str = Field(description="Doctor's medical registration number")
|
| 27 |
+
diagnosis: str = Field(description="Primary diagnosis or reason for visit")
|
| 28 |
+
is_treatment_excluded: bool = Field(default=False, description="True if the treatment matches an exclusion")
|
| 29 |
+
exclusion_reason: str = Field(default="None")
|
| 30 |
+
# Itemized breakdown instead of simple lists
|
| 31 |
+
itemized_claims: List[ItemizedClaimData] = Field(description="Line-by-line breakdown of all billed items with individual coverage assessment")
|
| 32 |
+
requires_pre_auth: bool = Field(default=False, description="True if procedure needs pre-auth")
|
| 33 |
+
missing_pre_auth: bool = Field(default=False, description="True if pre-auth is missing")
|
| 34 |
+
total_billed_amount: float = Field(description="Total amount billed in INR")
|
| 35 |
+
overall_medically_necessary: bool = Field(description="Is the overall treatment medically justified")
|
| 36 |
+
medical_necessity_reasoning: str = Field(description="Overall clinical assessment")
|
| 37 |
+
|
| 38 |
+
# ---------------------------------------------------------
|
| 39 |
+
# 2. API Input Schema
|
| 40 |
+
# (What the frontend/Postman sends to our FastAPI route)
|
| 41 |
+
# ---------------------------------------------------------
|
| 42 |
+
class ClaimSubmission(BaseModel):
|
| 43 |
+
member_id: str
|
| 44 |
+
claim_amount: float
|
| 45 |
+
cashless_request: bool = False
|
| 46 |
+
hospital_name: Optional[str] = None
|
| 47 |
+
# Note: Files (images/pdfs) will be handled via FastAPI UploadFile, not Pydantic
|
| 48 |
+
|
| 49 |
+
# ---------------------------------------------------------
|
| 50 |
+
# 3. Final Adjudication Output Schema
|
| 51 |
+
# (Matches the exact assignment requirement format)
|
| 52 |
+
# ---------------------------------------------------------
|
| 53 |
+
class AdjudicationResponse(BaseModel):
|
| 54 |
+
claim_id: str
|
| 55 |
+
decision: Literal["APPROVED", "REJECTED", "PARTIAL", "MANUAL_REVIEW"]
|
| 56 |
+
approved_amount: float
|
| 57 |
+
rejection_reasons: List[str]
|
| 58 |
+
confidence_score: float
|
| 59 |
+
notes: str
|
| 60 |
+
next_steps: str
|
| 61 |
+
cashless_approved: bool = False
|
| 62 |
+
network_discount: float = 0.0
|
deepseek.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ollama
|
| 2 |
+
|
| 3 |
+
with open("handwriting.png", "rb") as f:
|
| 4 |
+
image_data = f.read()
|
| 5 |
+
|
| 6 |
+
response = ollama.chat(
|
| 7 |
+
model="deepseek-ocr",
|
| 8 |
+
messages=[
|
| 9 |
+
{
|
| 10 |
+
"role": "user",
|
| 11 |
+
"content": "Extract all text from this medical prescription image.",
|
| 12 |
+
"images": [image_data]
|
| 13 |
+
}
|
| 14 |
+
]
|
| 15 |
+
)
|
| 16 |
+
print(response["message"]["content"])
|
download.png
ADDED
|
gemini_vision.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from google import genai
|
| 3 |
+
from google.genai import types
|
| 4 |
+
from PIL import Image
|
| 5 |
+
|
| 6 |
+
def extract_text_gemini(image_path: str) -> str:
|
| 7 |
+
"""
|
| 8 |
+
Sends an image to Gemini Vision and extracts the text/data.
|
| 9 |
+
Requires GEMINI_API_KEY environment variable to be set.
|
| 10 |
+
"""
|
| 11 |
+
# Initialize the client (automatically picks up GEMINI_API_KEY from env)
|
| 12 |
+
client = genai.Client()
|
| 13 |
+
|
| 14 |
+
# Load the image
|
| 15 |
+
image = Image.open(image_path)
|
| 16 |
+
|
| 17 |
+
# Define the prompt
|
| 18 |
+
prompt = "Extract all text, handwriting, and tabular data from this medical document accurately."
|
| 19 |
+
|
| 20 |
+
# Call the model (gemini-2.5-flash is currently recommended for fast, multimodal tasks)
|
| 21 |
+
response = client.models.generate_content(
|
| 22 |
+
model="gemini-2.5-flash",
|
| 23 |
+
contents=[prompt, image]
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
return response.text
|
| 27 |
+
|
| 28 |
+
# Example Usage:
|
| 29 |
+
api_key = os.environ["GEMINI_API_KEY"]
|
| 30 |
+
text_output = extract_text_gemini("handwriting.png")
|
| 31 |
+
print(text_output)
|
google_cloud_vision.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from google.cloud import vision
|
| 3 |
+
|
| 4 |
+
def extract_raw_text(image_path: str) -> str:
|
| 5 |
+
"""
|
| 6 |
+
Sends an image to Google Cloud Vision API and returns the raw extracted text.
|
| 7 |
+
Requires GOOGLE_APPLICATION_CREDENTIALS environment variable to be set.
|
| 8 |
+
"""
|
| 9 |
+
# Initialize the Vision API Client
|
| 10 |
+
client = vision.ImageAnnotatorClient()
|
| 11 |
+
|
| 12 |
+
# Load the image into memory
|
| 13 |
+
with open(image_path, 'rb') as image_file:
|
| 14 |
+
content = image_file.read()
|
| 15 |
+
|
| 16 |
+
image = vision.Image(content=content)
|
| 17 |
+
|
| 18 |
+
# Perform Document Text Detection (optimized for handwriting/dense text)
|
| 19 |
+
response = client.document_text_detection(image=image)
|
| 20 |
+
|
| 21 |
+
# Error handling for the API response
|
| 22 |
+
if response.error.message:
|
| 23 |
+
raise Exception(f"Google Cloud Vision API Error: {response.error.message}")
|
| 24 |
+
|
| 25 |
+
# Extract and return the full text annotation
|
| 26 |
+
full_text = response.full_text_annotation.text
|
| 27 |
+
return full_text
|
| 28 |
+
|
| 29 |
+
# Example Usage:
|
| 30 |
+
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "credentials.json"
|
| 31 |
+
raw_text = extract_raw_text("115.jpg")
|
| 32 |
+
print(raw_text)
|
main.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from contextlib import asynccontextmanager
|
| 2 |
+
from fastapi import FastAPI
|
| 3 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 4 |
+
from app.api import auth_routes
|
| 5 |
+
|
| 6 |
+
from app.core.rag import rag_engine
|
| 7 |
+
from app.api import claims
|
| 8 |
+
|
| 9 |
+
@asynccontextmanager
|
| 10 |
+
async def lifespan(app: FastAPI):
|
| 11 |
+
"""
|
| 12 |
+
Handles startup and shutdown events asynchronously.
|
| 13 |
+
This guarantees our DB and AI memory are ready before the first request arrives.
|
| 14 |
+
"""
|
| 15 |
+
# --- Startup ---
|
| 16 |
+
rag_engine.initialize()
|
| 17 |
+
yield
|
| 18 |
+
# --- Shutdown ---
|
| 19 |
+
|
| 20 |
+
# Initialize the FastAPI application
|
| 21 |
+
app = FastAPI(
|
| 22 |
+
title="Plum OPD Adjudication Engine",
|
| 23 |
+
description="AI-powered automation tool for processing OPD medical claims.",
|
| 24 |
+
version="1.0.0",
|
| 25 |
+
lifespan=lifespan
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Configure Cross-Origin Resource Sharing (CORS)
|
| 29 |
+
# This allows your frontend (e.g., React on port 3000) to talk to this API
|
| 30 |
+
app.add_middleware(
|
| 31 |
+
CORSMiddleware,
|
| 32 |
+
allow_origins=["*"], # In production, replace "*" with your Vercel frontend URL
|
| 33 |
+
allow_credentials=True,
|
| 34 |
+
allow_methods=["*"],
|
| 35 |
+
allow_headers=["*"],
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
# Register our API routes
|
| 39 |
+
app.include_router(claims.router, prefix="/api")
|
| 40 |
+
|
| 41 |
+
app.include_router(auth_routes.router, prefix="/api")
|
| 42 |
+
|
| 43 |
+
@app.get("/")
|
| 44 |
+
async def health_check():
|
| 45 |
+
"""Simple endpoint to verify the server is running."""
|
| 46 |
+
return {
|
| 47 |
+
"status": "online",
|
| 48 |
+
"service": "Plum OPD Adjudication Engine",
|
| 49 |
+
"database": "connected",
|
| 50 |
+
"rag_memory": "loaded" if rag_engine.vector_store else "offline"
|
| 51 |
+
}
|
plum-opd-frontend/.gitignore
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.*
|
| 7 |
+
.yarn/*
|
| 8 |
+
!.yarn/patches
|
| 9 |
+
!.yarn/plugins
|
| 10 |
+
!.yarn/releases
|
| 11 |
+
!.yarn/versions
|
| 12 |
+
|
| 13 |
+
# testing
|
| 14 |
+
/coverage
|
| 15 |
+
|
| 16 |
+
# next.js
|
| 17 |
+
/.next/
|
| 18 |
+
/out/
|
| 19 |
+
|
| 20 |
+
# production
|
| 21 |
+
/build
|
| 22 |
+
|
| 23 |
+
# misc
|
| 24 |
+
.DS_Store
|
| 25 |
+
*.pem
|
| 26 |
+
|
| 27 |
+
# debug
|
| 28 |
+
npm-debug.log*
|
| 29 |
+
yarn-debug.log*
|
| 30 |
+
yarn-error.log*
|
| 31 |
+
.pnpm-debug.log*
|
| 32 |
+
|
| 33 |
+
# env files (can opt-in for committing if needed)
|
| 34 |
+
.env*
|
| 35 |
+
|
| 36 |
+
# vercel
|
| 37 |
+
.vercel
|
| 38 |
+
|
| 39 |
+
# typescript
|
| 40 |
+
*.tsbuildinfo
|
| 41 |
+
next-env.d.ts
|
plum-opd-frontend/AGENTS.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!-- BEGIN:nextjs-agent-rules -->
|
| 2 |
+
# This is NOT the Next.js you know
|
| 3 |
+
|
| 4 |
+
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
| 5 |
+
<!-- END:nextjs-agent-rules -->
|
plum-opd-frontend/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@AGENTS.md
|
plum-opd-frontend/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
| 2 |
+
|
| 3 |
+
## Getting Started
|
| 4 |
+
|
| 5 |
+
First, run the development server:
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
npm run dev
|
| 9 |
+
# or
|
| 10 |
+
yarn dev
|
| 11 |
+
# or
|
| 12 |
+
pnpm dev
|
| 13 |
+
# or
|
| 14 |
+
bun dev
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
| 18 |
+
|
| 19 |
+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
| 20 |
+
|
| 21 |
+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
| 22 |
+
|
| 23 |
+
## Learn More
|
| 24 |
+
|
| 25 |
+
To learn more about Next.js, take a look at the following resources:
|
| 26 |
+
|
| 27 |
+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
| 28 |
+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
| 29 |
+
|
| 30 |
+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
| 31 |
+
|
| 32 |
+
## Deploy on Vercel
|
| 33 |
+
|
| 34 |
+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
| 35 |
+
|
| 36 |
+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
plum-opd-frontend/app/admin/page.tsx
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
import React, { useState, useEffect } from 'react';
|
| 3 |
+
import { useRouter } from 'next/navigation';
|
| 4 |
+
import { Settings, Save, AlertTriangle, CheckCircle, RefreshCw, ArrowLeft, FileJson } from 'lucide-react';
|
| 5 |
+
|
| 6 |
+
export default function AdminDashboard() {
|
| 7 |
+
const router = useRouter();
|
| 8 |
+
|
| 9 |
+
const [policyJson, setPolicyJson] = useState<string>('');
|
| 10 |
+
const [isLoading, setIsLoading] = useState(true);
|
| 11 |
+
const [isSaving, setIsSaving] = useState(false);
|
| 12 |
+
const [status, setStatus] = useState<{ type: 'success' | 'error', message: string } | null>(null);
|
| 13 |
+
|
| 14 |
+
// Fetch the current policy on page load
|
| 15 |
+
useEffect(() => {
|
| 16 |
+
const fetchPolicy = async () => {
|
| 17 |
+
try {
|
| 18 |
+
const response = await fetch("http://127.0.0.1:8000/api/admin/policy");
|
| 19 |
+
if (!response.ok) throw new Error("Failed to connect to backend policy store.");
|
| 20 |
+
|
| 21 |
+
const data = await response.json();
|
| 22 |
+
setPolicyJson(JSON.stringify(data, null, 2));
|
| 23 |
+
} catch (err: any) {
|
| 24 |
+
setStatus({ type: 'error', message: err.message || "Could not load policy data." });
|
| 25 |
+
} finally {
|
| 26 |
+
setIsLoading(false);
|
| 27 |
+
}
|
| 28 |
+
};
|
| 29 |
+
fetchPolicy();
|
| 30 |
+
}, []);
|
| 31 |
+
|
| 32 |
+
const handleSavePolicy = async () => {
|
| 33 |
+
setIsSaving(true);
|
| 34 |
+
setStatus(null);
|
| 35 |
+
|
| 36 |
+
// 1. Validate JSON syntax before sending to the backend
|
| 37 |
+
let parsedJson;
|
| 38 |
+
try {
|
| 39 |
+
parsedJson = JSON.parse(policyJson);
|
| 40 |
+
} catch (e) {
|
| 41 |
+
setStatus({ type: 'error', message: "Invalid JSON format. Please check your syntax." });
|
| 42 |
+
setIsSaving(false);
|
| 43 |
+
return;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// 2. Send the update command to the FastAPI server
|
| 47 |
+
try {
|
| 48 |
+
const response = await fetch("http://127.0.0.1:8000/api/admin/policy/update", {
|
| 49 |
+
method: "POST",
|
| 50 |
+
headers: { "Content-Type": "application/json" },
|
| 51 |
+
body: JSON.stringify({ policy_json: parsedJson })
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
if (!response.ok) throw new Error("Backend rejected the policy update.");
|
| 55 |
+
|
| 56 |
+
const result = await response.json();
|
| 57 |
+
setStatus({ type: 'success', message: "Policy saved successfully. RAG memory has been rebuilt." });
|
| 58 |
+
|
| 59 |
+
// Clear success message after 4 seconds
|
| 60 |
+
setTimeout(() => setStatus(null), 4000);
|
| 61 |
+
} catch (err: any) {
|
| 62 |
+
setStatus({ type: 'error', message: err.message || "Failed to update engine rules." });
|
| 63 |
+
} finally {
|
| 64 |
+
setIsSaving(false);
|
| 65 |
+
}
|
| 66 |
+
};
|
| 67 |
+
|
| 68 |
+
return (
|
| 69 |
+
<div className="min-h-screen bg-slate-900 text-slate-200 antialiased p-6 font-sans">
|
| 70 |
+
<div className="max-w-5xl mx-auto">
|
| 71 |
+
|
| 72 |
+
{/* Navigation and Header */}
|
| 73 |
+
<header className="mb-8 flex items-center justify-between border-b border-slate-800 pb-4">
|
| 74 |
+
<div className="flex items-center gap-4">
|
| 75 |
+
<button
|
| 76 |
+
onClick={() => router.push('/')}
|
| 77 |
+
className="p-2 hover:bg-slate-800 rounded-lg text-slate-400 hover:text-white transition-colors"
|
| 78 |
+
title="Return to Portal"
|
| 79 |
+
>
|
| 80 |
+
<ArrowLeft size={20} />
|
| 81 |
+
</button>
|
| 82 |
+
<div>
|
| 83 |
+
<h1 className="text-2xl font-bold tracking-tight text-white flex items-center gap-2">
|
| 84 |
+
<Settings className="text-emerald-400" size={24} /> Configuration Control
|
| 85 |
+
</h1>
|
| 86 |
+
<p className="text-sm text-slate-400 mt-1">Direct memory manipulation for the RAG evaluation engine.</p>
|
| 87 |
+
</div>
|
| 88 |
+
</div>
|
| 89 |
+
|
| 90 |
+
<button
|
| 91 |
+
onClick={handleSavePolicy}
|
| 92 |
+
disabled={isSaving || isLoading}
|
| 93 |
+
className="bg-emerald-600 hover:bg-emerald-500 text-white px-5 py-2.5 rounded-lg text-sm font-medium transition-colors flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
| 94 |
+
>
|
| 95 |
+
{isSaving ? <RefreshCw className="animate-spin" size={16} /> : <Save size={16} />}
|
| 96 |
+
Commit to AI Memory
|
| 97 |
+
</button>
|
| 98 |
+
</header>
|
| 99 |
+
|
| 100 |
+
{/* Status Alerts */}
|
| 101 |
+
{status && (
|
| 102 |
+
<div className={`mb-6 p-4 rounded-xl flex items-start gap-3 border ${status.type === 'success' ? 'bg-emerald-900/30 border-emerald-800 text-emerald-300' : 'bg-rose-900/30 border-rose-800 text-rose-300'}`}>
|
| 103 |
+
{status.type === 'success' ? <CheckCircle size={20} className="mt-0.5 shrink-0" /> : <AlertTriangle size={20} className="mt-0.5 shrink-0" />}
|
| 104 |
+
<p className="text-sm font-medium">{status.message}</p>
|
| 105 |
+
</div>
|
| 106 |
+
)}
|
| 107 |
+
|
| 108 |
+
{/* Code Editor */}
|
| 109 |
+
<div className="bg-slate-950 border border-slate-800 rounded-xl overflow-hidden shadow-2xl flex flex-col h-[65vh]">
|
| 110 |
+
<div className="bg-slate-900 px-4 py-2 border-b border-slate-800 flex items-center gap-2">
|
| 111 |
+
<FileJson size={16} className="text-slate-500" />
|
| 112 |
+
<span className="text-xs font-mono text-slate-400 tracking-wider">policy_dataset.json</span>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
{isLoading ? (
|
| 116 |
+
<div className="flex-1 flex flex-col items-center justify-center text-slate-500">
|
| 117 |
+
<RefreshCw className="animate-spin mb-3" size={24} />
|
| 118 |
+
<p className="text-sm font-medium">Extracting rules from vector database...</p>
|
| 119 |
+
</div>
|
| 120 |
+
) : (
|
| 121 |
+
<textarea
|
| 122 |
+
value={policyJson}
|
| 123 |
+
onChange={(e) => setPolicyJson(e.target.value)}
|
| 124 |
+
spellCheck={false}
|
| 125 |
+
className="flex-1 bg-transparent text-emerald-300 font-mono text-sm p-6 focus:outline-none resize-none leading-relaxed"
|
| 126 |
+
placeholder="Paste or edit policy JSON here..."
|
| 127 |
+
/>
|
| 128 |
+
)}
|
| 129 |
+
</div>
|
| 130 |
+
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
);
|
| 134 |
+
}
|
plum-opd-frontend/app/favicon.ico
ADDED
|
|
plum-opd-frontend/app/globals.css
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--background: #ffffff;
|
| 5 |
+
--foreground: #171717;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
@theme inline {
|
| 9 |
+
--color-background: var(--background);
|
| 10 |
+
--color-foreground: var(--foreground);
|
| 11 |
+
--font-sans: var(--font-geist-sans);
|
| 12 |
+
--font-mono: var(--font-geist-mono);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
@media (prefers-color-scheme: dark) {
|
| 16 |
+
:root {
|
| 17 |
+
--background: #0a0a0a;
|
| 18 |
+
--foreground: #ededed;
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
body {
|
| 23 |
+
background: var(--background);
|
| 24 |
+
color: var(--foreground);
|
| 25 |
+
font-family: Arial, Helvetica, sans-serif;
|
| 26 |
+
}
|
plum-opd-frontend/app/layout.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import { Geist, Geist_Mono } from "next/font/google";
|
| 3 |
+
import "./globals.css";
|
| 4 |
+
|
| 5 |
+
const geistSans = Geist({
|
| 6 |
+
variable: "--font-geist-sans",
|
| 7 |
+
subsets: ["latin"],
|
| 8 |
+
});
|
| 9 |
+
|
| 10 |
+
const geistMono = Geist_Mono({
|
| 11 |
+
variable: "--font-geist-mono",
|
| 12 |
+
subsets: ["latin"],
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
export const metadata: Metadata = {
|
| 16 |
+
title: "PLUM App",
|
| 17 |
+
description: "Generated by create next app",
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
export default function RootLayout({
|
| 21 |
+
children,
|
| 22 |
+
}: Readonly<{
|
| 23 |
+
children: React.ReactNode;
|
| 24 |
+
}>) {
|
| 25 |
+
return (
|
| 26 |
+
<html
|
| 27 |
+
lang="en"
|
| 28 |
+
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
| 29 |
+
>
|
| 30 |
+
<body className="min-h-full flex flex-col">{children}</body>
|
| 31 |
+
</html>
|
| 32 |
+
);
|
| 33 |
+
}
|
plum-opd-frontend/app/page.tsx
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
import React from 'react';
|
| 3 |
+
import { useRouter } from 'next/navigation';
|
| 4 |
+
import { ShieldCheck, User, Settings, ArrowRight } from 'lucide-react';
|
| 5 |
+
|
| 6 |
+
export default function LoginPortal() {
|
| 7 |
+
const router = useRouter();
|
| 8 |
+
|
| 9 |
+
return (
|
| 10 |
+
<div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center p-6 font-sans">
|
| 11 |
+
|
| 12 |
+
{/* Branding Header */}
|
| 13 |
+
<div className="text-center mb-12">
|
| 14 |
+
<div className="bg-emerald-600 p-3 rounded-2xl text-white inline-block mb-4 shadow-lg shadow-emerald-600/20">
|
| 15 |
+
<ShieldCheck size={40} />
|
| 16 |
+
</div>
|
| 17 |
+
<h1 className="text-3xl font-bold tracking-tight text-slate-900">Plum Claims Engine</h1>
|
| 18 |
+
<p className="text-slate-500 mt-2 font-medium">Select your portal access level</p>
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
{/* Login Options Grid */}
|
| 22 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-3xl">
|
| 23 |
+
|
| 24 |
+
{/* Employee Login Card */}
|
| 25 |
+
<button
|
| 26 |
+
onClick={() => router.push('/user')}
|
| 27 |
+
className="group relative bg-white p-8 rounded-2xl border border-slate-200 shadow-sm hover:shadow-md hover:border-emerald-500 transition-all duration-300 text-left flex flex-col h-full overflow-hidden"
|
| 28 |
+
>
|
| 29 |
+
<div className="absolute top-0 right-0 w-32 h-32 bg-emerald-50 rounded-bl-full -mr-8 -mt-8 transition-transform group-hover:scale-110" />
|
| 30 |
+
<div className="relative z-10 flex-grow">
|
| 31 |
+
<div className="bg-emerald-100 p-3 rounded-lg text-emerald-700 w-fit mb-6">
|
| 32 |
+
<User size={24} />
|
| 33 |
+
</div>
|
| 34 |
+
<h2 className="text-xl font-bold text-slate-900 mb-2">Employee Portal</h2>
|
| 35 |
+
<p className="text-sm text-slate-500 leading-relaxed mb-8">
|
| 36 |
+
Submit new OPD medical claims, upload prescriptions, and view your real-time AI adjudication status and wallet limits.
|
| 37 |
+
</p>
|
| 38 |
+
</div>
|
| 39 |
+
<div className="relative z-10 flex items-center text-sm font-bold text-emerald-600 uppercase tracking-wider group-hover:translate-x-2 transition-transform">
|
| 40 |
+
Access Dashboard <ArrowRight size={16} className="ml-2" />
|
| 41 |
+
</div>
|
| 42 |
+
</button>
|
| 43 |
+
|
| 44 |
+
{/* Administrator Login Card */}
|
| 45 |
+
<button
|
| 46 |
+
onClick={() => router.push('/admin')}
|
| 47 |
+
className="group relative bg-slate-900 p-8 rounded-2xl border border-slate-800 shadow-lg hover:shadow-xl hover:border-slate-600 transition-all duration-300 text-left flex flex-col h-full overflow-hidden"
|
| 48 |
+
>
|
| 49 |
+
<div className="absolute top-0 right-0 w-32 h-32 bg-slate-800 rounded-bl-full -mr-8 -mt-8 transition-transform group-hover:scale-110" />
|
| 50 |
+
<div className="relative z-10 flex-grow">
|
| 51 |
+
<div className="bg-slate-800 p-3 rounded-lg text-emerald-400 w-fit mb-6">
|
| 52 |
+
<Settings size={24} />
|
| 53 |
+
</div>
|
| 54 |
+
<h2 className="text-xl font-bold text-white mb-2">Admin Control Center</h2>
|
| 55 |
+
<p className="text-sm text-slate-400 leading-relaxed mb-8">
|
| 56 |
+
Manage global policy terms, update financial limits, and instantly rewrite the AI engine's clinical memory rules.
|
| 57 |
+
</p>
|
| 58 |
+
</div>
|
| 59 |
+
<div className="relative z-10 flex items-center text-sm font-bold text-emerald-400 uppercase tracking-wider group-hover:translate-x-2 transition-transform">
|
| 60 |
+
Manage Policies <ArrowRight size={16} className="ml-2" />
|
| 61 |
+
</div>
|
| 62 |
+
</button>
|
| 63 |
+
|
| 64 |
+
</div>
|
| 65 |
+
|
| 66 |
+
{/* Footer Note */}
|
| 67 |
+
<div className="mt-12 text-xs font-medium text-slate-400 tracking-wider uppercase text-center">
|
| 68 |
+
Plum Internship Assignment • Single-Pass Architecture
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
);
|
| 72 |
+
}
|
plum-opd-frontend/app/user/page.tsx
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
import React, { useState } from 'react';
|
| 3 |
+
import { Upload, ShieldCheck, Activity, AlertTriangle, CheckCircle, RefreshCw, FileText, IndianRupee } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
export default function ClaimsDashboard() {
|
| 6 |
+
// Form State
|
| 7 |
+
const [memberId, setMemberId] = useState('EMP001');
|
| 8 |
+
const [claimAmount, setClaimAmount] = useState('1500');
|
| 9 |
+
const [cashlessRequest, setCashlessRequest] = useState(false);
|
| 10 |
+
const [hospitalName, setHospitalName] = useState('Apollo Hospitals');
|
| 11 |
+
const [selectedFile, setSelectedFile] = useState<File[]>([]);
|
| 12 |
+
|
| 13 |
+
// Workflow UX States
|
| 14 |
+
const [isProcessing, setIsProcessing] = useState(false);
|
| 15 |
+
const [currentStep, setCurrentStep] = useState(0);
|
| 16 |
+
const [result, setResult] = useState<any>(null);
|
| 17 |
+
const [error, setError] = useState<string | null>(null);
|
| 18 |
+
|
| 19 |
+
const workflowSteps = [
|
| 20 |
+
"Scanning medical document & layout matrices...",
|
| 21 |
+
"Querying local LangChain FAISS policy context stores...",
|
| 22 |
+
"Orchestrating multi-modal Gemini 2.5 Flash validation...",
|
| 23 |
+
"Applying mathematical policy caps and co-pays..."
|
| 24 |
+
];
|
| 25 |
+
|
| 26 |
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 27 |
+
if (e.target.files) {
|
| 28 |
+
// Convert the FileList object to a standard JavaScript Array
|
| 29 |
+
setSelectedFile(Array.from(e.target.files));
|
| 30 |
+
}
|
| 31 |
+
};
|
| 32 |
+
const executeAdjudication = async (e: React.FormEvent) => {
|
| 33 |
+
e.preventDefault();
|
| 34 |
+
if (!selectedFile) {
|
| 35 |
+
setError("Please upload a prescription or bill document to proceed.");
|
| 36 |
+
return;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
setIsProcessing(true);
|
| 40 |
+
setCurrentStep(0);
|
| 41 |
+
setError(null);
|
| 42 |
+
setResult(null);
|
| 43 |
+
|
| 44 |
+
// Animate the fake stepped loader steps every 900ms to keep user engaged
|
| 45 |
+
const stepInterval = setInterval(() => {
|
| 46 |
+
setCurrentStep((prev) => {
|
| 47 |
+
if (prev < workflowSteps.length - 1) return prev + 1;
|
| 48 |
+
clearInterval(stepInterval);
|
| 49 |
+
return prev;
|
| 50 |
+
});
|
| 51 |
+
}, 900);
|
| 52 |
+
|
| 53 |
+
try {
|
| 54 |
+
// Build the Multi-part form boundary payload
|
| 55 |
+
const formData = new FormData();
|
| 56 |
+
formData.append("member_id", memberId);
|
| 57 |
+
formData.append("claim_amount", parseFloat(claimAmount).toFixed(2));
|
| 58 |
+
formData.append("cashless_request", cashlessRequest.toString());
|
| 59 |
+
formData.append("hospital_name", hospitalName);
|
| 60 |
+
// Loop through the array and append each file under the "documents" key
|
| 61 |
+
selectedFile.forEach((file) => {
|
| 62 |
+
formData.append("documents", file);
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
const response = await fetch("http://127.0.0.1:8000/api/claims/adjudicate", {
|
| 66 |
+
method: "POST",
|
| 67 |
+
body: formData,
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
clearInterval(stepInterval);
|
| 71 |
+
|
| 72 |
+
if (!response.ok) {
|
| 73 |
+
const errorData = await response.json();
|
| 74 |
+
throw new Error(errorData.detail || "Server failed to process claim rules.");
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
const data = await response.json();
|
| 78 |
+
setResult(data);
|
| 79 |
+
} catch (err: any) {
|
| 80 |
+
clearInterval(stepInterval);
|
| 81 |
+
setError(err.message || "Unable to establish communication with the adjudication backend.");
|
| 82 |
+
} finally {
|
| 83 |
+
setIsProcessing(false);
|
| 84 |
+
}
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
return (
|
| 88 |
+
<div className="min-h-screen bg-slate-50 text-slate-800 antialiased p-6 font-sans">
|
| 89 |
+
<header className="max-w-6xl mx-auto mb-8 border-b border-slate-200 pb-4">
|
| 90 |
+
<div className="flex items-center gap-3">
|
| 91 |
+
<div className="bg-emerald-600 p-2 rounded-lg text-white">
|
| 92 |
+
<ShieldCheck size={28} />
|
| 93 |
+
</div>
|
| 94 |
+
<div>
|
| 95 |
+
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Plum OPD Advantage</h1>
|
| 96 |
+
<p className="text-sm text-slate-500">Autonomous AI Claim Adjudication Engine</p>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
</header>
|
| 100 |
+
|
| 101 |
+
<main className="max-w-6xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-8 items-start">
|
| 102 |
+
{/* Form Entry Column */}
|
| 103 |
+
<section className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
|
| 104 |
+
<h2 className="text-lg font-semibold text-slate-900 mb-4 flex items-center gap-2">
|
| 105 |
+
<FileText size={20} className="text-emerald-600" /> New Claim Submission
|
| 106 |
+
</h2>
|
| 107 |
+
|
| 108 |
+
<form onSubmit={executeAdjudication} className="space-y-4">
|
| 109 |
+
<div>
|
| 110 |
+
<label className="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1">Member Reference ID</label>
|
| 111 |
+
<input type="text" value={memberId} onChange={(e) => setMemberId(e.target.value)} required className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:border-emerald-500" />
|
| 112 |
+
</div>
|
| 113 |
+
|
| 114 |
+
<div className="grid grid-cols-2 gap-4">
|
| 115 |
+
<div>
|
| 116 |
+
<label className="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1">Claimed Amount (INR)</label>
|
| 117 |
+
<input type="number" value={claimAmount} onChange={(e) => setClaimAmount(e.target.value)} required className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:border-emerald-500" />
|
| 118 |
+
</div>
|
| 119 |
+
<div>
|
| 120 |
+
<label className="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1">Hospital / Clinic</label>
|
| 121 |
+
<input type="text" value={hospitalName} onChange={(e) => setHospitalName(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:border-emerald-500" />
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
<div className="flex items-center gap-2 py-2">
|
| 126 |
+
<input type="checkbox" id="cashless" checked={cashlessRequest} onChange={(e) => setCashlessRequest(e.target.checked)} className="h-4 w-4 rounded text-emerald-600 border-slate-300 focus:ring-emerald-500" />
|
| 127 |
+
<label htmlFor="cashless" className="text-sm font-medium text-slate-700 selection:bg-transparent cursor-pointer">Request Cashless Settlement</label>
|
| 128 |
+
</div>
|
| 129 |
+
|
| 130 |
+
<div>
|
| 131 |
+
<label className="block text-xs font-semibold uppercase tracking-wider text-slate-500 mb-1">Medical Attachment</label>
|
| 132 |
+
<div className="border-2 border-dashed border-slate-300 rounded-xl p-6 text-center hover:border-emerald-500 transition-colors relative bg-slate-50">
|
| 133 |
+
<input type="file" multiple accept="image/*,application/pdf" onChange={handleFileChange} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
|
| 134 |
+
<Upload className="mx-auto text-slate-400 mb-2" size={32} />
|
| 135 |
+
<span className="block text-sm font-medium text-slate-700">{selectedFile.length > 0? `${selectedFile.length} document(s) attached`: "Upload or drop multiple receipts & prescriptions"}</span>
|
| 136 |
+
<span className="text-xs text-slate-400 mt-1 block">Supports JPEG, PNG, or PDF files</span>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
|
| 140 |
+
<button type="submit" disabled={isProcessing} className="w-full bg-slate-900 hover:bg-slate-800 text-white font-medium py-2.5 px-4 rounded-lg text-sm transition-colors flex items-center justify-center gap-2 disabled:bg-slate-400">
|
| 141 |
+
{isProcessing ? <RefreshCw className="animate-spin" size={16} /> : <Activity size={16} />}
|
| 142 |
+
{isProcessing ? "Processing Rule Engine..." : "Analyze and Adjudicate"}
|
| 143 |
+
</button>
|
| 144 |
+
</form>
|
| 145 |
+
</section>
|
| 146 |
+
|
| 147 |
+
{/* Results Processing Column */}
|
| 148 |
+
<section className="space-y-6">
|
| 149 |
+
{/* Active Process Stepper Card */}
|
| 150 |
+
{isProcessing && (
|
| 151 |
+
<div className="bg-slate-900 text-white p-6 rounded-xl shadow-lg border border-slate-800 animate-pulse">
|
| 152 |
+
<h3 className="text-sm font-semibold tracking-wider uppercase text-emerald-400 mb-4 flex items-center gap-2">
|
| 153 |
+
<RefreshCw className="animate-spin" size={16} /> AI Adjudication Execution Core
|
| 154 |
+
</h3>
|
| 155 |
+
<div className="space-y-3">
|
| 156 |
+
{workflowSteps.map((step, idx) => (
|
| 157 |
+
<div key={idx} className={`flex items-center gap-3 text-sm transition-opacity duration-300 ${idx === currentStep ? 'opacity-100 font-medium text-white' : idx < currentStep ? 'opacity-50 text-emerald-300' : 'opacity-20'}`}>
|
| 158 |
+
<div className={`h-2 w-2 rounded-full ${idx <= currentStep ? 'bg-emerald-400' : 'bg-slate-700'}`} />
|
| 159 |
+
{step}
|
| 160 |
+
</div>
|
| 161 |
+
))}
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
)}
|
| 165 |
+
|
| 166 |
+
{/* Error Message Layout */}
|
| 167 |
+
{error && (
|
| 168 |
+
<div className="bg-rose-50 border border-rose-200 text-rose-900 p-4 rounded-xl flex items-start gap-3">
|
| 169 |
+
<AlertTriangle className="text-rose-600 shrink-0 mt-0.5" size={20} />
|
| 170 |
+
<div>
|
| 171 |
+
<h4 className="font-semibold text-sm">System Pipeline Exception</h4>
|
| 172 |
+
<p className="text-xs text-rose-700 mt-0.5 whitespace-pre-wrap">{error}</p>
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
)}
|
| 176 |
+
|
| 177 |
+
{/* Adjudication Results Breakdown Card */}
|
| 178 |
+
{result && (
|
| 179 |
+
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden animate-fadeIn">
|
| 180 |
+
<div className={`px-6 py-4 flex items-center justify-between border-b ${result.decision === 'APPROVED' ? 'bg-emerald-50 border-emerald-100' : result.decision === 'PARTIAL' ? 'bg-amber-50 border-amber-100' : 'bg-rose-50 border-rose-100'}`}>
|
| 181 |
+
<div className="flex items-center gap-2">
|
| 182 |
+
{result.decision === 'APPROVED' ? <CheckCircle className="text-emerald-600" /> : <AlertTriangle className={result.decision === 'PARTIAL' ? "text-amber-600" : "text-rose-600"} />}
|
| 183 |
+
<span className="text-xs font-semibold tracking-wider uppercase text-slate-500">Decision Outcome</span>
|
| 184 |
+
</div>
|
| 185 |
+
<span className={`text-xs font-bold tracking-widest uppercase px-2.5 py-1 rounded-full border ${result.decision === 'APPROVED' ? 'bg-emerald-200/50 text-emerald-800 border-emerald-300' : result.decision === 'PARTIAL' ? 'bg-amber-200/50 text-amber-800 border-amber-300' : 'bg-rose-200/50 text-rose-800 border-rose-300'}`}>
|
| 186 |
+
{result.decision}
|
| 187 |
+
</span>
|
| 188 |
+
</div>
|
| 189 |
+
|
| 190 |
+
<div className="p-6 space-y-5">
|
| 191 |
+
<div className="grid grid-cols-2 gap-4 border-b border-slate-100 pb-4">
|
| 192 |
+
<div>
|
| 193 |
+
<span className="block text-xs font-semibold tracking-wider uppercase text-slate-400">Approved Wallet Balance</span>
|
| 194 |
+
<span className="text-2xl font-bold text-slate-900 flex items-center"><IndianRupee size={20} className="text-slate-400" />{result.approved_amount.toLocaleString('en-IN')}</span>
|
| 195 |
+
</div>
|
| 196 |
+
<div>
|
| 197 |
+
<span className="block text-xs font-semibold tracking-wider uppercase text-slate-400">AI Confidence Score</span>
|
| 198 |
+
<span className="text-2xl font-bold text-slate-900">{(result.confidence_score * 100).toFixed(0)}%</span>
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
|
| 202 |
+
{result.rejection_reasons && result.rejection_reasons.length > 0 && (
|
| 203 |
+
<div>
|
| 204 |
+
<span className="block text-xs font-semibold tracking-wider uppercase text-slate-400 mb-1.5">Rule Violation Ledger</span>
|
| 205 |
+
<ul className="space-y-1.5">
|
| 206 |
+
{result.rejection_reasons.map((reason: string, i: number) => (
|
| 207 |
+
<li key={i} className="text-xs bg-rose-50 border border-rose-100 rounded-lg p-2.5 text-rose-900 font-medium tracking-tight">
|
| 208 |
+
{reason}
|
| 209 |
+
</li>
|
| 210 |
+
))}
|
| 211 |
+
</ul>
|
| 212 |
+
</div>
|
| 213 |
+
)}
|
| 214 |
+
|
| 215 |
+
<div>
|
| 216 |
+
<span className="block text-xs font-semibold tracking-wider uppercase text-slate-400 mb-0.5">Engine Metadata Observations</span>
|
| 217 |
+
<p className="text-sm text-slate-700 bg-slate-50 rounded-lg p-3 border border-slate-150 leading-relaxed">{result.notes}</p>
|
| 218 |
+
</div>
|
| 219 |
+
|
| 220 |
+
<div className="border-t border-slate-100 pt-4">
|
| 221 |
+
<span className="block text-xs font-semibold tracking-wider uppercase text-slate-400 mb-0.5">Recommended Claimant Actions</span>
|
| 222 |
+
<p className="text-xs text-slate-500 leading-relaxed font-medium">{result.next_steps}</p>
|
| 223 |
+
</div>
|
| 224 |
+
|
| 225 |
+
<div className="text-right text-[10px] text-slate-400 font-mono tracking-wider pt-2">
|
| 226 |
+
ID: {result.claim_id}
|
| 227 |
+
</div>
|
| 228 |
+
</div>
|
| 229 |
+
</div>
|
| 230 |
+
)}
|
| 231 |
+
|
| 232 |
+
{/* Default Neutral Waiting Slate */}
|
| 233 |
+
{!isProcessing && !result && !error && (
|
| 234 |
+
<div className="h-64 border-2 border-dashed border-slate-200 rounded-xl flex flex-col items-center justify-center p-6 text-center bg-white">
|
| 235 |
+
<Activity size={36} className="text-slate-300 mb-2 animate-pulse" />
|
| 236 |
+
<h3 className="text-sm font-semibold text-slate-700">Awaiting Adjudication Flow</h3>
|
| 237 |
+
<p className="text-xs text-slate-400 max-w-xs mt-1">Submit a medical claim on the left to initialize the single-pass AI review pipeline.</p>
|
| 238 |
+
</div>
|
| 239 |
+
)}
|
| 240 |
+
</section>
|
| 241 |
+
</main>
|
| 242 |
+
</div>
|
| 243 |
+
);
|
| 244 |
+
}
|
plum-opd-frontend/eslint.config.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig, globalIgnores } from "eslint/config";
|
| 2 |
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
| 3 |
+
import nextTs from "eslint-config-next/typescript";
|
| 4 |
+
|
| 5 |
+
const eslintConfig = defineConfig([
|
| 6 |
+
...nextVitals,
|
| 7 |
+
...nextTs,
|
| 8 |
+
// Override default ignores of eslint-config-next.
|
| 9 |
+
globalIgnores([
|
| 10 |
+
// Default ignores of eslint-config-next:
|
| 11 |
+
".next/**",
|
| 12 |
+
"out/**",
|
| 13 |
+
"build/**",
|
| 14 |
+
"next-env.d.ts",
|
| 15 |
+
]),
|
| 16 |
+
]);
|
| 17 |
+
|
| 18 |
+
export default eslintConfig;
|
plum-opd-frontend/next.config.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { NextConfig } from "next";
|
| 2 |
+
|
| 3 |
+
const nextConfig: NextConfig = {
|
| 4 |
+
/* config options here */
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default nextConfig;
|
plum-opd-frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
plum-opd-frontend/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "plum-opd-frontend",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "next dev",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"start": "next start",
|
| 9 |
+
"lint": "eslint"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"lucide-react": "^1.17.0",
|
| 13 |
+
"next": "16.2.7",
|
| 14 |
+
"react": "19.2.4",
|
| 15 |
+
"react-dom": "19.2.4"
|
| 16 |
+
},
|
| 17 |
+
"devDependencies": {
|
| 18 |
+
"@tailwindcss/postcss": "^4",
|
| 19 |
+
"@types/node": "^20",
|
| 20 |
+
"@types/react": "^19",
|
| 21 |
+
"@types/react-dom": "^19",
|
| 22 |
+
"eslint": "^9",
|
| 23 |
+
"eslint-config-next": "16.2.7",
|
| 24 |
+
"tailwindcss": "^4",
|
| 25 |
+
"typescript": "^5"
|
| 26 |
+
}
|
| 27 |
+
}
|
plum-opd-frontend/postcss.config.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const config = {
|
| 2 |
+
plugins: {
|
| 3 |
+
"@tailwindcss/postcss": {},
|
| 4 |
+
},
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default config;
|
plum-opd-frontend/public/file.svg
ADDED
|
|