Battlecon commited on
Commit
05cb41b
·
0 Parent(s):

Initial clean deployment commit

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +4 -0
  2. Dockerfile +7 -0
  3. README.md +87 -0
  4. __pycache__/main.cpython-313.pyc +0 -0
  5. app/__init__.py +0 -0
  6. app/__pycache__/__init__.cpython-313.pyc +0 -0
  7. app/api/__init__.py +0 -0
  8. app/api/__pycache__/__init__.cpython-313.pyc +0 -0
  9. app/api/__pycache__/auth_routes.cpython-313.pyc +0 -0
  10. app/api/__pycache__/claims.cpython-313.pyc +0 -0
  11. app/api/auth_routes.py +41 -0
  12. app/api/claims.py +214 -0
  13. app/core/__init__.py +0 -0
  14. app/core/__pycache__/__init__.cpython-313.pyc +0 -0
  15. app/core/__pycache__/config.cpython-313.pyc +0 -0
  16. app/core/__pycache__/database.cpython-313.pyc +0 -0
  17. app/core/__pycache__/llm.cpython-313.pyc +0 -0
  18. app/core/__pycache__/rag.cpython-313.pyc +0 -0
  19. app/core/config.py +16 -0
  20. app/core/database.py +34 -0
  21. app/core/llm.py +127 -0
  22. app/core/rag.py +76 -0
  23. app/data/adjudication_rules.md +138 -0
  24. app/data/policy_terms.json +118 -0
  25. app/data/test_cases.json +309 -0
  26. app/models/__init__.py +0 -0
  27. app/models/__pycache__/__init__.cpython-313.pyc +0 -0
  28. app/models/__pycache__/schemas.cpython-313.pyc +0 -0
  29. app/models/schemas.py +62 -0
  30. deepseek.py +16 -0
  31. download.png +0 -0
  32. gemini_vision.py +31 -0
  33. google_cloud_vision.py +32 -0
  34. main.py +51 -0
  35. plum-opd-frontend/.gitignore +41 -0
  36. plum-opd-frontend/AGENTS.md +5 -0
  37. plum-opd-frontend/CLAUDE.md +1 -0
  38. plum-opd-frontend/README.md +36 -0
  39. plum-opd-frontend/app/admin/page.tsx +134 -0
  40. plum-opd-frontend/app/favicon.ico +0 -0
  41. plum-opd-frontend/app/globals.css +26 -0
  42. plum-opd-frontend/app/layout.tsx +33 -0
  43. plum-opd-frontend/app/page.tsx +72 -0
  44. plum-opd-frontend/app/user/page.tsx +244 -0
  45. plum-opd-frontend/eslint.config.mjs +18 -0
  46. plum-opd-frontend/next.config.ts +7 -0
  47. plum-opd-frontend/package-lock.json +0 -0
  48. plum-opd-frontend/package.json +27 -0
  49. plum-opd-frontend/postcss.config.mjs +7 -0
  50. 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