lloza7 commited on
Commit
fee6d7b
·
verified ·
1 Parent(s): 84580c9

Upload 19 files

Browse files
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)