Upload 19 files
Browse files- thicc/.DS_Store +0 -0
- thicc/ai_logic/config.py +121 -0
- thicc/ai_logic/intent_parser.py +80 -0
- thicc/ai_logic/rag_service.py +307 -0
- thicc/app.py +164 -0
- thicc/data/data_loader.py +31 -0
- thicc/data/hospitals/cedars_sinai_medical_center_data.csv +8 -0
- thicc/data/hospitals/cleveland_clinic_data.csv +6 -0
- thicc/data/hospitals/houston_methodist_hospital_data.csv +8 -0
- thicc/data/hospitals/johns_hopkins_hospital_data.csv +8 -0
- thicc/data/hospitals/massachusetts_general_hospital_data.csv +8 -0
- thicc/data/hospitals/mayo_clinic_data.csv +8 -0
- thicc/data/hospitals/newyork_presbyterian_hospital_data.csv +8 -0
- thicc/data/hospitals/northwestern_memorial_hospital_data.csv +8 -0
- thicc/data/hospitals/ucla_medical_center_data.csv +8 -0
- thicc/data/hospitals/ucsf_medical_center_data.csv +8 -0
- thicc/insurance/cost_estimator.py +13 -0
- thicc/insurance/coverage_explainer.py +248 -0
- thicc/insurance/plans.py +14 -0
thicc/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
thicc/ai_logic/config.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Configuration for LLM and RAG components.
|
| 3 |
+
Supports both OpenAI and Ollama (local) providers.
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Optional, Literal
|
| 8 |
+
|
| 9 |
+
# Load environment variables from .env file if it exists
|
| 10 |
+
try:
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
env_path = os.environ.get("THICC_ENV", Path(os.getcwd()) / ".env")
|
| 13 |
+
if env_path.exists():
|
| 14 |
+
print(f"Loading environment variables from {env_path}")
|
| 15 |
+
load_dotenv(env_path)
|
| 16 |
+
except ImportError:
|
| 17 |
+
pass # python-dotenv not installed, will use system environment variables
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# LLM Provider Selection
|
| 21 |
+
# Supported: openai, ollama, llama_cpp
|
| 22 |
+
LLM_PROVIDER: Literal["openai", "ollama", "llama_cpp"] = os.getenv("LLM_PROVIDER", "llama_cpp").lower()
|
| 23 |
+
|
| 24 |
+
# OpenAI Configuration
|
| 25 |
+
OPENAI_API_KEY: Optional[str] = os.getenv("OPENAI_API_KEY")
|
| 26 |
+
OPENAI_MODEL: str = os.getenv("OPENAI_MODEL", "gpt-4o-mini") # or "gpt-3.5-turbo"
|
| 27 |
+
|
| 28 |
+
# Ollama Configuration
|
| 29 |
+
OLLAMA_BASE_URL: str = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
|
| 30 |
+
OLLAMA_MODEL: str = os.getenv("OLLAMA_MODEL", "llama3.2") # or "mistral", "phi3", etc.
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# llama.cpp Configuration (OpenAI-compatible server)
|
| 34 |
+
LLAMA_CPP_API_BASE: str = os.getenv("LLAMA_CPP_API_BASE", "http://192.168.0.28:8012/v1")
|
| 35 |
+
LLAMA_CPP_MODEL: str = os.getenv("LLAMA_CPP_MODEL", "llama-2-7b-chat")
|
| 36 |
+
|
| 37 |
+
# Embedding Configuration
|
| 38 |
+
EMBEDDING_PROVIDER: Literal["openai", "ollama", "local"] = os.getenv("EMBEDDING_PROVIDER", "local").lower()
|
| 39 |
+
OPENAI_EMBEDDING_MODEL: str = os.getenv("EMBEDDING_MODEL", "text-embedding-3-small")
|
| 40 |
+
OLLAMA_EMBEDDING_MODEL: str = os.getenv("OLLAMA_EMBEDDING_MODEL", "nomic-embed-text")
|
| 41 |
+
LOCAL_EMBEDDING_MODEL: str = os.getenv("LOCAL_EMBEDDING_MODEL", "all-MiniLM-L6-v2")
|
| 42 |
+
|
| 43 |
+
# RAG Configuration
|
| 44 |
+
VECTOR_STORE_PERSIST_DIR: str = os.getenv("VECTOR_STORE_DIR", "./chroma_db")
|
| 45 |
+
CHUNK_SIZE: int = 500
|
| 46 |
+
CHUNK_OVERLAP: int = 50
|
| 47 |
+
|
| 48 |
+
# LLM Temperature (0.0 = deterministic, 1.0 = creative)
|
| 49 |
+
TEMPERATURE: float = float(os.getenv("TEMPERATURE", "0.3"))
|
| 50 |
+
|
| 51 |
+
def validate_config():
|
| 52 |
+
"""Validate that required configuration is present based on provider."""
|
| 53 |
+
if LLM_PROVIDER == "openai":
|
| 54 |
+
if not OPENAI_API_KEY:
|
| 55 |
+
raise ValueError(
|
| 56 |
+
"LLM_PROVIDER is set to 'openai' but OPENAI_API_KEY environment variable is not set. "
|
| 57 |
+
"Please set it or change LLM_PROVIDER to 'ollama' or 'llama_cpp'."
|
| 58 |
+
)
|
| 59 |
+
elif LLM_PROVIDER == "ollama":
|
| 60 |
+
# Check if Ollama is accessible
|
| 61 |
+
try:
|
| 62 |
+
import requests
|
| 63 |
+
response = requests.get(f"{OLLAMA_BASE_URL}/api/tags", timeout=2)
|
| 64 |
+
if response.status_code != 200:
|
| 65 |
+
raise ValueError(
|
| 66 |
+
f"Ollama server not accessible at {OLLAMA_BASE_URL}. "
|
| 67 |
+
"Please start Ollama with: ollama serve"
|
| 68 |
+
)
|
| 69 |
+
except Exception as e:
|
| 70 |
+
raise ValueError(
|
| 71 |
+
f"Cannot connect to Ollama at {OLLAMA_BASE_URL}. "
|
| 72 |
+
f"Please start Ollama server with: ollama serve\n"
|
| 73 |
+
f"Error: {e}"
|
| 74 |
+
)
|
| 75 |
+
elif LLM_PROVIDER == "llama_cpp":
|
| 76 |
+
# Check if llama.cpp server is accessible
|
| 77 |
+
try:
|
| 78 |
+
import requests
|
| 79 |
+
response = requests.get(f"{LLAMA_CPP_API_BASE}/models", timeout=2)
|
| 80 |
+
if response.status_code != 200:
|
| 81 |
+
raise ValueError(
|
| 82 |
+
f"llama.cpp server not accessible at {LLAMA_CPP_API_BASE}. "
|
| 83 |
+
"Please start llama.cpp with OpenAI API compatibility."
|
| 84 |
+
)
|
| 85 |
+
except Exception as e:
|
| 86 |
+
raise ValueError(
|
| 87 |
+
f"Cannot connect to llama.cpp at {LLAMA_CPP_API_BASE}. "
|
| 88 |
+
f"Please start llama.cpp server with OpenAI API compatibility.\n"
|
| 89 |
+
f"Error: {e}"
|
| 90 |
+
)
|
| 91 |
+
else:
|
| 92 |
+
raise ValueError(
|
| 93 |
+
f"Invalid LLM_PROVIDER: {LLM_PROVIDER}. Must be 'openai', 'ollama', or 'llama_cpp'."
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
def get_provider_info() -> dict:
|
| 97 |
+
"""Get information about the current LLM provider configuration."""
|
| 98 |
+
if LLM_PROVIDER == "openai":
|
| 99 |
+
llm_model = OPENAI_MODEL
|
| 100 |
+
url = None
|
| 101 |
+
elif LLM_PROVIDER == "ollama":
|
| 102 |
+
llm_model = OLLAMA_MODEL
|
| 103 |
+
url = OLLAMA_BASE_URL
|
| 104 |
+
print(f"Ollama URL: {url}")
|
| 105 |
+
elif LLM_PROVIDER == "llama_cpp":
|
| 106 |
+
llm_model = LLAMA_CPP_MODEL
|
| 107 |
+
url = LLAMA_CPP_API_BASE
|
| 108 |
+
else:
|
| 109 |
+
llm_model = None
|
| 110 |
+
url = None
|
| 111 |
+
return {
|
| 112 |
+
"llm_provider": LLM_PROVIDER,
|
| 113 |
+
"llm_model": llm_model,
|
| 114 |
+
"embedding_provider": EMBEDDING_PROVIDER,
|
| 115 |
+
"embedding_model": (
|
| 116 |
+
LOCAL_EMBEDDING_MODEL if EMBEDDING_PROVIDER == "local"
|
| 117 |
+
else OLLAMA_EMBEDDING_MODEL if EMBEDDING_PROVIDER == "ollama"
|
| 118 |
+
else OPENAI_EMBEDDING_MODEL
|
| 119 |
+
),
|
| 120 |
+
"provider_url": url,
|
| 121 |
+
}
|
thicc/ai_logic/intent_parser.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Optional
|
| 3 |
+
|
| 4 |
+
import pandas as pd
|
| 5 |
+
|
| 6 |
+
from .rag_service import get_rag_service
|
| 7 |
+
from insurance.coverage_explainer import CoverageExplainer
|
| 8 |
+
|
| 9 |
+
USE_LLM = os.getenv("USE_LLM", "true").lower() == "true"
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def parse_intent_simple(user_input: str, services_data: pd.DataFrame) -> Optional[str]:
|
| 13 |
+
"""Simple rule-based intent parsing with coverage question support."""
|
| 14 |
+
user_input = user_input.lower()
|
| 15 |
+
|
| 16 |
+
# Check coverage questions first (not really used now, but harmless)
|
| 17 |
+
if CoverageExplainer.identify_coverage_question(user_input):
|
| 18 |
+
return "coverage_explanation"
|
| 19 |
+
|
| 20 |
+
# Check for help/list requests
|
| 21 |
+
if any(word in user_input for word in ["help", "support", "services", "available"]):
|
| 22 |
+
return "list_services"
|
| 23 |
+
|
| 24 |
+
# Service keyword matching - match service descriptions mentioned in the user input
|
| 25 |
+
res = services_data[
|
| 26 |
+
services_data["description"].apply(
|
| 27 |
+
lambda desc: desc.lower() in user_input
|
| 28 |
+
)
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
if not res.empty:
|
| 32 |
+
return res.iloc[0]["intent"]
|
| 33 |
+
|
| 34 |
+
return None
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def parse_intent_with_llm(
|
| 38 |
+
user_input: str,
|
| 39 |
+
services_data: pd.DataFrame,
|
| 40 |
+
hospital_name: str = "Unknown Hospital",
|
| 41 |
+
) -> Optional[str]:
|
| 42 |
+
"""LLM-powered intent parsing with coverage question detection."""
|
| 43 |
+
|
| 44 |
+
# Coverage questions bypass RAG (though app.py already handles these first)
|
| 45 |
+
if CoverageExplainer.identify_coverage_question(user_input):
|
| 46 |
+
return "coverage_explanation"
|
| 47 |
+
|
| 48 |
+
try:
|
| 49 |
+
rag_service = get_rag_service()
|
| 50 |
+
rag_service.initialize_vector_store(
|
| 51 |
+
services_data,
|
| 52 |
+
hospital_name,
|
| 53 |
+
force_reload=False,
|
| 54 |
+
)
|
| 55 |
+
intent = rag_service.parse_intent_with_llm(user_input, services_data)
|
| 56 |
+
return intent
|
| 57 |
+
|
| 58 |
+
except Exception as e:
|
| 59 |
+
print(f"Error in LLM parsing: {e}. Falling back to simple parsing.")
|
| 60 |
+
return parse_intent_simple(user_input, services_data)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def parse_intent(
|
| 64 |
+
user_input: str,
|
| 65 |
+
services_data: pd.DataFrame,
|
| 66 |
+
hospital_name: str = "Unknown Hospital",
|
| 67 |
+
use_llm: Optional[bool] = None,
|
| 68 |
+
) -> Optional[str]:
|
| 69 |
+
"""
|
| 70 |
+
Main intent parser - detects service requests or coverage questions.
|
| 71 |
+
Returns: service intent, "list_services", "coverage_explanation", or None.
|
| 72 |
+
Note: coverage questions are already handled in app.py before this is called.
|
| 73 |
+
"""
|
| 74 |
+
should_use_llm = use_llm if use_llm is not None else USE_LLM
|
| 75 |
+
|
| 76 |
+
if should_use_llm:
|
| 77 |
+
return parse_intent_with_llm(user_input, services_data, hospital_name)
|
| 78 |
+
else:
|
| 79 |
+
print("Using simple parsing.")
|
| 80 |
+
return parse_intent_simple(user_input, services_data)
|
thicc/ai_logic/rag_service.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
RAG (Retrieval-Augmented Generation) service for healthcare cost information.
|
| 3 |
+
|
| 4 |
+
This module provides semantic search capabilities over hospital services data
|
| 5 |
+
using vector embeddings and LLM-powered query understanding.
|
| 6 |
+
|
| 7 |
+
Supports both OpenAI and Ollama (local) LLM providers.
|
| 8 |
+
"""
|
| 9 |
+
import os
|
| 10 |
+
from typing import List, Dict, Optional, Tuple
|
| 11 |
+
import pandas as pd
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
import textwrap
|
| 14 |
+
|
| 15 |
+
from langchain_chroma import Chroma
|
| 16 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 17 |
+
from langchain_core.documents import Document
|
| 18 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 19 |
+
|
| 20 |
+
from . import config
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _get_llm():
|
| 24 |
+
"""Get the appropriate LLM based on provider configuration."""
|
| 25 |
+
if config.LLM_PROVIDER == "openai":
|
| 26 |
+
from langchain_openai import ChatOpenAI
|
| 27 |
+
return ChatOpenAI(
|
| 28 |
+
model=config.OPENAI_MODEL,
|
| 29 |
+
temperature=config.TEMPERATURE,
|
| 30 |
+
openai_api_key=config.OPENAI_API_KEY
|
| 31 |
+
)
|
| 32 |
+
elif config.LLM_PROVIDER == "ollama":
|
| 33 |
+
from langchain_ollama import ChatOllama
|
| 34 |
+
return ChatOllama(
|
| 35 |
+
model=config.OLLAMA_MODEL,
|
| 36 |
+
temperature=config.TEMPERATURE,
|
| 37 |
+
base_url=config.OLLAMA_BASE_URL
|
| 38 |
+
)
|
| 39 |
+
else:
|
| 40 |
+
raise ValueError(f"Unsupported LLM provider: {config.LLM_PROVIDER}")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _get_embeddings():
|
| 44 |
+
"""Get the appropriate embeddings based on provider configuration."""
|
| 45 |
+
if config.EMBEDDING_PROVIDER == "openai":
|
| 46 |
+
from langchain_openai import OpenAIEmbeddings
|
| 47 |
+
return OpenAIEmbeddings(
|
| 48 |
+
model=config.OPENAI_EMBEDDING_MODEL,
|
| 49 |
+
openai_api_key=config.OPENAI_API_KEY
|
| 50 |
+
)
|
| 51 |
+
elif config.EMBEDDING_PROVIDER == "ollama":
|
| 52 |
+
from langchain_ollama import OllamaEmbeddings
|
| 53 |
+
return OllamaEmbeddings(
|
| 54 |
+
model=config.OLLAMA_EMBEDDING_MODEL,
|
| 55 |
+
base_url=config.OLLAMA_BASE_URL
|
| 56 |
+
)
|
| 57 |
+
elif config.EMBEDDING_PROVIDER == "local":
|
| 58 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 59 |
+
return HuggingFaceEmbeddings(
|
| 60 |
+
model_name=config.LOCAL_EMBEDDING_MODEL,
|
| 61 |
+
model_kwargs={'device': 'cpu'},
|
| 62 |
+
encode_kwargs={'normalize_embeddings': True}
|
| 63 |
+
)
|
| 64 |
+
else:
|
| 65 |
+
raise ValueError(f"Unsupported embedding provider: {config.EMBEDDING_PROVIDER}")
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class HealthcareRAGService:
|
| 69 |
+
"""
|
| 70 |
+
RAG service for healthcare cost information retrieval and intent parsing.
|
| 71 |
+
|
| 72 |
+
This service creates vector embeddings of hospital services and uses
|
| 73 |
+
semantic search to find relevant services based on user queries.
|
| 74 |
+
|
| 75 |
+
Supports multiple LLM providers: OpenAI and Ollama (local).
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
def __init__(self, persist_directory: str = config.VECTOR_STORE_PERSIST_DIR):
|
| 79 |
+
"""
|
| 80 |
+
Initialize the RAG service.
|
| 81 |
+
|
| 82 |
+
Args:
|
| 83 |
+
persist_directory: Directory to persist the vector store
|
| 84 |
+
"""
|
| 85 |
+
config.validate_config()
|
| 86 |
+
self.persist_directory = persist_directory
|
| 87 |
+
self.embeddings = _get_embeddings()
|
| 88 |
+
self.llm = _get_llm()
|
| 89 |
+
self.vector_store: Optional[Chroma] = None
|
| 90 |
+
|
| 91 |
+
def create_documents_from_services(
|
| 92 |
+
self,
|
| 93 |
+
services_df: pd.DataFrame,
|
| 94 |
+
hospital_name: str
|
| 95 |
+
) -> List[Document]:
|
| 96 |
+
"""
|
| 97 |
+
Convert service data into LangChain Documents with metadata.
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
services_df: DataFrame containing service information
|
| 101 |
+
hospital_name: Name of the hospital
|
| 102 |
+
|
| 103 |
+
Returns:
|
| 104 |
+
List of Document objects
|
| 105 |
+
"""
|
| 106 |
+
documents = []
|
| 107 |
+
|
| 108 |
+
for _, row in services_df.iterrows():
|
| 109 |
+
content = textwrap.dedent(f"""
|
| 110 |
+
Service: {row['description']}
|
| 111 |
+
Intent: {row['intent']}
|
| 112 |
+
Hospital: {hospital_name}
|
| 113 |
+
Gross Charge: ${row['gross_charge']}
|
| 114 |
+
Negotiated Rate: ${row['negotiated_rate']}
|
| 115 |
+
|
| 116 |
+
This service provides {row['description'].lower()} at {hospital_name}.
|
| 117 |
+
Common queries: {row['intent'].replace('_', ' ')}
|
| 118 |
+
""")
|
| 119 |
+
|
| 120 |
+
metadata = {
|
| 121 |
+
"intent": row['intent'],
|
| 122 |
+
"description": row['description'],
|
| 123 |
+
"gross_charge": float(row['gross_charge']),
|
| 124 |
+
"negotiated_rate": float(row['negotiated_rate']),
|
| 125 |
+
"hospital": hospital_name,
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
documents.append(Document(page_content=content, metadata=metadata))
|
| 129 |
+
|
| 130 |
+
return documents
|
| 131 |
+
|
| 132 |
+
def initialize_vector_store(
|
| 133 |
+
self,
|
| 134 |
+
services_df: pd.DataFrame,
|
| 135 |
+
hospital_name: str,
|
| 136 |
+
force_reload: bool = False
|
| 137 |
+
):
|
| 138 |
+
"""
|
| 139 |
+
Initialize or reload the vector store with service data.
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
services_df: DataFrame containing service information
|
| 143 |
+
hospital_name: Name of the hospital
|
| 144 |
+
force_reload: If True, recreate the vector store even if it exists
|
| 145 |
+
"""
|
| 146 |
+
# Create documents from services data
|
| 147 |
+
documents = self.create_documents_from_services(services_df, hospital_name)
|
| 148 |
+
|
| 149 |
+
# Check if we should use existing vector store
|
| 150 |
+
if not force_reload and os.path.exists(self.persist_directory):
|
| 151 |
+
try:
|
| 152 |
+
self.vector_store = Chroma(
|
| 153 |
+
persist_directory=self.persist_directory,
|
| 154 |
+
embedding_function=self.embeddings
|
| 155 |
+
)
|
| 156 |
+
# Add new documents to existing store
|
| 157 |
+
self.vector_store.add_documents(documents)
|
| 158 |
+
return
|
| 159 |
+
except Exception as e:
|
| 160 |
+
print(f"Could not load existing vector store: {e}. Creating new one.")
|
| 161 |
+
|
| 162 |
+
# Create new vector store
|
| 163 |
+
self.vector_store = Chroma.from_documents(
|
| 164 |
+
documents=documents,
|
| 165 |
+
embedding=self.embeddings,
|
| 166 |
+
persist_directory=self.persist_directory
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
def parse_intent_with_llm(
|
| 170 |
+
self,
|
| 171 |
+
user_query: str,
|
| 172 |
+
available_services: pd.DataFrame
|
| 173 |
+
) -> Optional[str]:
|
| 174 |
+
"""
|
| 175 |
+
Use LLM with RAG to parse user intent and match to available services.
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
user_query: The user's natural language query
|
| 179 |
+
available_services: DataFrame of available services
|
| 180 |
+
|
| 181 |
+
Returns:
|
| 182 |
+
The matched intent string or None if no match found
|
| 183 |
+
"""
|
| 184 |
+
if self.vector_store is None:
|
| 185 |
+
raise ValueError("Vector store not initialized. Call initialize_vector_store first.")
|
| 186 |
+
|
| 187 |
+
# Perform semantic search to find relevant services
|
| 188 |
+
search_results = self.vector_store.similarity_search_with_score(user_query, k=3)
|
| 189 |
+
context_services = []
|
| 190 |
+
for doc, score in search_results:
|
| 191 |
+
context_services.append({
|
| 192 |
+
"description": doc.metadata["description"],
|
| 193 |
+
"intent": doc.metadata["intent"],
|
| 194 |
+
"relevance_score": score
|
| 195 |
+
})
|
| 196 |
+
|
| 197 |
+
# LLM Prompt for intent parsing
|
| 198 |
+
prompt_template = ChatPromptTemplate.from_messages([
|
| 199 |
+
(
|
| 200 |
+
"system",
|
| 201 |
+
textwrap.dedent("""You are a healthcare assistant helping users find medical services.
|
| 202 |
+
Your task is to understand the user's intent and match it to one of the available services.
|
| 203 |
+
|
| 204 |
+
Available services from semantic search:
|
| 205 |
+
{context}
|
| 206 |
+
|
| 207 |
+
Instructions:
|
| 208 |
+
1. Analyze the user's query carefully
|
| 209 |
+
2. Match it to the most relevant service from the context
|
| 210 |
+
3. Return ONLY the intent string (e.g., "mri_information", "xray_information")
|
| 211 |
+
4. If the user is asking for general help or a list of services, return "list_services"
|
| 212 |
+
5. If no service matches well, return "unknown"
|
| 213 |
+
|
| 214 |
+
Return only the intent string, nothing else.
|
| 215 |
+
""")
|
| 216 |
+
),
|
| 217 |
+
("user", "{query}")
|
| 218 |
+
])
|
| 219 |
+
|
| 220 |
+
context_text = "\n".join([
|
| 221 |
+
f"- {s['description']} (intent: {s['intent']}, relevance: {s['relevance_score']:.3f})"
|
| 222 |
+
for s in context_services
|
| 223 |
+
])
|
| 224 |
+
|
| 225 |
+
messages = prompt_template.format_messages(
|
| 226 |
+
context=context_text,
|
| 227 |
+
query=user_query
|
| 228 |
+
)
|
| 229 |
+
response = self.llm.invoke(messages)
|
| 230 |
+
intent = response.content.strip()
|
| 231 |
+
|
| 232 |
+
if intent == "list_services" or intent == "unknown":
|
| 233 |
+
return intent if intent == "list_services" else None
|
| 234 |
+
|
| 235 |
+
if intent in available_services["intent"].values:
|
| 236 |
+
return intent
|
| 237 |
+
|
| 238 |
+
# Try partial matching intents
|
| 239 |
+
for service_intent in available_services["intent"].values:
|
| 240 |
+
if intent.lower() in service_intent.lower() or service_intent.lower() in intent.lower():
|
| 241 |
+
return service_intent
|
| 242 |
+
|
| 243 |
+
return None
|
| 244 |
+
|
| 245 |
+
def get_conversational_response(
|
| 246 |
+
self,
|
| 247 |
+
user_query: str,
|
| 248 |
+
service_info: Optional[Dict] = None,
|
| 249 |
+
plan_name: str = "No Insurance"
|
| 250 |
+
) -> str:
|
| 251 |
+
"""
|
| 252 |
+
Generate a natural, conversational response using the LLM.
|
| 253 |
+
|
| 254 |
+
Args:
|
| 255 |
+
user_query: User's original query
|
| 256 |
+
service_info: Information about the matched service (if any)
|
| 257 |
+
plan_name: User's insurance plan name
|
| 258 |
+
|
| 259 |
+
Returns:
|
| 260 |
+
Natural language response
|
| 261 |
+
"""
|
| 262 |
+
if service_info is None:
|
| 263 |
+
prompt_template = ChatPromptTemplate.from_messages([
|
| 264 |
+
(
|
| 265 |
+
"system",
|
| 266 |
+
textwrap.dedent("""
|
| 267 |
+
You are a helpful healthcare cost estimator assistant.
|
| 268 |
+
The user asked about a service we don't have information for.
|
| 269 |
+
Politely let them know and suggest they ask about available services.
|
| 270 |
+
""")
|
| 271 |
+
),
|
| 272 |
+
("user", "{query}")
|
| 273 |
+
])
|
| 274 |
+
messages = prompt_template.format_messages(query=user_query)
|
| 275 |
+
else:
|
| 276 |
+
prompt_template = ChatPromptTemplate.from_messages([
|
| 277 |
+
(
|
| 278 |
+
"system",
|
| 279 |
+
textwrap.dedent("""You are a helpful healthcare cost assistant.
|
| 280 |
+
Provide a clear, friendly response about the service cost.
|
| 281 |
+
|
| 282 |
+
Service Information:
|
| 283 |
+
- Description: {description}
|
| 284 |
+
- Estimated Cost: ${cost:.2f}
|
| 285 |
+
- Insurance Plan: {plan}
|
| 286 |
+
- Gross Charge: ${gross_charge}
|
| 287 |
+
- Negotiated Rate: ${negotiated_rate}
|
| 288 |
+
|
| 289 |
+
Provide a natural, conversational response that includes the estimated cost and any relevant details.""")
|
| 290 |
+
),
|
| 291 |
+
("user", "{query}")
|
| 292 |
+
])
|
| 293 |
+
messages = prompt_template.format_messages(
|
| 294 |
+
description=service_info.get("description", "Unknown"),
|
| 295 |
+
cost=service_info.get("estimated_cost", 0),
|
| 296 |
+
plan=plan_name,
|
| 297 |
+
gross_charge=service_info.get("gross_charge", 0),
|
| 298 |
+
negotiated_rate=service_info.get("negotiated_rate", 0),
|
| 299 |
+
query=user_query
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
response = self.llm.invoke(messages)
|
| 303 |
+
return response.content.strip()
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def get_rag_service() -> HealthcareRAGService:
|
| 307 |
+
return HealthcareRAGService()
|
thicc/app.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from ai_logic.intent_parser import parse_intent
|
| 3 |
+
from data.data_loader import list_services, load_services_data, HOSPITALS
|
| 4 |
+
from insurance import plans, cost_estimator
|
| 5 |
+
from insurance.coverage_explainer import CoverageExplainer
|
| 6 |
+
|
| 7 |
+
# UI dropdowns
|
| 8 |
+
plan_dropdown = gr.Dropdown(
|
| 9 |
+
choices=list(plans.SAMPLE_PLANS.keys()) + ["No Insurance"],
|
| 10 |
+
value="PPO",
|
| 11 |
+
label="Select plan",
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
hospital_dropdown = gr.Dropdown(
|
| 15 |
+
choices=list(HOSPITALS.keys()),
|
| 16 |
+
value=list(HOSPITALS.keys())[0],
|
| 17 |
+
label="Select a Hospital",
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def respond(message, history, plan_name: str, hospital_name: str) -> str:
|
| 22 |
+
"""Main response function - handles cost estimation and coverage explanations."""
|
| 23 |
+
|
| 24 |
+
# --- 1) Coverage questions first ---
|
| 25 |
+
if CoverageExplainer.identify_coverage_question(message):
|
| 26 |
+
# Figure out which term (deductible, copay, coinsurance, etc.)
|
| 27 |
+
term = CoverageExplainer.get_matching_term(message)
|
| 28 |
+
|
| 29 |
+
if term:
|
| 30 |
+
# Explain the specific term (NOT the whole message)
|
| 31 |
+
explanation = CoverageExplainer.explain_term(term)
|
| 32 |
+
|
| 33 |
+
# Add plan-specific context if a sample plan is selected
|
| 34 |
+
if plan_name != "No Insurance" and plan_name in plans.SAMPLE_PLANS:
|
| 35 |
+
plan = plans.SAMPLE_PLANS[plan_name]
|
| 36 |
+
plan_details = {
|
| 37 |
+
"deductible": plan.deductible,
|
| 38 |
+
"copay": plan.copay,
|
| 39 |
+
"coinsurance": plan.coinsurance,
|
| 40 |
+
}
|
| 41 |
+
explanation += "\n\n---\n\n"
|
| 42 |
+
explanation += CoverageExplainer.format_plan_coverage_summary(
|
| 43 |
+
plan_name, plan_details
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
return explanation
|
| 47 |
+
else:
|
| 48 |
+
# If we can't match a specific term, give the full coverage explainer
|
| 49 |
+
return CoverageExplainer.explain_all_terms()
|
| 50 |
+
|
| 51 |
+
# --- 2) Service cost estimation path ---
|
| 52 |
+
hospital_data_path = HOSPITALS.get(hospital_name)
|
| 53 |
+
services_data = load_services_data(hospital_data_path)
|
| 54 |
+
requested_info = parse_intent(message, services_data, hospital_name=hospital_name)
|
| 55 |
+
|
| 56 |
+
if requested_info is None or requested_info == "list_services":
|
| 57 |
+
services_list = list_services(services_data)
|
| 58 |
+
response = (
|
| 59 |
+
"**Available services:**\n"
|
| 60 |
+
+ "\n".join(f"• {service}" for service in services_list)
|
| 61 |
+
)
|
| 62 |
+
response += (
|
| 63 |
+
"\n\n💡 **Tip**: You can ask me about insurance terms like "
|
| 64 |
+
"'What is a deductible?' or 'Explain coinsurance'."
|
| 65 |
+
)
|
| 66 |
+
return response
|
| 67 |
+
|
| 68 |
+
service_data = services_data[
|
| 69 |
+
services_data["intent"].str.contains(requested_info, case=False, na=False)
|
| 70 |
+
]
|
| 71 |
+
|
| 72 |
+
if service_data.empty:
|
| 73 |
+
return (
|
| 74 |
+
"Sorry, no information found for your request.\n\nYou can:\n"
|
| 75 |
+
"• Ask about available services\n"
|
| 76 |
+
"• Ask about insurance terms (e.g., 'What is a copay?')\n"
|
| 77 |
+
"• Get cost estimates for specific procedures"
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
service_description = service_data.iloc[0]["description"]
|
| 81 |
+
price = service_data.iloc[0]["negotiated_rate"]
|
| 82 |
+
|
| 83 |
+
# Map dropdown choice -> InsurancePlan object
|
| 84 |
+
if plan_name == "No Insurance":
|
| 85 |
+
plan = plans.NO_INSURANCE_PLAN
|
| 86 |
+
else:
|
| 87 |
+
plan = plans.SAMPLE_PLANS.get(plan_name, plans.NO_INSURANCE_PLAN)
|
| 88 |
+
|
| 89 |
+
cost = cost_estimator.estimate_cost(price, plan, deductible_met=True)
|
| 90 |
+
|
| 91 |
+
# --- 3) Format response with cost breakdown ---
|
| 92 |
+
|
| 93 |
+
# Special handling for No Insurance so messaging isn't confusing
|
| 94 |
+
if plan_name == "No Insurance":
|
| 95 |
+
response = f"""**Cost Estimate for {service_description}**
|
| 96 |
+
|
| 97 |
+
• Hospital: {hospital_name}
|
| 98 |
+
• Insurance Plan: {plan_name}
|
| 99 |
+
• Estimated Cost: **${cost:.2f}**
|
| 100 |
+
|
| 101 |
+
Because you selected **No Insurance**, this demo assumes you pay the full negotiated rate.
|
| 102 |
+
|
| 103 |
+
💡 **Understanding your cost**:
|
| 104 |
+
• Negotiated rate: ${price:.2f}
|
| 105 |
+
• Your insurance covers: $0.00
|
| 106 |
+
• You pay: ${cost:.2f}
|
| 107 |
+
|
| 108 |
+
If you want to see how deductibles, copays, and coinsurance work, switch to a sample plan above and ask something like:
|
| 109 |
+
• "What is a deductible?"
|
| 110 |
+
• "Explain coinsurance"
|
| 111 |
+
"""
|
| 112 |
+
return response
|
| 113 |
+
|
| 114 |
+
# For actual plans
|
| 115 |
+
response = f"""**Cost Estimate for {service_description}**
|
| 116 |
+
|
| 117 |
+
• Hospital: {hospital_name}
|
| 118 |
+
• Insurance Plan: {plan_name}
|
| 119 |
+
• Estimated Cost: **${cost:.2f}**
|
| 120 |
+
|
| 121 |
+
This estimate assumes your deductible has been met.
|
| 122 |
+
|
| 123 |
+
💡 **Understanding your cost**:
|
| 124 |
+
• Negotiated rate: ${price:.2f}
|
| 125 |
+
• Your insurance covers: ${price - cost:.2f}
|
| 126 |
+
• You pay: ${cost:.2f}"""
|
| 127 |
+
|
| 128 |
+
# Explain payment type
|
| 129 |
+
if plan.copay and cost == plan.copay:
|
| 130 |
+
response += (
|
| 131 |
+
f"\n\n*You're paying a fixed copay of ${plan.copay:.2f} for this service.*"
|
| 132 |
+
)
|
| 133 |
+
elif plan.coinsurance and plan.coinsurance > 0:
|
| 134 |
+
response += (
|
| 135 |
+
f"\n\n*You're paying {plan.coinsurance*100:.0f}% coinsurance "
|
| 136 |
+
f"({plan.coinsurance*100:.0f}% of ${price:.2f}).*"
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
response += (
|
| 140 |
+
"\n\n**Need help?** Ask me 'What is coinsurance?' "
|
| 141 |
+
"or any other insurance term!"
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
return response
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# Gradio interface (no examples table)
|
| 148 |
+
demo = gr.ChatInterface(
|
| 149 |
+
fn=respond,
|
| 150 |
+
title="THICC Cost Chatbot - Now with Coverage Explanations! 🏥",
|
| 151 |
+
description="""Get healthcare cost estimates and understand your insurance coverage.
|
| 152 |
+
|
| 153 |
+
**What you can ask:**
|
| 154 |
+
• Cost estimates: "How much does an MRI cost?"
|
| 155 |
+
• Coverage terms: "What is a deductible?" or "Explain coinsurance"
|
| 156 |
+
• Available services: "What services are available?"
|
| 157 |
+
""",
|
| 158 |
+
type="messages",
|
| 159 |
+
additional_inputs=[plan_dropdown, hospital_dropdown],
|
| 160 |
+
additional_inputs_accordion="Tell us about your insurance plan and hospital",
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
if __name__ == "__main__":
|
| 164 |
+
demo.launch()
|
thicc/data/data_loader.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
# Directory of this file: .../thicc/data
|
| 5 |
+
BASE_DIR = Path(__file__).resolve().parent
|
| 6 |
+
|
| 7 |
+
HOSPITALS = {
|
| 8 |
+
"Mayo Clinic": BASE_DIR / "hospitals" / "mayo_clinic_data.csv",
|
| 9 |
+
"Cleveland Clinic": BASE_DIR / "hospitals" / "cleveland_clinic_data.csv",
|
| 10 |
+
"Johns Hopkins Hospital": BASE_DIR / "hospitals" / "johns_hopkins_hospital_data.csv",
|
| 11 |
+
"Massachusetts General Hospital": BASE_DIR / "hospitals" / "massachusetts_general_hospital_data.csv",
|
| 12 |
+
"UCLA Medical Center": BASE_DIR / "hospitals" / "ucla_medical_center_data.csv",
|
| 13 |
+
"Cedars-Sinai Medical Center": BASE_DIR / "hospitals" / "cedars_sinai_medical_center_data.csv",
|
| 14 |
+
"NewYork-Presbyterian Hospital": BASE_DIR / "hospitals" / "newyork_presbyterian_hospital_data.csv",
|
| 15 |
+
"Northwestern Memorial Hospital": BASE_DIR / "hospitals" / "northwestern_memorial_hospital_data.csv",
|
| 16 |
+
"UCSF Medical Center": BASE_DIR / "hospitals" / "ucsf_medical_center_data.csv",
|
| 17 |
+
"Houston Methodist Hospital": BASE_DIR / "hospitals" / "houston_methodist_hospital_data.csv",
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
def load_services_data(path) -> pd.DataFrame:
|
| 21 |
+
"""Load price data from a CSV file."""
|
| 22 |
+
df = pd.read_csv(path)
|
| 23 |
+
return df[[
|
| 24 |
+
"intent",
|
| 25 |
+
"description",
|
| 26 |
+
"gross_charge",
|
| 27 |
+
"negotiated_rate",
|
| 28 |
+
]]
|
| 29 |
+
|
| 30 |
+
def list_services(services_data: pd.DataFrame) -> list[str]:
|
| 31 |
+
return services_data["description"].tolist()
|
thicc/data/hospitals/cedars_sinai_medical_center_data.csv
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
intent,description,gross_charge,negotiated_rate
|
| 2 |
+
"mri_information","MRI",1300,1080
|
| 3 |
+
"xray_information","X-Ray",2180,1780
|
| 4 |
+
"general_consultation","General Consultation",1620,1320
|
| 5 |
+
"neurosurgery_information","Neurosurgery",20000,17000
|
| 6 |
+
"pain_management_information","Pain Management",2500,2000
|
| 7 |
+
"endocrinology_information","Endocrinology",3700,3100
|
| 8 |
+
"bariatric_surgery_information","Bariatric Surgery",15000,12000
|
thicc/data/hospitals/cleveland_clinic_data.csv
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
intent,description,gross_charge,negotiated_rate
|
| 2 |
+
"mri_information","MRI",1320,1100
|
| 3 |
+
"xray_information","X-Ray",2200,1800
|
| 4 |
+
"general_consultation","General Consultation",1550,1250
|
| 5 |
+
"cardiac_surgery_information","Cardiac Surgery",15000,12000
|
| 6 |
+
"dialysis_information","Dialysis",4000,3200
|
thicc/data/hospitals/houston_methodist_hospital_data.csv
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
intent,description,gross_charge,negotiated_rate
|
| 2 |
+
"mri_information","MRI",1310,1090
|
| 3 |
+
"xray_information","X-Ray",2130,1730
|
| 4 |
+
"general_consultation","General Consultation",1610,1310
|
| 5 |
+
"transplant_surgery_information","Transplant Surgery",48000,40000
|
| 6 |
+
"wound_care_information","Wound Care",1800,1400
|
| 7 |
+
"hyperbaric_therapy_information","Hyperbaric Therapy",3500,3000
|
| 8 |
+
"occupational_therapy_information","Occupational Therapy",2100,1700
|
thicc/data/hospitals/johns_hopkins_hospital_data.csv
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
intent,description,gross_charge,negotiated_rate
|
| 2 |
+
"mri_information","MRI",1400,1150
|
| 3 |
+
"xray_information","X-Ray",2050,1600
|
| 4 |
+
"general_consultation","General Consultation",1700,1400
|
| 5 |
+
"organ_transplant_information","Organ Transplant",50000,42000
|
| 6 |
+
"pediatric_care_information","Pediatric Care",3000,2500
|
| 7 |
+
"sleep_disorder_clinic_information","Sleep Disorder Clinic",2100,1700
|
| 8 |
+
"infectious_disease_information","Infectious Disease",4200,3500
|
thicc/data/hospitals/massachusetts_general_hospital_data.csv
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
intent,description,gross_charge,negotiated_rate
|
| 2 |
+
"mri_information","MRI",1280,1050
|
| 3 |
+
"xray_information","X-Ray",2150,1750
|
| 4 |
+
"general_consultation","General Consultation",1650,1350
|
| 5 |
+
"fertility_treatment_information","Fertility Treatment",12000,10000
|
| 6 |
+
"oncology_information","Oncology",8000,7000
|
| 7 |
+
"burn_unit_information","Burn Unit",6000,5000
|
| 8 |
+
"speech_therapy_information","Speech Therapy",1800,1400
|
thicc/data/hospitals/mayo_clinic_data.csv
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
intent,description,gross_charge,negotiated_rate
|
| 2 |
+
"mri_information","MRI",1234,1000
|
| 3 |
+
"xray_information","X-Ray",2100,1700
|
| 4 |
+
"general_consultation","General Consultation",1600,1300
|
| 5 |
+
"sleep_medicine_information","Sleep Medicine",2200,1800
|
| 6 |
+
"rheumatology_information","Rheumatology",3500,2900
|
| 7 |
+
"geriatrics_information","Geriatrics",2500,2000
|
| 8 |
+
"nutrition_counseling_information","Nutrition Counseling",1200,900
|
thicc/data/hospitals/newyork_presbyterian_hospital_data.csv
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
intent,description,gross_charge,negotiated_rate
|
| 2 |
+
"mri_information","MRI",1370,1130
|
| 3 |
+
"xray_information","X-Ray",2120,1720
|
| 4 |
+
"general_consultation","General Consultation",1680,1380
|
| 5 |
+
"psychiatric_care_information","Psychiatric Care",4000,3500
|
| 6 |
+
"rehabilitation_information","Rehabilitation",2200,1800
|
| 7 |
+
"neonatal_care_information","Neonatal Care",5000,4200
|
| 8 |
+
"speech_pathology_information","Speech Pathology",2100,1700
|
thicc/data/hospitals/northwestern_memorial_hospital_data.csv
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
intent,description,gross_charge,negotiated_rate
|
| 2 |
+
"mri_information","MRI",1290,1060
|
| 3 |
+
"xray_information","X-Ray",2170,1770
|
| 4 |
+
"general_consultation","General Consultation",1630,1330
|
| 5 |
+
"dermatology_information","Dermatology",1200,900
|
| 6 |
+
"allergy_treatment_information","Allergy Treatment",1100,850
|
| 7 |
+
"urology_information","Urology",2700,2200
|
| 8 |
+
"pulmonology_information","Pulmonology",3500,2900
|
thicc/data/hospitals/ucla_medical_center_data.csv
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
intent,description,gross_charge,negotiated_rate
|
| 2 |
+
"mri_information","MRI",1350,1120
|
| 3 |
+
"xray_information","X-Ray",2250,1850
|
| 4 |
+
"general_consultation","General Consultation",1580,1280
|
| 5 |
+
"sports_medicine_information","Sports Medicine",3500,3000
|
| 6 |
+
"plastic_surgery_information","Plastic Surgery",9000,7500
|
| 7 |
+
"immunology_information","Immunology",3200,2700
|
| 8 |
+
"gastroenterology_information","Gastroenterology",4100,3500
|
thicc/data/hospitals/ucsf_medical_center_data.csv
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
intent,description,gross_charge,negotiated_rate
|
| 2 |
+
"mri_information","MRI",1390,1160
|
| 3 |
+
"xray_information","X-Ray",2190,1790
|
| 4 |
+
"general_consultation","General Consultation",1690,1390
|
| 5 |
+
"aids_hiv_care_information","AIDS/HIV Care",7000,6000
|
| 6 |
+
"genetic_counseling_information","Genetic Counseling",2500,2000
|
| 7 |
+
"transgender_health_information","Transgender Health",6000,5000
|
| 8 |
+
"integrative_medicine_information","Integrative Medicine",3200,2700
|
thicc/insurance/cost_estimator.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .plans import InsurancePlan
|
| 2 |
+
|
| 3 |
+
def estimate_cost(price: float, plan: InsurancePlan | None = None, deductible_met: bool = False) -> float:
|
| 4 |
+
price = float(price)
|
| 5 |
+
|
| 6 |
+
if plan is None or getattr(plan, "plan_name", "") == "No Insurance":
|
| 7 |
+
return price # No insurance, full price
|
| 8 |
+
|
| 9 |
+
if deductible_met:
|
| 10 |
+
# If deductible is met, only copay and coinsurance apply
|
| 11 |
+
return plan.copay or (price * plan.coinsurance)
|
| 12 |
+
|
| 13 |
+
return min(price, plan.deductible) + plan.copay
|
thicc/insurance/coverage_explainer.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Coverage Explanations Module - Provides insurance term explanations.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
class CoverageExplainer:
|
| 6 |
+
"""Explains insurance coverage terms in user-friendly language."""
|
| 7 |
+
|
| 8 |
+
COVERAGE_TERMS = {
|
| 9 |
+
"deductible": {
|
| 10 |
+
"brief": "The amount you pay before insurance starts covering costs",
|
| 11 |
+
"detailed": """A deductible is the amount you must pay out-of-pocket for covered healthcare services before your insurance plan starts to pay.
|
| 12 |
+
|
| 13 |
+
**Example**: If you have a $1,000 deductible:
|
| 14 |
+
• You pay the first $1,000 of covered services yourself
|
| 15 |
+
• After meeting your deductible, you typically pay only copays or coinsurance
|
| 16 |
+
• Deductibles reset yearly
|
| 17 |
+
|
| 18 |
+
**Important**: Some services like preventive care may be covered before you meet your deductible.""",
|
| 19 |
+
"keywords": ["deductible", "deductibles", "yearly amount", "before insurance pays"],
|
| 20 |
+
},
|
| 21 |
+
|
| 22 |
+
"copay": {
|
| 23 |
+
"brief": "A fixed amount you pay for a covered service",
|
| 24 |
+
"detailed": """A copay (copayment) is a fixed amount you pay for a covered healthcare service, usually at the time of service.
|
| 25 |
+
|
| 26 |
+
**Example**:
|
| 27 |
+
• Doctor visit: $25 copay
|
| 28 |
+
• Specialist visit: $50 copay
|
| 29 |
+
• Prescription: $10 copay
|
| 30 |
+
|
| 31 |
+
**Key Points**:
|
| 32 |
+
• Copays are typically the same regardless of the actual cost
|
| 33 |
+
• You usually pay copays even after meeting your deductible
|
| 34 |
+
• Different services have different copay amounts""",
|
| 35 |
+
"keywords": ["copay", "copayment", "fixed amount", "flat fee"],
|
| 36 |
+
},
|
| 37 |
+
|
| 38 |
+
"coinsurance": {
|
| 39 |
+
"brief": "Your percentage share of costs after deductible",
|
| 40 |
+
"detailed": """Coinsurance is your share of the costs of a covered healthcare service, calculated as a percentage of the allowed amount for the service.
|
| 41 |
+
|
| 42 |
+
**Example**: With 20% coinsurance:
|
| 43 |
+
• Insurance pays 80% of covered costs
|
| 44 |
+
• You pay 20% of covered costs
|
| 45 |
+
• This applies AFTER you meet your deductible
|
| 46 |
+
|
| 47 |
+
**Real Scenario**:
|
| 48 |
+
MRI costs $1,000 (after deductible met)
|
| 49 |
+
• Insurance pays: $800 (80%)
|
| 50 |
+
• You pay: $200 (20%)""",
|
| 51 |
+
"keywords": ["coinsurance", "percentage", "percent of cost", "cost sharing", "80/20", "70/30"],
|
| 52 |
+
},
|
| 53 |
+
|
| 54 |
+
"out_of_network": {
|
| 55 |
+
"brief": "Higher costs for providers not in your insurance network",
|
| 56 |
+
"detailed": """Out-of-network refers to healthcare providers who haven't contracted with your insurance company to provide services at negotiated rates.
|
| 57 |
+
|
| 58 |
+
**Penalties and Higher Costs**:
|
| 59 |
+
• Higher deductibles (often double in-network amounts)
|
| 60 |
+
• Higher coinsurance (40-50% vs 20-30%)
|
| 61 |
+
• No negotiated rates (you may pay full price)
|
| 62 |
+
• Balance billing (provider can bill you the difference)
|
| 63 |
+
|
| 64 |
+
**Example Cost Difference**:
|
| 65 |
+
Same procedure:
|
| 66 |
+
• In-network: You pay $500 (20% coinsurance)
|
| 67 |
+
• Out-of-network: You pay $2,000 (50% coinsurance + balance billing)
|
| 68 |
+
|
| 69 |
+
**Tip**: Always check if a provider is in-network before receiving care, except in emergencies.""",
|
| 70 |
+
"keywords": [
|
| 71 |
+
"out of network",
|
| 72 |
+
"out-of-network",
|
| 73 |
+
"network penalties",
|
| 74 |
+
"provider network",
|
| 75 |
+
"in-network",
|
| 76 |
+
"network",
|
| 77 |
+
],
|
| 78 |
+
},
|
| 79 |
+
|
| 80 |
+
"out_of_pocket_maximum": {
|
| 81 |
+
"brief": "The most you'll pay in a year for covered services",
|
| 82 |
+
"detailed": """The out-of-pocket maximum is the most you have to pay for covered services in a plan year. After you reach this amount, your insurance pays 100% of covered services.
|
| 83 |
+
|
| 84 |
+
**What Counts**:
|
| 85 |
+
✓ Deductibles
|
| 86 |
+
✓ Copayments
|
| 87 |
+
✓ Coinsurance
|
| 88 |
+
|
| 89 |
+
**What Doesn't Count**:
|
| 90 |
+
✗ Monthly premiums
|
| 91 |
+
✗ Out-of-network costs (usually)
|
| 92 |
+
✗ Non-covered services
|
| 93 |
+
|
| 94 |
+
**Example**: $8,000 out-of-pocket maximum
|
| 95 |
+
Once you've paid $8,000 in deductibles, copays, and coinsurance, your insurance covers 100% for the rest of the year.""",
|
| 96 |
+
"keywords": [
|
| 97 |
+
"out of pocket maximum",
|
| 98 |
+
"out-of-pocket maximum",
|
| 99 |
+
"max",
|
| 100 |
+
"maximum",
|
| 101 |
+
"yearly limit",
|
| 102 |
+
"annual limit",
|
| 103 |
+
"oop max",
|
| 104 |
+
],
|
| 105 |
+
},
|
| 106 |
+
|
| 107 |
+
"premium": {
|
| 108 |
+
"brief": "Monthly payment to maintain insurance coverage",
|
| 109 |
+
"detailed": """Your premium is the amount you pay for your health insurance every month to maintain coverage, regardless of whether you use services.
|
| 110 |
+
|
| 111 |
+
**Key Points**:
|
| 112 |
+
• Due monthly whether you use healthcare or not
|
| 113 |
+
• Doesn't count toward deductible or out-of-pocket maximum
|
| 114 |
+
• Higher premiums often mean lower deductibles/copays
|
| 115 |
+
• Employer may pay part of your premium
|
| 116 |
+
|
| 117 |
+
**Example Monthly Premiums**:
|
| 118 |
+
• Individual: $450/month
|
| 119 |
+
• Family: $1,200/month""",
|
| 120 |
+
"keywords": ["premium", "monthly payment", "monthly cost", "insurance payment"],
|
| 121 |
+
},
|
| 122 |
+
|
| 123 |
+
"prior_authorization": {
|
| 124 |
+
"brief": "Insurance approval needed before certain services",
|
| 125 |
+
"detailed": """Prior authorization (preauthorization) means your insurance company must approve a service before you receive it for the service to be covered.
|
| 126 |
+
|
| 127 |
+
**Common Services Requiring Authorization**:
|
| 128 |
+
• MRI/CT scans
|
| 129 |
+
• Surgery
|
| 130 |
+
• Expensive medications
|
| 131 |
+
• Specialist referrals (HMO plans)
|
| 132 |
+
|
| 133 |
+
**Important**:
|
| 134 |
+
• Without authorization, insurance may deny coverage
|
| 135 |
+
• Your doctor typically handles the authorization
|
| 136 |
+
• Can take days to weeks for approval
|
| 137 |
+
• Emergency services don't require prior authorization""",
|
| 138 |
+
"keywords": [
|
| 139 |
+
"prior authorization",
|
| 140 |
+
"preauthorization",
|
| 141 |
+
"pre-approval",
|
| 142 |
+
"authorization",
|
| 143 |
+
"approval needed",
|
| 144 |
+
],
|
| 145 |
+
},
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
@classmethod
|
| 149 |
+
def explain_term(cls, term: str) -> str | None:
|
| 150 |
+
"""Get detailed explanation for a specific coverage term."""
|
| 151 |
+
term_lower = term.lower().strip()
|
| 152 |
+
|
| 153 |
+
for key, info in cls.COVERAGE_TERMS.items():
|
| 154 |
+
if key in term_lower or any(keyword in term_lower for keyword in info["keywords"]):
|
| 155 |
+
return info["detailed"]
|
| 156 |
+
|
| 157 |
+
return None
|
| 158 |
+
|
| 159 |
+
@classmethod
|
| 160 |
+
def get_brief_explanation(cls, term: str) -> str | None:
|
| 161 |
+
"""Get brief explanation for a coverage term."""
|
| 162 |
+
term_lower = term.lower().strip()
|
| 163 |
+
|
| 164 |
+
for key, info in cls.COVERAGE_TERMS.items():
|
| 165 |
+
if key in term_lower or any(keyword in term_lower for keyword in info["keywords"]):
|
| 166 |
+
return info["brief"]
|
| 167 |
+
|
| 168 |
+
return None
|
| 169 |
+
|
| 170 |
+
@classmethod
|
| 171 |
+
def explain_all_terms(cls) -> str:
|
| 172 |
+
"""Return explanations for all coverage terms."""
|
| 173 |
+
explanations = ["**Understanding Your Insurance Coverage**\n"]
|
| 174 |
+
|
| 175 |
+
for term, info in cls.COVERAGE_TERMS.items():
|
| 176 |
+
title = term.replace("_", " ").title()
|
| 177 |
+
explanations.append(f"**{title}**")
|
| 178 |
+
explanations.append(info["detailed"])
|
| 179 |
+
explanations.append("")
|
| 180 |
+
|
| 181 |
+
return "\n".join(explanations)
|
| 182 |
+
|
| 183 |
+
@classmethod
|
| 184 |
+
def identify_coverage_question(cls, user_input: str) -> bool:
|
| 185 |
+
"""Check if user is asking about coverage terms."""
|
| 186 |
+
user_input_lower = user_input.lower()
|
| 187 |
+
|
| 188 |
+
# General coverage question patterns
|
| 189 |
+
general_terms = [
|
| 190 |
+
"what is",
|
| 191 |
+
"what's",
|
| 192 |
+
"explain",
|
| 193 |
+
"how does",
|
| 194 |
+
"how do",
|
| 195 |
+
"tell me about",
|
| 196 |
+
"help me understand",
|
| 197 |
+
"coverage",
|
| 198 |
+
"insurance terms",
|
| 199 |
+
"insurance work",
|
| 200 |
+
]
|
| 201 |
+
|
| 202 |
+
# Check for general questions with coverage terms
|
| 203 |
+
if any(term in user_input_lower for term in general_terms):
|
| 204 |
+
for term_info in cls.COVERAGE_TERMS.values():
|
| 205 |
+
if any(keyword in user_input_lower for keyword in term_info["keywords"]):
|
| 206 |
+
return True
|
| 207 |
+
|
| 208 |
+
# Direct term mentions
|
| 209 |
+
for term_info in cls.COVERAGE_TERMS.values():
|
| 210 |
+
if any(keyword in user_input_lower for keyword in term_info["keywords"]):
|
| 211 |
+
return True
|
| 212 |
+
|
| 213 |
+
return False
|
| 214 |
+
|
| 215 |
+
@classmethod
|
| 216 |
+
def get_matching_term(cls, user_input: str) -> str | None:
|
| 217 |
+
"""Extract which coverage term the user is asking about."""
|
| 218 |
+
user_input_lower = user_input.lower()
|
| 219 |
+
|
| 220 |
+
for term_key, term_info in cls.COVERAGE_TERMS.items():
|
| 221 |
+
for keyword in term_info["keywords"]:
|
| 222 |
+
if keyword in user_input_lower:
|
| 223 |
+
return term_key
|
| 224 |
+
|
| 225 |
+
return None
|
| 226 |
+
|
| 227 |
+
@classmethod
|
| 228 |
+
def format_plan_coverage_summary(cls, plan_name: str, plan_details: dict) -> str:
|
| 229 |
+
"""Format a summary of coverage for a specific plan."""
|
| 230 |
+
deductible = plan_details.get("deductible", 0)
|
| 231 |
+
copay = plan_details.get("copay", 0)
|
| 232 |
+
coinsurance = plan_details.get("coinsurance", 0) * 100
|
| 233 |
+
|
| 234 |
+
return f"""**Your {plan_name} Plan Coverage**
|
| 235 |
+
|
| 236 |
+
**Deductible**: ${deductible:,.0f}
|
| 237 |
+
• You pay this amount first before insurance helps
|
| 238 |
+
|
| 239 |
+
**Copay**: ${copay:.0f}
|
| 240 |
+
• Fixed amount you pay per visit
|
| 241 |
+
|
| 242 |
+
**Coinsurance**: {coinsurance:.0f}%
|
| 243 |
+
• Your percentage share after deductible
|
| 244 |
+
|
| 245 |
+
**How it works**:
|
| 246 |
+
1. You pay 100% until you meet your ${deductible:,.0f} deductible
|
| 247 |
+
2. Then you pay ${copay:.0f} copays or {coinsurance:.0f}% coinsurance
|
| 248 |
+
3. Insurance covers the rest up to allowed amounts"""
|
thicc/insurance/plans.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class InsurancePlan:
|
| 2 |
+
def __init__(self, plan_name, copay, deductible, coinsurance):
|
| 3 |
+
self.plan_name = plan_name
|
| 4 |
+
self.copay = copay
|
| 5 |
+
self.deductible = deductible
|
| 6 |
+
self.coinsurance = coinsurance
|
| 7 |
+
|
| 8 |
+
SAMPLE_PLANS = {
|
| 9 |
+
"HMO": InsurancePlan("HMO", 25, 0, 0.0),
|
| 10 |
+
"PPO": InsurancePlan("PPO", 20, 500, 0.2),
|
| 11 |
+
"HDHP": InsurancePlan("HDHP", 20, 1500, 0.1),
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
NO_INSURANCE_PLAN = InsurancePlan("No Insurance", 0, 0, 1.0)
|