diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..01e630219b9045fab4076194f98a2ea3c75ace59 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +105.jpg +handwriting.png +105.jpg +handwriting.png diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..13040a223142b9723d46bffce34917eb7bf934bb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.11-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +# Run Uvicorn on the port HF expects (7860) +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d8da8daad3c18bfac78c755cc15bae1a96a27727 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# Plum AI Automation Engineer \- Intern Assignment Package + +## πŸ“‹ Overview + +Welcome\! This package contains everything you need to complete the OPD Claim Adjudication Tool assignment for the AI Automation Engineer intern position at Plum. + +## πŸ“ Package Contents + +assignment\_package/ + +β”‚ + +β”œβ”€β”€ README.md \# This file + +β”œβ”€β”€ plum\_intern\_assignment.md \# Main assignment document with requirements + +β”œβ”€β”€ policy\_terms.json \# Insurance policy configuration + +β”œβ”€β”€ adjudication\_rules.md \# Business logic for claim decisions + +β”œβ”€β”€ test\_cases.json \# Test scenarios with expected outputs + +└── sample\_documents\_guide.md \# Guide for creating test documents + +## 🎯 Your Mission + +Build an AI-powered web application that automates the adjudication (approval/rejection) of OPD insurance claims by: + +1. Processing medical documents (bills, prescriptions) +2. Extracting relevant information using AI/LLMs +3. Validating against policy terms +4. Making intelligent approval/rejection decisions + +## πŸš€ Getting Started + +### Step 1: Read the Assignment + +Start with `plum_intern_assignment.md` to understand the full requirements and evaluation criteria. + +### Step 2: Understand the Business Logic + +- Review `policy_terms.json` to understand coverage limits and exclusions +- Study `adjudication_rules.md` to learn the decision-making process +- Examine `test_cases.json` to see expected behavior + +### Step 3: Set Up Your Development Environment + +\# Clone this assignment package + +\# Set up your preferred tech stack (React/Next.js \+ Node/Python) + +\# Get API keys for LLM services (OpenAI, Claude, or open-source) + +### Step 4: Create Test Documents + +Use `sample_documents_guide.md` to understand medical document formats and create mock documents for testing. + +### Step 5: Build Your Solution + +Focus on: + +- Document upload and processing +- AI-powered data extraction +- Rule engine implementation +- Clean, intuitive UI +- Comprehensive testing + +## πŸ’‘ Pro Tips + +1. **Start Simple**: Build a basic working version first, then add advanced features +2. **Use AI Tools**: We encourage using Cursor, Copilot, or other AI coding assistants +3. **Document Everything**: Clear documentation shows your thinking process +4. **Test Thoroughly**: Use all provided test cases and create additional ones +5. **Ask Early**: If something is unclear, ask within the first 24 hours + +## πŸ“Š Evaluation Focus Areas + +- **Core Functionality** (40%): Does it work correctly? +- **AI Integration** (25%): How effectively do you use LLMs? +- **Code Quality** (20%): Is the code clean and maintainable? +- **User Experience** (15%): Is it easy to use? + +## ⏰ Timeline + +- **Total Duration**: 2-3 days from receipt + +# intern-assignment diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78209ebd2a3af1cb4a69c425261b7d9142439bc7 Binary files /dev/null and b/__pycache__/main.cpython-313.pyc differ diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/__pycache__/__init__.cpython-313.pyc b/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6fc0f74ece1d3abfe70b1bd31ac53502cd025b05 Binary files /dev/null and b/app/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/api/__pycache__/__init__.cpython-313.pyc b/app/api/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..510408df1bc462221c5acec29b98ed1ee439b622 Binary files /dev/null and b/app/api/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/api/__pycache__/auth_routes.cpython-313.pyc b/app/api/__pycache__/auth_routes.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f038a25a7e335f129ecd3b32fa2423fd230df187 Binary files /dev/null and b/app/api/__pycache__/auth_routes.cpython-313.pyc differ diff --git a/app/api/__pycache__/claims.cpython-313.pyc b/app/api/__pycache__/claims.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..daf86a9a1278be1984d74f5aa87b1f827a3d368f Binary files /dev/null and b/app/api/__pycache__/claims.cpython-313.pyc differ diff --git a/app/api/auth_routes.py b/app/api/auth_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..38cfbc895f0733c7b10f243bfbe72972a850e63a --- /dev/null +++ b/app/api/auth_routes.py @@ -0,0 +1,41 @@ +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +import json +import os +from app.core.rag import rag_engine + +router = APIRouter(tags=["Portal Controls"]) + +class PolicyUpdatePayload(BaseModel): + policy_json: dict + +# --- User Routes --- +@router.get("/user/{member_id}/wallet") +async def get_wallet_limit(member_id: str): + """Mocks database retrieval of a user's wallet limit.""" + return { + "member_id": member_id, + "total_limit": 50000.00, + "available_limit": 35000.00 # Example remaining balance + } + +# --- Admin Routes --- +@router.get("/admin/policy") +async def get_current_policy(): + """Fetches the current policy JSON for the admin editor.""" + if not os.path.exists(rag_engine.policy_file): + raise HTTPException(status_code=404, detail="Policy file not found.") + + with open(rag_engine.policy_file, "r") as f: + return json.load(f) + +@router.post("/admin/policy/update") +async def update_policy(payload: PolicyUpdatePayload): + """Overwrites the policy file and instantly rebuilds the AI's RAG memory.""" + with open(rag_engine.policy_file, "w") as f: + json.dump(payload.policy_json, f, indent=2) + + # CRITICAL: Rebuild the local FAISS vector database + rag_engine.initialize() + + return {"status": "success", "message": "Policy updated. AI memory successfully refreshed."} \ No newline at end of file diff --git a/app/api/claims.py b/app/api/claims.py new file mode 100644 index 0000000000000000000000000000000000000000..3faf4bc96111c29f6e591e2181ba9f74cc4e3f6d --- /dev/null +++ b/app/api/claims.py @@ -0,0 +1,214 @@ +import uuid +from fastapi import APIRouter, UploadFile, File, Form, Depends, HTTPException, status +from pydantic import ValidationError +from thefuzz import fuzz +from datetime import datetime, timezone, timedelta +from dateutil.parser import parse + +from app.models.schemas import ClaimSubmission, AdjudicationResponse, ExtractedClaimData +from app.core.rag import rag_engine +from app.core.llm import analyze_claim_documents +from app.core.database import users_collection, claims_collection + +from typing import List + +router = APIRouter(prefix="/claims", tags=["Claims Adjudication"]) + +def parse_date_safely(date_str: str) -> datetime: + """Standardizes heterogeneous date strings into exact datetime objects.""" + try: + return parse(date_str) + except Exception: + raise ValueError(f"Unable to parse date string format: {date_str}") + +@router.post("/adjudicate", response_model=AdjudicationResponse) +async def adjudicate_claim( + member_id: str = Form(...), + claim_amount: float = Form(...), + cashless_request: bool = Form(False), + hospital_name: str = Form(None), + documents: List[UploadFile] = File(...) +): + """ + Main orchestration endpoint for OPD claim adjudication. + Executes in under 5 seconds by chaining fast local data checks with a single Gemini Vision call. + """ + claim_id = f"CLM-{uuid.uuid4().hex[:8].upper()}" + + # ========================================== + # GATEKEEPER: THE FAIL-FAST LAYER (~0.05s) + # ========================================== + + # 1. Fetch User Data + user_data = await users_collection.find_one({"member_id": member_id}) + if not user_data: + raise HTTPException(status_code=404, detail="Member ID not found in database.") + + early_rejection_reason = None + + # Gate 1: Policy Status Verification + if user_data.get("policy_status") != "ACTIVE": + early_rejection_reason = "POLICY_INACTIVE: Coverage is expired or suspended." + + # Gate 2: Absolute Limit Verification (Empty Wallet) + max_opd_limit = user_data.get("opd_wallet_balance", 0.0) + if max_opd_limit <= 0: + early_rejection_reason = "LIMIT_EXHAUSTED: Annual OPD wallet balance is zero." + + # Gate 3: Fraud Velocity Check + if member_id != "EMP000": + today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + claims_today = await claims_collection.count_documents({ + "member_id": member_id, + "created_at": {"$gte": today_start} + }) + if claims_today >= 3: + early_rejection_reason = "FRAUD_FLAG: Claim velocity exceeds daily limits. Manual review required." + + # --- EARLY EXIT IF GATEKEEPER FAILS --- + if early_rejection_reason: + # Save the rejected attempt to the database for auditing + await claims_collection.insert_one({ + "claim_id": claim_id, + "member_id": member_id, + "decision": "REJECTED", + "requested_amount": claim_amount, + "approved_amount": 0.0, + "rejection_reasons": [early_rejection_reason], + "notes": "Rejected at Gatekeeper level. AI evaluation bypassed.", + "created_at": datetime.now(timezone.utc) + }) + + return AdjudicationResponse( + claim_id=claim_id, + decision="REJECTED", + approved_amount=0.0, + rejection_reasons=[early_rejection_reason], + confidence_score=1.0, + notes="Rejected at Gatekeeper level. AI evaluation bypassed.", + next_steps="Review your policy status and wallet limits.", + cashless_approved=False, + network_discount=0.0 + ) + + rejection_reasons = [] + notes_accumulator = [] + + # 1. High-speed File Processing + try: + images_bytes_list = [] + for doc in documents: + images_bytes_list.append({ + "bytes": await doc.read(), + "mime_type": doc.content_type + }) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Failed to read file attachment payload: {str(e)}" + ) + print(">>> Starting RAG retrieval...") + # 2. Local Fast Semantic RAG Retrieval (~0.02s) + rag_context = rag_engine.get_relevant_context(query="OPD exclusions consultation caps policy terms limits validity", k=6) + print(">>> RAG retrieval complete!") + # 3. Single-Pass Multimodal Gemini Evaluation (~2-3s) + try: + extracted_data: ExtractedClaimData = await analyze_claim_documents(images_bytes_list, rag_context) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"AI extraction layer execution failed: {str(e)}" + ) + + # 4. Deterministic Rule Matching & Itemized Adjudication + base_eligible_amount = 0.0 + + # Loop through itemized claims to calculate the strictly eligible amount + for item in extracted_data.itemized_claims: + if item.is_covered and item.is_medically_necessary: + base_eligible_amount += item.amount + else: + # Log the specific item that failed in the notes, NOT as a hard rejection + reason = item.exclusion_reason if not item.is_covered else item.medical_necessity_reason + notes_accumulator.append(f"DEDUCTION: {item.description} excluded ({reason}).") + + # If NO items were valid, then fail the whole claim + if base_eligible_amount <= 0: + rejection_reasons.append("No eligible items found. All billed items were excluded or deemed medically unnecessary.") + + # Check 4c: Fuzzy Name Matching + name_match_score = fuzz.token_sort_ratio(member_id.lower(), extracted_data.patient_name.lower()) + if name_match_score < 75: + notes_accumulator.append(f"Warning: Low string variance on patient identity matching ({name_match_score}%). Verify credentials.") + + # 5. Financial Limit Calculations & Cashless Routing Engine + max_opd_limit = 5000.00 + + # Apply global policy cap to the VALID items + if base_eligible_amount > max_opd_limit: + notes_accumulator.append(f"Claim request bounded by maximum individual policy limit of INR {max_opd_limit}.") + base_eligible_amount = max_opd_limit + + network_discount = 0.0 + cashless_approved = False + + # CASHLESS VS REIMBURSEMENT ROUTING + if cashless_request: + network_discount = base_eligible_amount * 0.20 # 20% Hospital Network Discount + final_approved_amount = base_eligible_amount - network_discount + cashless_approved = True + notes_accumulator.append(f"CASHLESS APPROVED: Applied 20% network discount (INR {network_discount}).") + else: + copay_ratio = 0.10 # Standard 10% Co-pay for User Reimbursement + calculated_copay = base_eligible_amount * copay_ratio + final_approved_amount = base_eligible_amount - calculated_copay + + if calculated_copay > 0 and final_approved_amount > 0: + notes_accumulator.append(f"PARTIAL APPROVAL: A mandatory {int(copay_ratio*100)}% co-pay deduction (INR {calculated_copay}) was applied to eligible items.") + + # 6. Final State Resolution Strategy + if rejection_reasons: + # This now only triggers if base_eligible_amount is 0 + final_decision = "REJECTED" + final_approved_amount = 0.0 + next_steps = "The claim has been rejected based on policy terms. You may submit an appeal via your dashboard with valid counter-documentation." + elif cashless_request and not rejection_reasons: + final_decision = "APPROVED" + next_steps = "Cashless request authorized. The network hospital desk has been notified." + elif final_approved_amount < extracted_data.total_billed_amount: + # If they got less than they asked for (due to exclusions, caps, or copays), it's PARTIAL + final_decision = "PARTIAL" + next_steps = "Review the breakdown details. Excluded items and co-pays have been deducted." + else: + final_decision = "APPROVED" + next_steps = "No action required. The reimbursement transaction will settle directly to your verified bank account within 24 hours." + + # 7. Save Final Record & Deduct Wallet + await claims_collection.insert_one({ + "claim_id": claim_id, + "member_id": member_id, + "decision": final_decision, + "requested_amount": extracted_data.total_billed_amount, + "approved_amount": final_approved_amount, + "rejection_reasons": rejection_reasons, + "notes": " | ".join(notes_accumulator), + "created_at": datetime.now(timezone.utc) + }) + + if final_approved_amount > 0: + await users_collection.update_one( + {"member_id": member_id}, + {"$inc": {"opd_wallet_balance": -final_approved_amount}} + ) + + return AdjudicationResponse( + claim_id=claim_id, + decision=final_decision, + approved_amount=final_approved_amount, + rejection_reasons=rejection_reasons, + confidence_score=0.92 if not rejection_reasons else 0.98, + notes=" | ".join(notes_accumulator) if notes_accumulator else "Claim verified across all automated system parameters successfully.", + next_steps=next_steps, + cashless_approved=cashless_approved, + network_discount=network_discount + ) \ No newline at end of file diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/core/__pycache__/__init__.cpython-313.pyc b/app/core/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1bcec1edb344e76bfacc328c2364891a4fbe07c Binary files /dev/null and b/app/core/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/core/__pycache__/config.cpython-313.pyc b/app/core/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d967c2e0ce6bd8fcdb3a7a9676ad780e499621a1 Binary files /dev/null and b/app/core/__pycache__/config.cpython-313.pyc differ diff --git a/app/core/__pycache__/database.cpython-313.pyc b/app/core/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..657a2480c1a734a0aa133786331b75aba7762f00 Binary files /dev/null and b/app/core/__pycache__/database.cpython-313.pyc differ diff --git a/app/core/__pycache__/llm.cpython-313.pyc b/app/core/__pycache__/llm.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5dbc72a761e544a779f71d7a6e59052e184bcac4 Binary files /dev/null and b/app/core/__pycache__/llm.cpython-313.pyc differ diff --git a/app/core/__pycache__/rag.cpython-313.pyc b/app/core/__pycache__/rag.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5d56fe5b0870398188f302d1dc2b185ee7d6c53 Binary files /dev/null and b/app/core/__pycache__/rag.cpython-313.pyc differ diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000000000000000000000000000000000000..ed7e417b7a3d824287aa804522032fccbcd60410 --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,16 @@ +import os +from dotenv import load_dotenv + +# Load variables from the .env file +load_dotenv() + +class Settings: + GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") + # Defaults to local MongoDB if no URI is provided in .env + MONGO_URI = os.getenv("MONGO_URI", "mongodb+srv://sudhanp2004:root@cluster0.wsmrt.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0") + DB_NAME = "plum_opd_db" + +settings = Settings() + +if not settings.GEMINI_API_KEY: + raise ValueError("GEMINI_API_KEY is missing. Please add it to your .env file.") \ No newline at end of file diff --git a/app/core/database.py b/app/core/database.py new file mode 100644 index 0000000000000000000000000000000000000000..0384ec0e75b685903933080fe6b1fd22fe35c13d --- /dev/null +++ b/app/core/database.py @@ -0,0 +1,34 @@ +# from motor.motor_asyncio import AsyncIOMotorClient +# from app.core.config import settings + +# class Database: +# client: AsyncIOMotorClient = None + +# db = Database() + +# async def connect_to_mongo(): +# """Initialize the MongoDB connection pool.""" +# db.client = AsyncIOMotorClient(settings.MONGO_URI) +# print("βœ… Connected to MongoDB") + +# async def close_mongo_connection(): +# """Close the MongoDB connection pool.""" +# if db.client: +# db.client.close() +# print("πŸ›‘ Closed MongoDB connection") + +# def get_database(): +# """Dependency to retrieve the database instance.""" +# return db.client[settings.DB_NAME] + +import os +from motor.motor_asyncio import AsyncIOMotorClient + +# Defaulting to localhost, but update this with your MongoDB Atlas URI in production +MONGO_URI = os.getenv("MONGO_URI", "mongodb+srv://sudhanp2004:root@cluster0.wsmrt.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0") + +client = AsyncIOMotorClient(MONGO_URI) +db = client.medical_adjudication + +users_collection = db.users +claims_collection = db.claims \ No newline at end of file diff --git a/app/core/llm.py b/app/core/llm.py new file mode 100644 index 0000000000000000000000000000000000000000..a02239c1361410ffb630038cad44e1d4b2043c01 --- /dev/null +++ b/app/core/llm.py @@ -0,0 +1,127 @@ +import io +import json +from PIL import Image +from google import genai +from google.genai import types +from app.core.config import settings +from app.models.schemas import ExtractedClaimData, ItemizedClaimData + +from typing import List, Dict + +# Initialize the official modern Google GenAI Client +# It automatically picks up GEMINI_API_KEY from the environment/settings +client = genai.Client(api_key=settings.GEMINI_API_KEY) + +async def analyze_claim_documents(images_bytes_list: List[Dict[str, any]], rag_policy_context: str) -> ExtractedClaimData: + """ + Sends the medical document image and dynamic RAG policy context to Gemini 2.5 Flash. + Enforces a strict structured JSON output matching the ExtractedClaimData schema. + """ + try: + # Build the single-pass prompt combining extraction rules, RAG guidelines, and formatting schemas + system_prompt = ( + "You are an expert medical insurance claims adjudication engine. " + "Your job is to extract data from the provided document image and evaluate it " + "against the accompanying Policy Context constraints.\n\n" + "STRICT RULES:\n" + "1. Extract patient info, treatment dates, provider details, and itemized lists accurately.\n" + "2. Cross-reference all extracted data against the provided Policy Context to determine " + "if any items match listed exclusions.\n" + "3. Assess medical necessity: verify if the clinical diagnosis logically justifies the " + "prescribed drugs, procedures, or diagnostic tests.\n" + "4. Return structural results matching the requested JSON schema exactly." + "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`." + "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." + "CRITICAL - ITEMIZED EVALUATION (NOT reject all if one item fails):\n" + "7. For EVERY line item (medicine, procedure, test, consultation), determine:\n" + " - Is it in the policy exclusions list?\n" + " - Is it medically necessary given the diagnosis?\n" + " - Does it match standard clinical protocols?\n" + "8. Mark is_covered and is_medically_necessary for each item INDEPENDENTLY.\n" + "9. IMPORTANT: Do NOT reject entire claimβ€”evaluate items separately.\n" + " Some items may be covered, others excluded. That's OKβ€”return individual assessments.\n" + "10. Break down the bill into individual line items. For each item, populate the itemized_claims list.\n" + "11. Return ONLY valid JSON matching the schema. No markdown, no code blocks.\n\n" + "RESPONSE FORMAT - Return ONLY this JSON structure (no other text):\n" + "{\n" + ' "patient_name": "string",\n' + ' "treatment_date": "YYYY-MM-DD",\n' + ' "doctor_name": "string",\n' + ' "doctor_reg_no": "string",\n' + ' "diagnosis": "string",\n' + ' "itemized_claims": [\n' + ' {\n' + ' "description": "item name",\n' + ' "amount": 1000.50,\n' + ' "category": "medicine|procedure|diagnostic_test|consultation",\n' + ' "is_covered": true/false,\n' + ' "exclusion_reason": "reason or None",\n' + ' "is_medically_necessary": true/false,\n' + ' "medical_necessity_reason": "reason"\n' + " }\n" + " ],\n" + ' "total_billed_amount": 5000.00,\n' + ' "overall_medically_necessary": true,\n' + ' "medical_necessity_reasoning": "overall assessment"\n' + "}" + ) + + user_content = f""" + POLICY CONTEXT (Retrieved rules for this claim context): + ----------------------------------------------------- + {rag_policy_context} + ----------------------------------------------------- + + Please evaluate the attached medical document image according to the rules above. + Extract each line item (medicine, procedure, test, consultation) separately. + For EACH item, determine: is it covered by policy? Is it medically necessary? + Return ONLY JSON, no explanations. + """ + + contents = [user_content] + + for img_bytes in images_bytes_list: + contents.append( + types.Part.from_bytes( + data=img_bytes["bytes"], + mime_type=img_bytes["mime_type"] + ) + ) + + print(">>> 1. Preparing to call Gemini API...") + # Execute the single-pass multimodal API call without strict schema (use text JSON instead) + response = await client.aio.models.generate_content( + model='gemini-2.5-flash', + contents=contents, + config=types.GenerateContentConfig( + system_instruction=system_prompt, + response_mime_type="application/json", + temperature=0.1, # Low temperature ensures highly predictable, deterministic evaluation + ) + ) + print(">>> 2. Gemini API Call Complete!") + # Parse the response text as JSON and validate with Pydantic + response_text = response.text.strip() + + # Remove markdown code blocks if present + if response_text.startswith("```json"): + response_text = response_text[7:] + if response_text.startswith("```"): + response_text = response_text[3:] + if response_text.endswith("```"): + response_text = response_text[:-3] + + response_text = response_text.strip() + + print(f"DEBUG: Raw Gemini Response:\n{response_text[:500]}") + + response_json = json.loads(response_text) + return ExtractedClaimData.model_validate(response_json) + + except json.JSONDecodeError as je: + print(f"❌ JSON Parse Error: {str(je)}") + print(f"Response text: {response_text[:1000]}") + raise RuntimeError(f"Failed to parse Gemini JSON response: {str(je)}") + except Exception as e: + print(f"❌ Gemini Adjudication Call Failed: {str(e)}") + raise RuntimeError(f"Failed to process document via Gemini AI: {str(e)}") \ No newline at end of file diff --git a/app/core/rag.py b/app/core/rag.py new file mode 100644 index 0000000000000000000000000000000000000000..db2d7bebbaf21d5414821ae0cdd70f28eb8a53cb --- /dev/null +++ b/app/core/rag.py @@ -0,0 +1,76 @@ +import os +import json +from langchain_core.documents import Document +from langchain_text_splitters import RecursiveCharacterTextSplitter +from langchain_community.vectorstores import FAISS +from langchain_huggingface import HuggingFaceEmbeddings + +class PolicyRAG: + def __init__(self): + self.vector_store = None + # Use a small, lightning-fast model for local embeddings + self.embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2") + + # Paths to your admin data + self.base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.policy_file = os.path.join(self.base_dir, "data", "policy_terms.json") + self.rules_file = os.path.join(self.base_dir, "data", "adjudication_rules.md") + + def initialize(self): + """ + Loads the policy and rules files, chunks them, and builds the FAISS index in memory. + This runs ONLY ONCE when the FastAPI server starts. + """ + print("🧠 Initializing LangChain RAG Vector Database...") + documents = [] + + # 1. Load and parse JSON Policy Terms + if os.path.exists(self.policy_file): + with open(self.policy_file, "r") as f: + policy_data = json.load(f) + # Convert the JSON dictionary into a flat readable string for the LLM + policy_text = json.dumps(policy_data, indent=2) + documents.append(Document(page_content=f"POLICY TERMS AND LIMITS:\n{policy_text}", metadata={"source": "policy_terms"})) + else: + print("⚠️ Warning: policy_terms.json not found.") + + # 2. Load Markdown Adjudication Rules + if os.path.exists(self.rules_file): + with open(self.rules_file, "r") as f: + rules_text = f.read() + documents.append(Document(page_content=f"ADJUDICATION RULES:\n{rules_text}", metadata={"source": "adjudication_rules"})) + else: + print("⚠️ Warning: adjudication_rules.md not found.") + + # 3. Chunk the documents so we only pull relevant sections + text_splitter = RecursiveCharacterTextSplitter( + chunk_size=1000, + chunk_overlap=100, + separators=["\n## ", "\n### ", "\n", " ", ""] + ) + chunks = text_splitter.split_documents(documents) + + # 4. Build the FAISS Vector Database + if chunks: + self.vector_store = FAISS.from_documents(chunks, self.embeddings) + print(f"βœ… RAG initialized successfully with {len(chunks)} knowledge chunks.") + else: + print("❌ RAG initialization failed: No documents loaded.") + + def get_relevant_context(self, query: str, k: int = 3) -> str: + """ + Searches the FAISS database for the most relevant policy chunks based on the query. + Execution time: ~0.02 seconds. + """ + if not self.vector_store: + return "No policy context available." + + # Retrieve top k matching documents + docs = self.vector_store.similarity_search(query, k=k) + + # Combine the content into a single string for the Gemini prompt + context = "\n\n---\n\n".join([doc.page_content for doc in docs]) + return context + +# Create a singleton instance to be imported by the API router +rag_engine = PolicyRAG() \ No newline at end of file diff --git a/app/data/adjudication_rules.md b/app/data/adjudication_rules.md new file mode 100644 index 0000000000000000000000000000000000000000..b6094d85568a471526f17affb429ae8d1c326d8c --- /dev/null +++ b/app/data/adjudication_rules.md @@ -0,0 +1,138 @@ +# OPD Claim Adjudication Rules + +## Overview +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. + +## Adjudication Flow + +### Step 1: Basic Eligibility Check +- **Policy Status**: Policy must be active on the date of treatment +- **Waiting Period**: Check if waiting periods have been satisfied +- **Member Verification**: Claimant must be a covered member (employee/dependent) + +### Step 2: Document Validation +All submitted documents must meet these criteria: +- **Legibility**: Documents must be clear and readable +- **Completeness**: All required fields must be visible +- **Authenticity**: + - Doctor's registration number must be valid (format: [State Code]/[Number]/[Year]) + - Hospital/Clinic registration must be verifiable + - Bills must have proper headers and stamps +- **Date Consistency**: All documents must have matching treatment dates +- **Patient Details**: Name and age must match policy records (minor variations acceptable) + +### Step 3: Coverage Verification +Check if the treatment/service is covered: +- Compare against covered services list +- Verify it's not in exclusions list +- Check for pre-authorization requirements + +### Step 4: Limit Validation +Verify claim amount against applicable limits: +1. **Annual Limit**: Total claims YTD + current claim ≀ Annual limit +2. **Sub-limits**: Category-specific limits (consultation, pharmacy, etc.) +3. **Per-claim Limit**: Single claim cannot exceed per-claim limit +4. **Co-payment Calculation**: Apply co-pay percentages where applicable + +### Step 5: Medical Necessity Review +Evaluate if treatment was medically necessary: +- Diagnosis must justify the treatment +- Prescription must align with diagnosis +- Test results must support the diagnosis (if applicable) +- Treatment must follow standard medical protocols + +## Approval Conditions +A claim is **APPROVED** when ALL of the following are true: +- βœ… Policy is active and waiting period satisfied +- βœ… All required documents are submitted and valid +- βœ… Treatment is covered under policy +- βœ… Claim amount is within limits (after co-pay) +- βœ… Medical necessity is established +- βœ… No fraud indicators detected + +## Rejection Reasons +A claim is **REJECTED** if ANY of the following apply: + +### Category 1: Eligibility Issues +- `POLICY_INACTIVE`: Policy not active on treatment date +- `WAITING_PERIOD`: Treatment during waiting period +- `MEMBER_NOT_COVERED`: Claimant not found in policy records + +### Category 2: Documentation Issues +- `MISSING_DOCUMENTS`: Required documents not submitted +- `ILLEGIBLE_DOCUMENTS`: Documents not readable +- `INVALID_PRESCRIPTION`: Prescription missing or invalid +- `DOCTOR_REG_INVALID`: Doctor registration number invalid/missing +- `DATE_MISMATCH`: Document dates don't match +- `PATIENT_MISMATCH`: Patient details don't match records + +### Category 3: Coverage Issues +- `SERVICE_NOT_COVERED`: Treatment/service not covered +- `EXCLUDED_CONDITION`: Condition in exclusions list +- `PRE_AUTH_MISSING`: Pre-authorization required but not obtained + +### Category 4: Limit Issues +- `ANNUAL_LIMIT_EXCEEDED`: Annual limit exhausted +- `SUB_LIMIT_EXCEEDED`: Category sub-limit exceeded +- `PER_CLAIM_EXCEEDED`: Single claim limit exceeded + +### Category 5: Medical Issues +- `NOT_MEDICALLY_NECESSARY`: Treatment not justified by diagnosis +- `EXPERIMENTAL_TREATMENT`: Experimental/unproven treatment +- `COSMETIC_PROCEDURE`: Cosmetic/aesthetic procedure + +### Category 6: Process Issues +- `LATE_SUBMISSION`: Submitted after 30-day deadline +- `DUPLICATE_CLAIM`: Same treatment already claimed +- `BELOW_MIN_AMOUNT`: Claim below β‚Ή500 minimum + +## Special Scenarios + +### 1. Partial Approval +Claims can be partially approved when: +- Part of the treatment is covered, part is not +- Claim exceeds limits (approve up to limit) +- Co-payment applies + +### 2. Refer for Manual Review +Send for human review when: +- Fraud indicators detected (unusual patterns, modified documents) +- High-value claims (>β‚Ή25,000) +- Complex medical conditions +- System confidence <70% +- Member appeals automated decision + +### 3. Network vs Non-Network +- **Network providers**: Apply network discounts, cashless possible +- **Non-network**: Full payment by member, standard reimbursement + +## Fraud Indicators +Watch for these red flags: +- Multiple claims from same provider on same day +- Unusually high frequency of claims +- Bills with suspicious alterations +- Diagnosis not matching age/gender +- Duplicate bills across different dates +- Provider not registered/blacklisted + +## Decision Output Format +Every decision should include: +```json +{ + "claim_id": "CLM_XXXXX", + "decision": "APPROVED/REJECTED/PARTIAL/MANUAL_REVIEW", + "approved_amount": 0000, + "rejection_reasons": [], + "confidence_score": 0.95, + "notes": "Additional observations", + "next_steps": "What the claimant should do" +} +``` + +## Priority Rules +When multiple rules conflict: +1. Safety first (reject suspicious/fraudulent claims) +2. Policy exclusions override everything +3. Hard limits cannot be exceeded +4. Medical necessity is mandatory +5. When in doubt, refer for manual review \ No newline at end of file diff --git a/app/data/policy_terms.json b/app/data/policy_terms.json new file mode 100644 index 0000000000000000000000000000000000000000..75c231b3061299a5848b931b4c06e242bddd947a --- /dev/null +++ b/app/data/policy_terms.json @@ -0,0 +1,118 @@ +{ + "policy_id": "PLUM_OPD_2024", + "policy_name": "Plum OPD Advantage", + "effective_date": "2024-01-01", + "policy_holder": { + "company": "TechCorp Solutions Pvt Ltd", + "employees_covered": 500, + "dependents_covered": true + }, + "coverage_details": { + "annual_limit": 50000, + "per_claim_limit": 5000, + "family_floater_limit": 150000, + "consultation_fees": { + "covered": true, + "sub_limit": 2000, + "copay_percentage": 10, + "network_discount": 20 + }, + "diagnostic_tests": { + "covered": true, + "sub_limit": 10000, + "pre_authorization_required": false, + "covered_tests": [ + "Blood tests", + "Urine tests", + "X-rays", + "ECG", + "Ultrasound", + "MRI (with pre-auth)", + "CT Scan (with pre-auth)" + ] + }, + "pharmacy": { + "covered": true, + "sub_limit": 15000, + "generic_drugs_mandatory": true, + "branded_drugs_copay": 30 + }, + "dental": { + "covered": true, + "sub_limit": 10000, + "routine_checkup_limit": 2000, + "procedures_covered": [ + "Filling", + "Extraction", + "Root canal", + "Cleaning" + ], + "cosmetic_procedures": false + }, + "vision": { + "covered": true, + "sub_limit": 5000, + "eye_test_covered": true, + "glasses_contact_lenses": true, + "lasik_surgery": false + }, + "alternative_medicine": { + "covered": true, + "sub_limit": 8000, + "covered_treatments": [ + "Ayurveda", + "Homeopathy", + "Unani" + ], + "therapy_sessions_limit": 20 + } + }, + "waiting_periods": { + "initial_waiting": 30, + "pre_existing_diseases": 365, + "maternity": 270, + "specific_ailments": { + "diabetes": 90, + "hypertension": 90, + "joint_replacement": 730 + } + }, + "exclusions": [ + "Cosmetic procedures", + "Weight loss treatments", + "Infertility treatments", + "Experimental treatments", + "Self-inflicted injuries", + "Adventure sports injuries", + "War and nuclear risks", + "HIV/AIDS treatment", + "Alcoholism/drug abuse treatment", + "Non-allopathic treatments (except listed)", + "Vitamins and supplements (unless prescribed for deficiency)" + ], + "claim_requirements": { + "documents_required": [ + "Original bills and receipts", + "Prescription from registered doctor", + "Diagnostic test reports (if applicable)", + "Pharmacy bills with prescription", + "Doctor's registration number must be visible", + "Patient details must match policy records" + ], + "submission_timeline_days": 30, + "minimum_claim_amount": 500 + }, + "network_hospitals": [ + "Apollo Hospitals", + "Fortis Healthcare", + "Max Healthcare", + "Manipal Hospitals", + "Narayana Health" + ], + "cashless_facilities": { + "available": true, + "network_only": true, + "pre_approval_required": false, + "instant_approval_limit": 5000 + } +} \ No newline at end of file diff --git a/app/data/test_cases.json b/app/data/test_cases.json new file mode 100644 index 0000000000000000000000000000000000000000..2b56b988c918efe000e673f9b2d96cae37ab8ad0 --- /dev/null +++ b/app/data/test_cases.json @@ -0,0 +1,309 @@ +{ + "test_cases": [ + { + "case_id": "TC001", + "case_name": "Simple Consultation - Approved", + "description": "Regular doctor consultation for fever, all documents valid", + "input_data": { + "member_id": "EMP001", + "member_name": "Rajesh Kumar", + "treatment_date": "2024-11-01", + "claim_amount": 1500, + "documents": { + "prescription": { + "doctor_name": "Dr. Sharma", + "doctor_reg": "KA/45678/2015", + "diagnosis": "Viral fever", + "medicines_prescribed": ["Paracetamol 650mg", "Vitamin C"] + }, + "bill": { + "consultation_fee": 1000, + "diagnostic_tests": 500, + "test_names": ["CBC", "Dengue test"] + } + } + }, + "expected_output": { + "decision": "APPROVED", + "approved_amount": 1350, + "deductions": { + "copay": 150 + }, + "confidence_score": 0.95 + } + }, + { + "case_id": "TC002", + "case_name": "Dental Treatment - Partial Approval", + "description": "Root canal treatment with cosmetic whitening", + "input_data": { + "member_id": "EMP002", + "member_name": "Priya Singh", + "treatment_date": "2024-10-15", + "claim_amount": 12000, + "documents": { + "prescription": { + "doctor_name": "Dr. Patel", + "doctor_reg": "MH/23456/2018", + "diagnosis": "Tooth decay requiring root canal", + "procedures": ["Root canal treatment", "Teeth whitening"] + }, + "bill": { + "root_canal": 8000, + "teeth_whitening": 4000 + } + } + }, + "expected_output": { + "decision": "PARTIAL", + "approved_amount": 8000, + "rejected_items": ["Teeth whitening - cosmetic procedure"], + "confidence_score": 0.92 + } + }, + { + "case_id": "TC003", + "case_name": "Limit Exceeded - Rejected", + "description": "Claim exceeds per-claim limit", + "input_data": { + "member_id": "EMP003", + "member_name": "Amit Verma", + "treatment_date": "2024-10-20", + "claim_amount": 7500, + "documents": { + "prescription": { + "doctor_name": "Dr. Gupta", + "doctor_reg": "DL/34567/2016", + "diagnosis": "Gastroenteritis", + "medicines_prescribed": ["Antibiotics", "Probiotics"] + }, + "bill": { + "consultation_fee": 2000, + "medicines": 5500 + } + } + }, + "expected_output": { + "decision": "REJECTED", + "rejection_reasons": ["PER_CLAIM_EXCEEDED"], + "notes": "Claim amount exceeds per-claim limit of β‚Ή5000", + "confidence_score": 0.98 + } + }, + { + "case_id": "TC004", + "case_name": "Missing Documents - Rejected", + "description": "Prescription missing from claim submission", + "input_data": { + "member_id": "EMP004", + "member_name": "Sneha Reddy", + "treatment_date": "2024-10-25", + "claim_amount": 2000, + "documents": { + "bill": { + "consultation_fee": 1500, + "medicines": 500 + } + } + }, + "expected_output": { + "decision": "REJECTED", + "rejection_reasons": ["MISSING_DOCUMENTS"], + "notes": "Prescription from registered doctor is required", + "confidence_score": 1.0 + } + }, + { + "case_id": "TC005", + "case_name": "Pre-existing Condition - Waiting Period", + "description": "Diabetes treatment within waiting period", + "input_data": { + "member_id": "EMP005", + "member_name": "Vikram Joshi", + "member_join_date": "2024-09-01", + "treatment_date": "2024-10-15", + "claim_amount": 3000, + "documents": { + "prescription": { + "doctor_name": "Dr. Mehta", + "doctor_reg": "GJ/56789/2014", + "diagnosis": "Type 2 Diabetes", + "medicines_prescribed": ["Metformin", "Glimepiride"] + }, + "bill": { + "consultation_fee": 1000, + "medicines": 2000 + } + } + }, + "expected_output": { + "decision": "REJECTED", + "rejection_reasons": ["WAITING_PERIOD"], + "notes": "Diabetes has 90-day waiting period. Eligible from 2024-11-30", + "confidence_score": 0.96 + } + }, + { + "case_id": "TC006", + "case_name": "Alternative Medicine - Approved", + "description": "Ayurvedic treatment within limits", + "input_data": { + "member_id": "EMP006", + "member_name": "Kavita Nair", + "treatment_date": "2024-10-28", + "claim_amount": 4000, + "documents": { + "prescription": { + "doctor_name": "Vaidya Krishnan", + "doctor_reg": "AYUR/KL/2345/2019", + "diagnosis": "Chronic joint pain", + "treatment": "Panchakarma therapy" + }, + "bill": { + "consultation_fee": 1000, + "therapy_charges": 3000 + } + } + }, + "expected_output": { + "decision": "APPROVED", + "approved_amount": 4000, + "notes": "Alternative medicine covered under policy", + "confidence_score": 0.89 + } + }, + { + "case_id": "TC007", + "case_name": "Diagnostic Tests - Pre-auth Required", + "description": "MRI scan without pre-authorization", + "input_data": { + "member_id": "EMP007", + "member_name": "Suresh Patil", + "treatment_date": "2024-11-02", + "claim_amount": 15000, + "documents": { + "prescription": { + "doctor_name": "Dr. Rao", + "doctor_reg": "AP/67890/2017", + "diagnosis": "Suspected lumbar disc herniation", + "tests_prescribed": ["MRI Lumbar Spine"] + }, + "bill": { + "mri_scan": 15000 + } + } + }, + "expected_output": { + "decision": "REJECTED", + "rejection_reasons": ["PRE_AUTH_MISSING"], + "notes": "MRI requires pre-authorization for claims above β‚Ή10000", + "confidence_score": 0.94 + } + }, + { + "case_id": "TC008", + "case_name": "Fraud Detection - Manual Review", + "description": "Multiple high-value claims same day", + "input_data": { + "member_id": "EMP008", + "member_name": "Ravi Menon", + "treatment_date": "2024-10-30", + "claim_amount": 4800, + "previous_claims_same_day": 3, + "documents": { + "prescription": { + "doctor_name": "Dr. Khan", + "doctor_reg": "UP/45678/2016", + "diagnosis": "Migraine", + "medicines_prescribed": ["Sumatriptan", "Propranolol"] + }, + "bill": { + "consultation_fee": 2000, + "medicines": 2800 + } + } + }, + "expected_output": { + "decision": "MANUAL_REVIEW", + "flags": ["Multiple claims same day", "Unusual pattern detected"], + "confidence_score": 0.65 + } + }, + { + "case_id": "TC009", + "case_name": "Excluded Treatment - Rejected", + "description": "Weight loss treatment not covered", + "input_data": { + "member_id": "EMP009", + "member_name": "Anita Desai", + "treatment_date": "2024-10-18", + "claim_amount": 8000, + "documents": { + "prescription": { + "doctor_name": "Dr. Banerjee", + "doctor_reg": "WB/34567/2015", + "diagnosis": "Obesity - BMI 35", + "treatment": "Bariatric consultation and diet plan" + }, + "bill": { + "consultation_fee": 3000, + "diet_plan": 5000 + } + } + }, + "expected_output": { + "decision": "REJECTED", + "rejection_reasons": ["SERVICE_NOT_COVERED"], + "notes": "Weight loss treatments are excluded from coverage", + "confidence_score": 0.97 + } + }, + { + "case_id": "TC010", + "case_name": "Network Hospital - Cashless Approved", + "description": "Treatment at network hospital with instant approval", + "input_data": { + "member_id": "EMP010", + "member_name": "Deepak Shah", + "treatment_date": "2024-11-03", + "claim_amount": 4500, + "hospital": "Apollo Hospitals", + "cashless_request": true, + "documents": { + "prescription": { + "doctor_name": "Dr. Iyer", + "doctor_reg": "TN/56789/2013", + "diagnosis": "Acute bronchitis", + "medicines_prescribed": ["Antibiotics", "Bronchodilators"] + }, + "bill": { + "consultation_fee": 1500, + "medicines": 3000 + } + } + }, + "expected_output": { + "decision": "APPROVED", + "approved_amount": 3600, + "cashless_approved": true, + "network_discount": 900, + "confidence_score": 0.93 + } + } + ], + "validation_notes": { + "scoring_guidelines": [ + "Each test case should complete within 5 seconds", + "Confidence scores should be provided for all decisions", + "System should handle edge cases gracefully", + "Clear reasoning should be provided for rejections" + ], + "bonus_test_scenarios": [ + "Handle illegible/blurry document images", + "Process handwritten prescriptions", + "Detect forged/modified documents", + "Handle claims in regional languages", + "Process multiple receipts in single claim" + ] + } +} \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/models/__pycache__/__init__.cpython-313.pyc b/app/models/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4188bc86a562cf9b74a6da1d3be16fc9408661e Binary files /dev/null and b/app/models/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/models/__pycache__/schemas.cpython-313.pyc b/app/models/__pycache__/schemas.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..993efee4b73bca017c2582fa8fcc2db59ff00aed Binary files /dev/null and b/app/models/__pycache__/schemas.cpython-313.pyc differ diff --git a/app/models/schemas.py b/app/models/schemas.py new file mode 100644 index 0000000000000000000000000000000000000000..e044ce80cb6fa46710ddcdb6c3073aa04ac73d4d --- /dev/null +++ b/app/models/schemas.py @@ -0,0 +1,62 @@ +from pydantic import BaseModel, Field +from typing import List, Optional, Literal +from datetime import date + +# --------------------------------------------------------- +# 0. Itemized Claim Item Schema +# (Individual item evaluation) +# --------------------------------------------------------- +class ItemizedClaimData(BaseModel): + description: str = Field(description="Item description (medicine/procedure/test/consultation)") + amount: float = Field(description="Amount billed for this item in INR") + category: str = Field(description="Category: medicine, procedure, diagnostic_test, consultation") + is_covered: bool = Field(description="True if covered by policy") + exclusion_reason: str = Field(description="If not covered, why. Otherwise 'None'") + is_medically_necessary: bool = Field(description="True if clinically justified") + medical_necessity_reason: str = Field(description="Why it is/isn't necessary") + +# --------------------------------------------------------- +# 1. Gemini Extraction Output Schema +# (We force Gemini to output this exact structure) +# --------------------------------------------------------- +class ExtractedClaimData(BaseModel): + patient_name: str = Field(description="Name of the patient on the document") + treatment_date: str = Field(description="Date of treatment in YYYY-MM-DD format") + doctor_name: str = Field(description="Name of the treating doctor") + doctor_reg_no: str = Field(description="Doctor's medical registration number") + diagnosis: str = Field(description="Primary diagnosis or reason for visit") + is_treatment_excluded: bool = Field(default=False, description="True if the treatment matches an exclusion") + exclusion_reason: str = Field(default="None") + # Itemized breakdown instead of simple lists + itemized_claims: List[ItemizedClaimData] = Field(description="Line-by-line breakdown of all billed items with individual coverage assessment") + requires_pre_auth: bool = Field(default=False, description="True if procedure needs pre-auth") + missing_pre_auth: bool = Field(default=False, description="True if pre-auth is missing") + total_billed_amount: float = Field(description="Total amount billed in INR") + overall_medically_necessary: bool = Field(description="Is the overall treatment medically justified") + medical_necessity_reasoning: str = Field(description="Overall clinical assessment") + +# --------------------------------------------------------- +# 2. API Input Schema +# (What the frontend/Postman sends to our FastAPI route) +# --------------------------------------------------------- +class ClaimSubmission(BaseModel): + member_id: str + claim_amount: float + cashless_request: bool = False + hospital_name: Optional[str] = None + # Note: Files (images/pdfs) will be handled via FastAPI UploadFile, not Pydantic + +# --------------------------------------------------------- +# 3. Final Adjudication Output Schema +# (Matches the exact assignment requirement format) +# --------------------------------------------------------- +class AdjudicationResponse(BaseModel): + claim_id: str + decision: Literal["APPROVED", "REJECTED", "PARTIAL", "MANUAL_REVIEW"] + approved_amount: float + rejection_reasons: List[str] + confidence_score: float + notes: str + next_steps: str + cashless_approved: bool = False + network_discount: float = 0.0 \ No newline at end of file diff --git a/deepseek.py b/deepseek.py new file mode 100644 index 0000000000000000000000000000000000000000..3c0fdb79a9a18b5c496256cd9f00a43e13186a64 --- /dev/null +++ b/deepseek.py @@ -0,0 +1,16 @@ +import ollama + +with open("handwriting.png", "rb") as f: + image_data = f.read() + +response = ollama.chat( + model="deepseek-ocr", + messages=[ + { + "role": "user", + "content": "Extract all text from this medical prescription image.", + "images": [image_data] + } + ] +) +print(response["message"]["content"]) \ No newline at end of file diff --git a/download.png b/download.png new file mode 100644 index 0000000000000000000000000000000000000000..1e4463ce43b5f41d6e8ee70db72e34a1e4b3267b Binary files /dev/null and b/download.png differ diff --git a/gemini_vision.py b/gemini_vision.py new file mode 100644 index 0000000000000000000000000000000000000000..16727556bcd5a31b4193ad715f3fef46cfd4a81f --- /dev/null +++ b/gemini_vision.py @@ -0,0 +1,31 @@ +import os +from google import genai +from google.genai import types +from PIL import Image + +def extract_text_gemini(image_path: str) -> str: + """ + Sends an image to Gemini Vision and extracts the text/data. + Requires GEMINI_API_KEY environment variable to be set. + """ + # Initialize the client (automatically picks up GEMINI_API_KEY from env) + client = genai.Client() + + # Load the image + image = Image.open(image_path) + + # Define the prompt + prompt = "Extract all text, handwriting, and tabular data from this medical document accurately." + + # Call the model (gemini-2.5-flash is currently recommended for fast, multimodal tasks) + response = client.models.generate_content( + model="gemini-2.5-flash", + contents=[prompt, image] + ) + + return response.text + +# Example Usage: +api_key = os.environ["GEMINI_API_KEY"] +text_output = extract_text_gemini("handwriting.png") +print(text_output) \ No newline at end of file diff --git a/google_cloud_vision.py b/google_cloud_vision.py new file mode 100644 index 0000000000000000000000000000000000000000..29da3c28c9d8938e607f5eee90d930d59b4c85ae --- /dev/null +++ b/google_cloud_vision.py @@ -0,0 +1,32 @@ +import os +from google.cloud import vision + +def extract_raw_text(image_path: str) -> str: + """ + Sends an image to Google Cloud Vision API and returns the raw extracted text. + Requires GOOGLE_APPLICATION_CREDENTIALS environment variable to be set. + """ + # Initialize the Vision API Client + client = vision.ImageAnnotatorClient() + + # Load the image into memory + with open(image_path, 'rb') as image_file: + content = image_file.read() + + image = vision.Image(content=content) + + # Perform Document Text Detection (optimized for handwriting/dense text) + response = client.document_text_detection(image=image) + + # Error handling for the API response + if response.error.message: + raise Exception(f"Google Cloud Vision API Error: {response.error.message}") + + # Extract and return the full text annotation + full_text = response.full_text_annotation.text + return full_text + +# Example Usage: +os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "credentials.json" +raw_text = extract_raw_text("115.jpg") +print(raw_text) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000000000000000000000000000000000000..657a83e75a821a146e5f25efb6f0c710c92cd511 --- /dev/null +++ b/main.py @@ -0,0 +1,51 @@ +from contextlib import asynccontextmanager +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from app.api import auth_routes + +from app.core.rag import rag_engine +from app.api import claims + +@asynccontextmanager +async def lifespan(app: FastAPI): + """ + Handles startup and shutdown events asynchronously. + This guarantees our DB and AI memory are ready before the first request arrives. + """ + # --- Startup --- + rag_engine.initialize() + yield + # --- Shutdown --- + +# Initialize the FastAPI application +app = FastAPI( + title="Plum OPD Adjudication Engine", + description="AI-powered automation tool for processing OPD medical claims.", + version="1.0.0", + lifespan=lifespan +) + +# Configure Cross-Origin Resource Sharing (CORS) +# This allows your frontend (e.g., React on port 3000) to talk to this API +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # In production, replace "*" with your Vercel frontend URL + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Register our API routes +app.include_router(claims.router, prefix="/api") + +app.include_router(auth_routes.router, prefix="/api") + +@app.get("/") +async def health_check(): + """Simple endpoint to verify the server is running.""" + return { + "status": "online", + "service": "Plum OPD Adjudication Engine", + "database": "connected", + "rag_memory": "loaded" if rag_engine.vector_store else "offline" + } \ No newline at end of file diff --git a/plum-opd-frontend/.gitignore b/plum-opd-frontend/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..5ef6a520780202a1d6addd833d800ccb1ecac0bb --- /dev/null +++ b/plum-opd-frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/plum-opd-frontend/AGENTS.md b/plum-opd-frontend/AGENTS.md new file mode 100644 index 0000000000000000000000000000000000000000..8bd0e39085d5260e7f8faffcad2fdc45e10aef33 --- /dev/null +++ b/plum-opd-frontend/AGENTS.md @@ -0,0 +1,5 @@ + +# This is NOT the Next.js you know + +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. + diff --git a/plum-opd-frontend/CLAUDE.md b/plum-opd-frontend/CLAUDE.md new file mode 100644 index 0000000000000000000000000000000000000000..43c994c2d3617f947bcb5adf1933e21dabe46bb5 --- /dev/null +++ b/plum-opd-frontend/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/plum-opd-frontend/README.md b/plum-opd-frontend/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e215bc4ccf138bbc38ad58ad57e92135484b3c0f --- /dev/null +++ b/plum-opd-frontend/README.md @@ -0,0 +1,36 @@ +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). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +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. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +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. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/plum-opd-frontend/app/admin/page.tsx b/plum-opd-frontend/app/admin/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0f5baf694f1a2090f2eb952626c8a65f43ac76fc --- /dev/null +++ b/plum-opd-frontend/app/admin/page.tsx @@ -0,0 +1,134 @@ +"use client"; +import React, { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { Settings, Save, AlertTriangle, CheckCircle, RefreshCw, ArrowLeft, FileJson } from 'lucide-react'; + +export default function AdminDashboard() { + const router = useRouter(); + + const [policyJson, setPolicyJson] = useState(''); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [status, setStatus] = useState<{ type: 'success' | 'error', message: string } | null>(null); + + // Fetch the current policy on page load + useEffect(() => { + const fetchPolicy = async () => { + try { + const response = await fetch("http://127.0.0.1:8000/api/admin/policy"); + if (!response.ok) throw new Error("Failed to connect to backend policy store."); + + const data = await response.json(); + setPolicyJson(JSON.stringify(data, null, 2)); + } catch (err: any) { + setStatus({ type: 'error', message: err.message || "Could not load policy data." }); + } finally { + setIsLoading(false); + } + }; + fetchPolicy(); + }, []); + + const handleSavePolicy = async () => { + setIsSaving(true); + setStatus(null); + + // 1. Validate JSON syntax before sending to the backend + let parsedJson; + try { + parsedJson = JSON.parse(policyJson); + } catch (e) { + setStatus({ type: 'error', message: "Invalid JSON format. Please check your syntax." }); + setIsSaving(false); + return; + } + + // 2. Send the update command to the FastAPI server + try { + const response = await fetch("http://127.0.0.1:8000/api/admin/policy/update", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ policy_json: parsedJson }) + }); + + if (!response.ok) throw new Error("Backend rejected the policy update."); + + const result = await response.json(); + setStatus({ type: 'success', message: "Policy saved successfully. RAG memory has been rebuilt." }); + + // Clear success message after 4 seconds + setTimeout(() => setStatus(null), 4000); + } catch (err: any) { + setStatus({ type: 'error', message: err.message || "Failed to update engine rules." }); + } finally { + setIsSaving(false); + } + }; + + return ( +
+
+ + {/* Navigation and Header */} +
+
+ +
+

+ Configuration Control +

+

Direct memory manipulation for the RAG evaluation engine.

+
+
+ + +
+ + {/* Status Alerts */} + {status && ( +
+ {status.type === 'success' ? : } +

{status.message}

+
+ )} + + {/* Code Editor */} +
+
+ + policy_dataset.json +
+ + {isLoading ? ( +
+ +

Extracting rules from vector database...

+
+ ) : ( +