Spaces:
Running
Running
| """ | |
| Explanation Tools — Custom tools for the Explanation Agent. | |
| Tools: | |
| 9. what_if_analyzer — Run decision engine with modified parameters | |
| 10. savings_calculator — Calculate savings from alternative choices | |
| 11. clause_explainer — Explain policy clauses in plain language | |
| """ | |
| import logging | |
| import copy | |
| from engine.decision_engine import evaluate | |
| from models.case import CaseFacts, RoomType | |
| from models.verdict import Verdict | |
| from tools.case_tools import hospital_cost_estimator | |
| logger = logging.getLogger(__name__) | |
| # --- Tool 9: What-If Analyzer --- | |
| _ROOM_DOWNGRADE_MAP = { | |
| "suite": "deluxe", | |
| "deluxe": "single_ac", | |
| "single_ac": "private", | |
| "private": "semi_private", | |
| "semi_private": "general", | |
| "icu": "icu", # Can't downgrade ICU | |
| "general": "general", # Already lowest | |
| } | |
| # Approximate room costs per tier for what-if estimation | |
| _ROOM_COST_ESTIMATES = { | |
| "general": {"tier_1": 1500, "tier_2": 800, "tier_3": 500}, | |
| "semi_private": {"tier_1": 4000, "tier_2": 2500, "tier_3": 1500}, | |
| "private": {"tier_1": 8000, "tier_2": 5000, "tier_3": 3000}, | |
| "single_ac": {"tier_1": 10000, "tier_2": 6000, "tier_3": 4000}, | |
| "deluxe": {"tier_1": 18000, "tier_2": 12000, "tier_3": 8000}, | |
| "suite": {"tier_1": 30000, "tier_2": 20000, "tier_3": 15000}, | |
| "icu": {"tier_1": 25000, "tier_2": 15000, "tier_3": 8000}, | |
| } | |
| def what_if_analyzer( | |
| rules: list[dict], | |
| original_facts: dict, | |
| sum_insured: float, | |
| modification: dict, | |
| ) -> dict: | |
| """ | |
| Run decision engine with modified case parameters and compare results. | |
| Args: | |
| rules: Policy rules | |
| original_facts: Original case facts as dict | |
| sum_insured: Policy sum insured | |
| modification: Dict of field → new value changes to apply | |
| Returns: | |
| { | |
| "original_verdict": {eligible, denied, coverage}, | |
| "modified_verdict": {eligible, denied, coverage}, | |
| "savings": float (positive = patient saves money), | |
| "changes_applied": dict, | |
| "recommendation": str | |
| } | |
| """ | |
| # Run original | |
| original_case = CaseFacts(**original_facts) | |
| original_verdict = evaluate(rules, original_case, sum_insured) | |
| # Apply modifications | |
| modified_data = copy.deepcopy(original_facts) | |
| for field, value in modification.items(): | |
| if field in modified_data: | |
| modified_data[field] = value | |
| # Recalculate total if room cost or stay changed | |
| if "room_cost_per_day" in modification or "stay_duration_days" in modification: | |
| room_cost = float(modified_data.get("room_cost_per_day", 0)) | |
| stay = int(modified_data.get("stay_duration_days", 1)) | |
| proc_cost = float(modified_data.get("procedure_cost", 0) or 0) | |
| modified_data["total_claimed_amount"] = (room_cost * stay) + proc_cost | |
| modified_case = CaseFacts(**modified_data) | |
| modified_verdict = evaluate(rules, modified_case, sum_insured) | |
| savings = modified_verdict.total_eligible - original_verdict.total_eligible | |
| savings_out_of_pocket = original_verdict.total_denied - modified_verdict.total_denied | |
| # Generate recommendation | |
| if savings_out_of_pocket > 0: | |
| recommendation = ( | |
| f"This change would reduce your out-of-pocket expense by ₹{savings_out_of_pocket:,.0f}. " | |
| f"Coverage improves from {original_verdict.coverage_percentage}% to {modified_verdict.coverage_percentage}%." | |
| ) | |
| elif savings_out_of_pocket == 0: | |
| recommendation = "This change would not affect your out-of-pocket expenses." | |
| else: | |
| recommendation = ( | |
| f"This change would increase your out-of-pocket expense by ₹{abs(savings_out_of_pocket):,.0f}." | |
| ) | |
| logger.info(f"[Tool:what_if_analyzer] Original: {original_verdict.coverage_percentage}% → " | |
| f"Modified: {modified_verdict.coverage_percentage}%, Savings: ₹{savings_out_of_pocket:,.0f}") | |
| return { | |
| "original_verdict": { | |
| "eligible": original_verdict.total_eligible, | |
| "denied": original_verdict.total_denied, | |
| "coverage_pct": original_verdict.coverage_percentage, | |
| }, | |
| "modified_verdict": { | |
| "eligible": modified_verdict.total_eligible, | |
| "denied": modified_verdict.total_denied, | |
| "coverage_pct": modified_verdict.coverage_percentage, | |
| }, | |
| "savings_out_of_pocket": savings_out_of_pocket, | |
| "changes_applied": modification, | |
| "recommendation": recommendation, | |
| } | |
| # --- Tool 10: Savings Calculator --- | |
| def savings_calculator( | |
| rules: list[dict], | |
| original_facts: dict, | |
| sum_insured: float, | |
| ) -> dict: | |
| """ | |
| Automatically calculate potential savings by exploring common alternatives: | |
| 1. Room downgrade (one level down) | |
| 2. Switch to network hospital | |
| 3. Both combined | |
| Args: | |
| rules: Policy rules | |
| original_facts: Original case facts | |
| sum_insured: Policy sum insured | |
| Returns: | |
| { | |
| "alternatives": [ | |
| {"change": str, "savings": float, "new_coverage": float, "details": str} | |
| ], | |
| "max_possible_savings": float, | |
| "best_alternative": str | |
| } | |
| """ | |
| original_case = CaseFacts(**original_facts) | |
| original_verdict = evaluate(rules, original_case, sum_insured) | |
| alternatives = [] | |
| # Alternative 1: Room downgrade | |
| current_room = original_facts.get("room_type", "single_ac") | |
| lower_room = _ROOM_DOWNGRADE_MAP.get(current_room, current_room) | |
| if lower_room != current_room: | |
| city_tier = original_facts.get("city_tier", "tier_1") | |
| new_room_cost = _ROOM_COST_ESTIMATES.get(lower_room, {}).get(city_tier, original_facts.get("room_cost_per_day", 0)) | |
| room_result = what_if_analyzer( | |
| rules, original_facts, sum_insured, | |
| { | |
| "room_type": lower_room, | |
| "room_cost_per_day": new_room_cost, | |
| } | |
| ) | |
| if room_result["savings_out_of_pocket"] > 0: | |
| alternatives.append({ | |
| "change": f"Switch from {current_room.replace('_', ' ')} to {lower_room.replace('_', ' ')}", | |
| "savings": room_result["savings_out_of_pocket"], | |
| "new_coverage": room_result["modified_verdict"]["coverage_pct"], | |
| "new_room_cost_per_day": new_room_cost, | |
| "details": room_result["recommendation"], | |
| }) | |
| # Alternative 2: Reduce stay by 1 day (if applicable) | |
| stay = int(original_facts.get("stay_duration_days", 1)) | |
| if stay > 2: | |
| stay_result = what_if_analyzer( | |
| rules, original_facts, sum_insured, | |
| {"stay_duration_days": stay - 1} | |
| ) | |
| if stay_result["savings_out_of_pocket"] > 0: | |
| alternatives.append({ | |
| "change": f"Reduce stay from {stay} to {stay - 1} days (if medically possible)", | |
| "savings": stay_result["savings_out_of_pocket"], | |
| "new_coverage": stay_result["modified_verdict"]["coverage_pct"], | |
| "details": stay_result["recommendation"], | |
| }) | |
| # Sort by savings | |
| alternatives.sort(key=lambda x: x["savings"], reverse=True) | |
| max_savings = alternatives[0]["savings"] if alternatives else 0 | |
| best = alternatives[0]["change"] if alternatives else "No cost-saving alternatives found" | |
| logger.info(f"[Tool:savings_calculator] Found {len(alternatives)} alternatives, max savings: ₹{max_savings:,.0f}") | |
| return { | |
| "alternatives": alternatives, | |
| "max_possible_savings": max_savings, | |
| "best_alternative": best, | |
| "original_out_of_pocket": original_verdict.total_denied, | |
| } | |
| # --- Tool 11: Clause Explainer --- | |
| _CATEGORY_EXPLANATIONS = { | |
| "room_rent": { | |
| "simple": "This rule limits how much the insurance will pay for your hospital room per day.", | |
| "example": "If your room costs ₹10,000/day but the policy cap is ₹5,000/day, " | |
| "you pay the extra ₹5,000/day from your pocket.", | |
| "tip": "Choose a room within the policy limit to avoid out-of-pocket costs.", | |
| }, | |
| "copay": { | |
| "simple": "Co-payment means you share a percentage of the total bill with the insurance company.", | |
| "example": "With 20% co-pay on a ₹1,00,000 bill, insurance pays ₹80,000 and you pay ₹20,000.", | |
| "tip": "If your policy has voluntary co-pay, removing it increases premium but eliminates co-pay deductions.", | |
| }, | |
| "sublimit": { | |
| "simple": "A sub-limit caps the maximum amount payable for a specific treatment or item.", | |
| "example": "If cataract surgery has a ₹40,000 sub-limit but the actual cost is ₹60,000, " | |
| "you pay the extra ₹20,000.", | |
| "tip": "Check sub-limits for your specific procedure before choosing a hospital.", | |
| }, | |
| "exclusion_permanent": { | |
| "simple": "This procedure/condition is not covered at all under your policy.", | |
| "example": "Cosmetic surgery is typically excluded. If you get rhinoplasty for cosmetic reasons, " | |
| "insurance won't pay anything.", | |
| "tip": "Check if there's a medical necessity exception — some exclusions don't apply if " | |
| "the treatment is medically required (e.g., reconstructive surgery after an accident).", | |
| }, | |
| "waiting_period_initial": { | |
| "simple": "You cannot make claims during the first 30 days of your policy (except for accidents).", | |
| "example": "If you buy insurance on January 1 and need planned surgery on January 15, it won't be covered.", | |
| "tip": "Emergency and accident-related hospitalization is covered from day 1.", | |
| }, | |
| "waiting_period_specific": { | |
| "simple": "Certain diseases/procedures have a mandatory waiting period (usually 2-4 years) " | |
| "before they're covered.", | |
| "example": "Knee replacement typically has a 24-month wait. If your policy is 6 months old, " | |
| "this surgery isn't covered yet.", | |
| "tip": "Keep your policy continuously renewed — waiting periods are served only once.", | |
| }, | |
| "waiting_period_pec": { | |
| "simple": "Pre-existing diseases (conditions you had before buying insurance) " | |
| "are covered only after 2-4 years.", | |
| "example": "If you have diabetes when you buy insurance, diabetes-related hospitalization " | |
| "won't be covered for 36-48 months.", | |
| "tip": "After 8 years of continuous coverage, the insurer cannot deny any claim citing pre-existing disease.", | |
| }, | |
| "deductible": { | |
| "simple": "A deductible is a fixed amount you must pay before insurance kicks in.", | |
| "example": "With a ₹10,000 deductible on a ₹50,000 bill, you pay ₹10,000 and insurance pays ₹40,000.", | |
| "tip": "Higher deductible = lower premium. Choose based on your financial comfort.", | |
| }, | |
| } | |
| def clause_explainer(category: str, clause_text: str = "", clause_ref: str = "") -> dict: | |
| """ | |
| Explain a policy clause in simple, patient-friendly language. | |
| Args: | |
| category: Rule category (room_rent, copay, exclusion_permanent, etc.) | |
| clause_text: The original clause text from the policy | |
| clause_ref: Clause reference number | |
| Returns: | |
| { | |
| "category": str, | |
| "simple_explanation": str, | |
| "example": str, | |
| "tip": str, | |
| "original_clause": str, | |
| "clause_reference": str | |
| } | |
| """ | |
| # Find the best matching explanation | |
| explanation = _CATEGORY_EXPLANATIONS.get(category) | |
| if not explanation: | |
| # Try to match partial category names | |
| for key, exp in _CATEGORY_EXPLANATIONS.items(): | |
| if key in category or category in key: | |
| explanation = exp | |
| break | |
| if not explanation: | |
| explanation = { | |
| "simple": f"This rule ({category}) affects how your claim is processed.", | |
| "example": "Please refer to your policy document for specific details.", | |
| "tip": "Contact your insurer's customer service for clarification on this clause.", | |
| } | |
| return { | |
| "category": category, | |
| "simple_explanation": explanation["simple"], | |
| "example": explanation["example"], | |
| "tip": explanation["tip"], | |
| "original_clause": clause_text, | |
| "clause_reference": clause_ref, | |
| } | |