Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
#!/usr/bin/env python3
|
2 |
"""
|
3 |
-
|
4 |
-
|
5 |
"""
|
6 |
|
7 |
import os
|
@@ -14,48 +14,17 @@ from dataclasses import dataclass, field
|
|
14 |
from enum import Enum
|
15 |
import requests
|
16 |
import gradio as gr
|
17 |
-
from datetime import datetime
|
18 |
-
|
19 |
|
20 |
-
# Set up environment
|
21 |
os.environ['OPENROUTER_API_KEY'] = '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'
|
22 |
-
os.environ['DATA_SOURCE'] = 'local_csv'
|
23 |
-
os.environ['DATA_PATH'] = 'data/sample_codes.csv'
|
24 |
|
25 |
# Configure logging
|
26 |
-
logging.basicConfig(
|
27 |
-
level=logging.INFO,
|
28 |
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
29 |
-
)
|
30 |
logger = logging.getLogger(__name__)
|
31 |
|
32 |
-
# ============= Data Classes
|
33 |
-
|
34 |
-
class ExplanationLength(Enum):
|
35 |
-
SHORT = "short"
|
36 |
-
DETAILED = "detailed"
|
37 |
-
EXTRA = "extra"
|
38 |
-
|
39 |
-
class CodeType(Enum):
|
40 |
-
CPT = "CPT"
|
41 |
-
HCPCS = "HCPCS"
|
42 |
-
ICD10 = "ICD-10"
|
43 |
-
DRG = "DRG"
|
44 |
-
UNKNOWN = "UNKNOWN"
|
45 |
-
|
46 |
-
class DataSource(Enum):
|
47 |
-
LOCAL_CSV = "local_csv"
|
48 |
-
LOCAL_JSON = "local_json"
|
49 |
-
API = "api"
|
50 |
-
|
51 |
-
@dataclass
|
52 |
-
class ConversationContext:
|
53 |
-
current_code: Optional[str] = None
|
54 |
-
current_code_type: Optional[CodeType] = None
|
55 |
-
current_code_info: Optional['CodeInfo'] = None
|
56 |
-
conversation_history: List[Dict[str, str]] = field(default_factory=list)
|
57 |
-
last_explanation_length: Optional[ExplanationLength] = None
|
58 |
-
turn_count: int = 0
|
59 |
|
60 |
@dataclass
|
61 |
class CodeInfo:
|
@@ -63,222 +32,139 @@ class CodeInfo:
|
|
63 |
description: str
|
64 |
code_type: str
|
65 |
additional_info: Optional[str] = None
|
66 |
-
effective_date: Optional[str] = None
|
67 |
category: Optional[str] = None
|
68 |
-
|
69 |
-
def to_dict(self) -> Dict[str, Any]:
|
70 |
-
return {
|
71 |
-
'code': self.code,
|
72 |
-
'description': self.description,
|
73 |
-
'code_type': self.code_type,
|
74 |
-
'additional_info': self.additional_info,
|
75 |
-
'effective_date': self.effective_date,
|
76 |
-
'category': self.category
|
77 |
-
}
|
78 |
|
79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
80 |
|
81 |
-
class
|
82 |
-
"""Loader for HCPCS/CPT/ICD-10/DRG codes"""
|
83 |
-
|
84 |
def __init__(self):
|
85 |
-
self.
|
86 |
-
self.data_path = os.getenv('DATA_PATH', 'data/sample_codes.csv')
|
87 |
-
self.codes_cache: Dict[str, CodeInfo] = {}
|
88 |
-
self.cache_timestamp: Optional[datetime] = None
|
89 |
-
|
90 |
-
def load_data(self) -> bool:
|
91 |
-
"""Load data from configured source"""
|
92 |
-
try:
|
93 |
-
return self._load_sample_data()
|
94 |
-
except Exception as e:
|
95 |
-
logger.error(f"Failed to load data: {e}")
|
96 |
-
return self._load_sample_data()
|
97 |
-
|
98 |
-
def _load_sample_data(self) -> bool:
|
99 |
-
"""Load sample data as fallback"""
|
100 |
-
logger.info("Loading sample data")
|
101 |
-
|
102 |
-
sample_codes = {
|
103 |
'A0429': CodeInfo(
|
104 |
code='A0429',
|
105 |
description='Ambulance service, basic life support, emergency transport (BLS-emergency)',
|
106 |
code_type='HCPCS',
|
107 |
-
additional_info='Ground ambulance emergency transport with BLS level care',
|
108 |
category='Ambulance Services'
|
109 |
),
|
110 |
'A0428': CodeInfo(
|
111 |
code='A0428',
|
112 |
-
description='Ambulance service, basic life support, non-emergency transport
|
113 |
code_type='HCPCS',
|
114 |
-
additional_info='
|
115 |
category='Ambulance Services'
|
116 |
),
|
117 |
'99213': CodeInfo(
|
118 |
code='99213',
|
119 |
-
description='Office
|
120 |
code_type='CPT',
|
121 |
-
additional_info='Typically 20-29 minutes
|
122 |
category='E&M Services'
|
123 |
),
|
124 |
'99214': CodeInfo(
|
125 |
code='99214',
|
126 |
-
description='Office
|
127 |
code_type='CPT',
|
128 |
-
additional_info='Typically 30-39 minutes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
129 |
category='E&M Services'
|
130 |
),
|
131 |
'93000': CodeInfo(
|
132 |
code='93000',
|
133 |
-
description='Electrocardiogram
|
134 |
code_type='CPT',
|
135 |
-
additional_info='Complete ECG
|
136 |
category='Cardiovascular'
|
137 |
),
|
138 |
'DRG470': CodeInfo(
|
139 |
code='DRG470',
|
140 |
-
description='Major hip and knee joint replacement
|
141 |
code_type='DRG',
|
142 |
-
additional_info='Medicare
|
143 |
category='Orthopedic'
|
144 |
),
|
145 |
'Z79.899': CodeInfo(
|
146 |
code='Z79.899',
|
147 |
-
description='Other long term
|
148 |
code_type='ICD-10',
|
149 |
-
additional_info='
|
150 |
-
category='
|
151 |
),
|
152 |
'E1399': CodeInfo(
|
153 |
code='E1399',
|
154 |
description='Durable medical equipment, miscellaneous',
|
155 |
code_type='HCPCS',
|
156 |
-
additional_info='DME not
|
157 |
-
category='
|
158 |
-
),
|
159 |
-
'G0442': CodeInfo(
|
160 |
-
code='G0442',
|
161 |
-
description='Annual alcohol screening, 5 to 10 minutes',
|
162 |
-
code_type='HCPCS',
|
163 |
-
additional_info='Medicare-covered annual alcohol misuse screening',
|
164 |
-
category='Preventive Services'
|
165 |
-
),
|
166 |
-
'90837': CodeInfo(
|
167 |
-
code='90837',
|
168 |
-
description='Psychotherapy, 60 minutes with patient',
|
169 |
-
code_type='CPT',
|
170 |
-
additional_info='Individual psychotherapy session, 53-60 minutes',
|
171 |
-
category='Psychiatric Services'
|
172 |
),
|
173 |
'J3420': CodeInfo(
|
174 |
code='J3420',
|
175 |
-
description='
|
176 |
code_type='HCPCS',
|
177 |
-
additional_info='
|
178 |
-
category='
|
179 |
),
|
180 |
'80053': CodeInfo(
|
181 |
code='80053',
|
182 |
description='Comprehensive metabolic panel',
|
183 |
code_type='CPT',
|
184 |
-
additional_info='14 tests including glucose, kidney
|
185 |
category='Laboratory'
|
186 |
),
|
187 |
-
'A0425': CodeInfo(
|
188 |
-
code='A0425',
|
189 |
-
description='Ground mileage, per statute mile',
|
190 |
-
code_type='HCPCS',
|
191 |
-
additional_info='Ambulance mileage for ground transport',
|
192 |
-
category='Ambulance Services'
|
193 |
-
),
|
194 |
-
'99215': CodeInfo(
|
195 |
-
code='99215',
|
196 |
-
description='Office visit, established patient, high complexity',
|
197 |
-
code_type='CPT',
|
198 |
-
additional_info='Typically 40-54 minutes with patient',
|
199 |
-
category='E&M Services'
|
200 |
-
),
|
201 |
'70450': CodeInfo(
|
202 |
code='70450',
|
203 |
-
description='CT
|
204 |
code_type='CPT',
|
205 |
-
additional_info='Computed tomography of head
|
206 |
category='Radiology'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
207 |
)
|
208 |
}
|
209 |
-
|
210 |
-
self.codes_cache = sample_codes
|
211 |
-
self.cache_timestamp = datetime.now()
|
212 |
-
return True
|
213 |
|
214 |
-
def
|
215 |
-
"""Look up a single code"""
|
216 |
code = code.strip().upper()
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
# Try with DRG prefix
|
222 |
-
if code.startswith('DRG') and len(code) == 6:
|
223 |
-
drg_code = code[3:]
|
224 |
-
if drg_code in self.codes_cache:
|
225 |
-
return self.codes_cache[drg_code]
|
226 |
-
|
227 |
-
# Try adding DRG prefix
|
228 |
-
if len(code) == 3 and code.isdigit():
|
229 |
drg_code = f"DRG{code}"
|
230 |
-
if drg_code in self.
|
231 |
-
return self.
|
232 |
-
|
233 |
return None
|
234 |
|
235 |
-
def
|
236 |
-
"""
|
237 |
-
|
238 |
-
'total_codes': len(self.codes_cache),
|
239 |
-
'source': self.data_source.value,
|
240 |
-
'codes_by_type': {}
|
241 |
-
}
|
242 |
-
|
243 |
-
for code_info in self.codes_cache.values():
|
244 |
-
code_type = code_info.code_type
|
245 |
-
stats['codes_by_type'][code_type] = stats['codes_by_type'].get(code_type, 0) + 1
|
246 |
-
|
247 |
-
return stats
|
248 |
-
|
249 |
-
# ============= Code Classifier =============
|
250 |
-
|
251 |
-
class CodeClassifier:
|
252 |
-
"""Classifies and validates healthcare billing codes"""
|
253 |
-
|
254 |
-
PATTERNS = {
|
255 |
-
CodeType.CPT: re.compile(r'^[0-9]{5}$'),
|
256 |
-
CodeType.HCPCS: re.compile(r'^[A-V][0-9]{4}$', re.IGNORECASE),
|
257 |
-
CodeType.ICD10: re.compile(r'^[A-Z][0-9]{2}\.?[0-9]{0,3}$', re.IGNORECASE),
|
258 |
-
CodeType.DRG: re.compile(r'^(?:DRG)?[0-9]{3}$', re.IGNORECASE),
|
259 |
-
}
|
260 |
-
|
261 |
-
@classmethod
|
262 |
-
def classify_code(cls, code: str) -> CodeType:
|
263 |
-
"""Classify a code based on its format"""
|
264 |
-
code = code.strip().upper()
|
265 |
-
|
266 |
-
if code.startswith('DRG'):
|
267 |
-
code = code[3:]
|
268 |
-
if cls.PATTERNS[CodeType.DRG].match(f"DRG{code}"):
|
269 |
-
return CodeType.DRG
|
270 |
-
|
271 |
-
for code_type, pattern in cls.PATTERNS.items():
|
272 |
-
if pattern.match(code):
|
273 |
-
return code_type
|
274 |
-
|
275 |
-
return CodeType.UNKNOWN
|
276 |
-
|
277 |
-
@classmethod
|
278 |
-
def extract_codes_from_text(cls, text: str) -> List[Tuple[str, CodeType]]:
|
279 |
-
"""Extract potential codes from free-form text"""
|
280 |
-
codes = []
|
281 |
-
|
282 |
patterns = [
|
283 |
r'\b([A-V][0-9]{4})\b', # HCPCS
|
284 |
r'\b([0-9]{5})\b', # CPT
|
@@ -287,300 +173,466 @@ class CodeClassifier:
|
|
287 |
]
|
288 |
|
289 |
for pattern in patterns:
|
290 |
-
matches = re.findall(pattern, text
|
291 |
for match in matches:
|
292 |
-
|
293 |
-
|
294 |
-
codes.append((match.upper(), code_type))
|
295 |
|
296 |
-
return
|
297 |
|
298 |
-
# =============
|
299 |
|
300 |
-
class
|
301 |
-
"""Client for OpenRouter LLM API"""
|
302 |
-
|
303 |
def __init__(self):
|
304 |
-
self.api_key =
|
305 |
-
self.
|
306 |
-
self.
|
307 |
-
self.temperature = 0.3
|
308 |
|
309 |
self.headers = {
|
310 |
'Authorization': f'Bearer {self.api_key}',
|
311 |
'Content-Type': 'application/json',
|
312 |
'HTTP-Referer': 'https://huggingface.co',
|
313 |
-
'X-Title': '
|
314 |
}
|
315 |
|
316 |
-
def
|
317 |
-
|
318 |
-
|
319 |
-
code_info: Optional[CodeInfo],
|
320 |
-
length: ExplanationLength,
|
321 |
-
context: Optional[ConversationContext] = None) -> str:
|
322 |
-
"""Generate explanation using LLM"""
|
323 |
-
|
324 |
-
if not code_info:
|
325 |
-
return f"I couldn't find code {code} in our database. This might be an invalid code or one that's not in our current dataset."
|
326 |
-
|
327 |
-
prompt = self._build_prompt(code, code_type, code_info, length, context)
|
328 |
-
|
329 |
-
max_tokens = {
|
330 |
-
ExplanationLength.SHORT: 150,
|
331 |
-
ExplanationLength.DETAILED: 300,
|
332 |
-
ExplanationLength.EXTRA: 500
|
333 |
-
}.get(length, 200)
|
334 |
-
|
335 |
-
payload = {
|
336 |
-
'model': self.model,
|
337 |
-
'messages': [
|
338 |
-
{'role': 'system', 'content': self._get_system_prompt()},
|
339 |
-
{'role': 'user', 'content': prompt}
|
340 |
-
],
|
341 |
-
'temperature': self.temperature,
|
342 |
-
'max_tokens': max_tokens
|
343 |
-
}
|
344 |
|
345 |
-
|
346 |
-
|
347 |
-
f'{self.base_url}/chat/completions',
|
348 |
-
headers=self.headers,
|
349 |
-
json=payload,
|
350 |
-
timeout=30
|
351 |
-
)
|
352 |
-
response.raise_for_status()
|
353 |
-
|
354 |
-
result = response.json()
|
355 |
-
explanation = result['choices'][0]['message']['content']
|
356 |
-
return explanation
|
357 |
-
|
358 |
-
except Exception as e:
|
359 |
-
logger.error(f"OpenRouter API error: {e}")
|
360 |
-
return self._get_fallback_explanation(code, code_type, code_info, length)
|
361 |
-
|
362 |
-
def _get_system_prompt(self) -> str:
|
363 |
-
return """You are a healthcare billing expert assistant. Provide accurate,
|
364 |
-
fact-based explanations of healthcare billing codes. Only provide information
|
365 |
-
you are certain about. Be concise but informative. Use plain language."""
|
366 |
-
|
367 |
-
def _build_prompt(self, code: str, code_type: CodeType, code_info: CodeInfo,
|
368 |
-
length: ExplanationLength, context: Optional[ConversationContext]) -> str:
|
369 |
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
Additional Information: {code_info.additional_info or 'N/A'}
|
374 |
-
"""
|
375 |
|
376 |
-
|
377 |
-
base_prompt += "\nProvide a SHORT explanation (3-4 lines) in simple terms."
|
378 |
-
elif length == ExplanationLength.DETAILED:
|
379 |
-
base_prompt += "\nProvide a DETAILED explanation including what it covers and when it's typically used."
|
380 |
-
elif length == ExplanationLength.EXTRA:
|
381 |
-
base_prompt += "\nProvide EXTENDED details with sections for: typical use cases, coverage warnings, and important notes."
|
382 |
|
383 |
-
return
|
|
|
|
|
|
|
|
|
384 |
|
385 |
-
def
|
386 |
-
|
387 |
-
|
388 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
389 |
else:
|
390 |
-
|
391 |
-
if code_info.additional_info:
|
392 |
-
explanation += f"\n\nAdditional Details: {code_info.additional_info}"
|
393 |
-
return explanation
|
394 |
-
|
395 |
-
# ============= Main Chatbot Class =============
|
396 |
-
|
397 |
-
class HealthcareBillingChatbot:
|
398 |
-
"""Main chatbot class"""
|
399 |
|
400 |
-
def
|
401 |
-
|
402 |
-
self.classifier = CodeClassifier()
|
403 |
-
self.llm_client = OpenRouterClient()
|
404 |
-
self.context = ConversationContext()
|
405 |
-
self.loader.load_data()
|
406 |
-
|
407 |
-
def process_input(self, user_input: str) -> str:
|
408 |
-
"""Process user input and generate response"""
|
409 |
-
self.context.turn_count += 1
|
410 |
|
411 |
-
|
412 |
-
|
413 |
-
|
|
|
|
|
|
|
414 |
|
415 |
-
|
416 |
-
|
417 |
-
'assistant': response
|
418 |
-
})
|
419 |
|
420 |
-
|
421 |
-
|
422 |
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
"""Determine user intent"""
|
427 |
-
lower_input = user_input.lower()
|
428 |
-
|
429 |
-
if self.context.current_code:
|
430 |
-
if any(keyword in lower_input for keyword in ['more', 'detail', 'explain']):
|
431 |
-
return 'more_details'
|
432 |
|
433 |
-
|
434 |
-
|
435 |
-
if any(keyword in lower_input for keyword in ['detail', 'comprehensive']):
|
436 |
-
return 'explain_detailed'
|
437 |
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
if not code_info:
|
451 |
-
return f"I couldn't find code {code} in our database. Please check the code and try again."
|
452 |
-
|
453 |
-
self.context.current_code = code
|
454 |
-
self.context.current_code_type = code_type
|
455 |
-
self.context.current_code_info = code_info
|
456 |
-
|
457 |
-
length = ExplanationLength.SHORT
|
458 |
-
if intent == 'explain_detailed':
|
459 |
-
length = ExplanationLength.DETAILED
|
460 |
-
|
461 |
-
explanation = self.llm_client.generate_explanation(
|
462 |
-
code, code_type, code_info, length, self.context
|
463 |
)
|
464 |
|
465 |
-
if
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
|
|
|
|
|
483 |
|
484 |
-
def
|
485 |
-
"""
|
486 |
-
|
487 |
-
|
488 |
-
|
489 |
-
|
490 |
-
|
491 |
-
|
492 |
-
|
493 |
-
|
494 |
-
|
|
|
|
|
|
|
495 |
|
496 |
-
|
497 |
-
|
498 |
|
499 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
500 |
|
501 |
# ============= Gradio Interface =============
|
502 |
|
503 |
-
def
|
504 |
-
|
505 |
-
chatbot = HealthcareBillingChatbot()
|
506 |
|
507 |
-
|
508 |
-
|
509 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
except Exception as e:
|
517 |
-
logger.error(f"Error: {e}")
|
518 |
-
error_msg = "I apologize, but I encountered an error. Please try again."
|
519 |
-
history.append((message, error_msg))
|
520 |
-
return "", history
|
521 |
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
|
|
|
|
|
|
|
|
526 |
|
527 |
-
|
528 |
-
|
529 |
-
|
530 |
-
|
531 |
-
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
541 |
|
542 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
543 |
|
|
|
544 |
with gr.Row():
|
545 |
msg = gr.Textbox(
|
546 |
-
|
547 |
-
|
548 |
-
|
549 |
-
scale=
|
|
|
|
|
550 |
)
|
551 |
-
|
|
|
|
|
|
|
552 |
|
553 |
with gr.Row():
|
554 |
-
|
555 |
-
|
556 |
-
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
|
562 |
-
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
567 |
|
568 |
# Event handlers
|
569 |
-
|
570 |
-
|
571 |
-
|
572 |
-
|
573 |
-
|
574 |
-
|
575 |
-
|
576 |
-
|
577 |
-
|
578 |
-
|
579 |
-
|
580 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
581 |
|
582 |
-
return
|
583 |
|
584 |
-
# Launch
|
585 |
-
|
586 |
-
|
|
|
|
|
|
|
|
|
|
|
|
1 |
#!/usr/bin/env python3
|
2 |
"""
|
3 |
+
Hybrid AI Assistant - General Purpose + Healthcare Billing Expert
|
4 |
+
A ChatGPT-style assistant that can handle any conversation while specializing in healthcare billing codes
|
5 |
"""
|
6 |
|
7 |
import os
|
|
|
14 |
from enum import Enum
|
15 |
import requests
|
16 |
import gradio as gr
|
17 |
+
from datetime import datetime
|
18 |
+
import random
|
19 |
|
20 |
+
# Set up environment
|
21 |
os.environ['OPENROUTER_API_KEY'] = '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'
|
|
|
|
|
22 |
|
23 |
# Configure logging
|
24 |
+
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
|
25 |
logger = logging.getLogger(__name__)
|
26 |
|
27 |
+
# ============= Data Classes =============
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
|
29 |
@dataclass
|
30 |
class CodeInfo:
|
|
|
32 |
description: str
|
33 |
code_type: str
|
34 |
additional_info: Optional[str] = None
|
|
|
35 |
category: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
|
37 |
+
@dataclass
|
38 |
+
class ConversationContext:
|
39 |
+
messages: List[Dict[str, str]] = field(default_factory=list)
|
40 |
+
detected_codes: List[str] = field(default_factory=list)
|
41 |
+
last_topic: Optional[str] = None
|
42 |
+
|
43 |
+
# ============= Healthcare Billing Database =============
|
44 |
|
45 |
+
class BillingCodesDB:
|
|
|
|
|
46 |
def __init__(self):
|
47 |
+
self.codes = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
'A0429': CodeInfo(
|
49 |
code='A0429',
|
50 |
description='Ambulance service, basic life support, emergency transport (BLS-emergency)',
|
51 |
code_type='HCPCS',
|
52 |
+
additional_info='Ground ambulance emergency transport with BLS level care. Used for emergency situations requiring immediate medical transport.',
|
53 |
category='Ambulance Services'
|
54 |
),
|
55 |
'A0428': CodeInfo(
|
56 |
code='A0428',
|
57 |
+
description='Ambulance service, basic life support, non-emergency transport',
|
58 |
code_type='HCPCS',
|
59 |
+
additional_info='Scheduled or non-urgent medical transport with basic life support.',
|
60 |
category='Ambulance Services'
|
61 |
),
|
62 |
'99213': CodeInfo(
|
63 |
code='99213',
|
64 |
+
description='Office visit for established patient, low complexity',
|
65 |
code_type='CPT',
|
66 |
+
additional_info='Typically 20-29 minutes. For straightforward medical issues.',
|
67 |
category='E&M Services'
|
68 |
),
|
69 |
'99214': CodeInfo(
|
70 |
code='99214',
|
71 |
+
description='Office visit for established patient, moderate complexity',
|
72 |
code_type='CPT',
|
73 |
+
additional_info='Typically 30-39 minutes. For moderately complex medical issues.',
|
74 |
+
category='E&M Services'
|
75 |
+
),
|
76 |
+
'99215': CodeInfo(
|
77 |
+
code='99215',
|
78 |
+
description='Office visit for established patient, high complexity',
|
79 |
+
code_type='CPT',
|
80 |
+
additional_info='Typically 40-54 minutes. For complex medical decision making.',
|
81 |
category='E&M Services'
|
82 |
),
|
83 |
'93000': CodeInfo(
|
84 |
code='93000',
|
85 |
+
description='Electrocardiogram (ECG/EKG) with interpretation',
|
86 |
code_type='CPT',
|
87 |
+
additional_info='Complete 12-lead ECG including test, interpretation, and report.',
|
88 |
category='Cardiovascular'
|
89 |
),
|
90 |
'DRG470': CodeInfo(
|
91 |
code='DRG470',
|
92 |
+
description='Major hip and knee joint replacement without complications',
|
93 |
code_type='DRG',
|
94 |
+
additional_info='Medicare payment group for joint replacement surgeries.',
|
95 |
category='Orthopedic'
|
96 |
),
|
97 |
'Z79.899': CodeInfo(
|
98 |
code='Z79.899',
|
99 |
+
description='Other long term drug therapy',
|
100 |
code_type='ICD-10',
|
101 |
+
additional_info='Indicates patient is on long-term medication.',
|
102 |
+
category='Diagnosis'
|
103 |
),
|
104 |
'E1399': CodeInfo(
|
105 |
code='E1399',
|
106 |
description='Durable medical equipment, miscellaneous',
|
107 |
code_type='HCPCS',
|
108 |
+
additional_info='For DME not elsewhere classified.',
|
109 |
+
category='Equipment'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
),
|
111 |
'J3420': CodeInfo(
|
112 |
code='J3420',
|
113 |
+
description='Vitamin B-12 injection',
|
114 |
code_type='HCPCS',
|
115 |
+
additional_info='Cyanocobalamin up to 1000 mcg.',
|
116 |
+
category='Injections'
|
117 |
),
|
118 |
'80053': CodeInfo(
|
119 |
code='80053',
|
120 |
description='Comprehensive metabolic panel',
|
121 |
code_type='CPT',
|
122 |
+
additional_info='14 blood tests including glucose, kidney, and liver function.',
|
123 |
category='Laboratory'
|
124 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
'70450': CodeInfo(
|
126 |
code='70450',
|
127 |
+
description='CT head/brain without contrast',
|
128 |
code_type='CPT',
|
129 |
+
additional_info='Computed tomography of head without contrast material.',
|
130 |
category='Radiology'
|
131 |
+
),
|
132 |
+
'90837': CodeInfo(
|
133 |
+
code='90837',
|
134 |
+
description='Psychotherapy, 60 minutes',
|
135 |
+
code_type='CPT',
|
136 |
+
additional_info='Individual psychotherapy session.',
|
137 |
+
category='Mental Health'
|
138 |
+
),
|
139 |
+
'36415': CodeInfo(
|
140 |
+
code='36415',
|
141 |
+
description='Venipuncture (blood draw)',
|
142 |
+
code_type='CPT',
|
143 |
+
additional_info='Collection of blood by needle.',
|
144 |
+
category='Laboratory'
|
145 |
+
),
|
146 |
+
'99282': CodeInfo(
|
147 |
+
code='99282',
|
148 |
+
description='Emergency department visit, low-moderate severity',
|
149 |
+
code_type='CPT',
|
150 |
+
additional_info='ED visit for problems of low to moderate severity.',
|
151 |
+
category='Emergency'
|
152 |
)
|
153 |
}
|
|
|
|
|
|
|
|
|
154 |
|
155 |
+
def lookup(self, code: str) -> Optional[CodeInfo]:
|
|
|
156 |
code = code.strip().upper()
|
157 |
+
if code in self.codes:
|
158 |
+
return self.codes[code]
|
159 |
+
if code.isdigit() and len(code) == 3:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
160 |
drg_code = f"DRG{code}"
|
161 |
+
if drg_code in self.codes:
|
162 |
+
return self.codes[drg_code]
|
|
|
163 |
return None
|
164 |
|
165 |
+
def search_codes(self, text: str) -> List[str]:
|
166 |
+
"""Extract potential billing codes from text"""
|
167 |
+
found_codes = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
168 |
patterns = [
|
169 |
r'\b([A-V][0-9]{4})\b', # HCPCS
|
170 |
r'\b([0-9]{5})\b', # CPT
|
|
|
173 |
]
|
174 |
|
175 |
for pattern in patterns:
|
176 |
+
matches = re.findall(pattern, text.upper())
|
177 |
for match in matches:
|
178 |
+
if self.lookup(match):
|
179 |
+
found_codes.append(match)
|
|
|
180 |
|
181 |
+
return found_codes
|
182 |
|
183 |
+
# ============= AI Assistant Class =============
|
184 |
|
185 |
+
class HybridAIAssistant:
|
|
|
|
|
186 |
def __init__(self):
|
187 |
+
self.api_key = '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'
|
188 |
+
self.billing_db = BillingCodesDB()
|
189 |
+
self.context = ConversationContext()
|
|
|
190 |
|
191 |
self.headers = {
|
192 |
'Authorization': f'Bearer {self.api_key}',
|
193 |
'Content-Type': 'application/json',
|
194 |
'HTTP-Referer': 'https://huggingface.co',
|
195 |
+
'X-Title': 'Hybrid AI Assistant'
|
196 |
}
|
197 |
|
198 |
+
def detect_intent(self, message: str) -> Dict[str, Any]:
|
199 |
+
"""Detect if the message is about billing codes or general conversation"""
|
200 |
+
lower_msg = message.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
201 |
|
202 |
+
# Check for billing codes in the message
|
203 |
+
codes = self.billing_db.search_codes(message)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
204 |
|
205 |
+
# Keywords that suggest billing/medical coding questions
|
206 |
+
billing_keywords = ['code', 'cpt', 'hcpcs', 'icd', 'drg', 'billing', 'medical code',
|
207 |
+
'healthcare code', 'diagnosis code', 'procedure code']
|
|
|
|
|
208 |
|
209 |
+
is_billing = any(keyword in lower_msg for keyword in billing_keywords) or len(codes) > 0
|
|
|
|
|
|
|
|
|
|
|
210 |
|
211 |
+
return {
|
212 |
+
'is_billing': is_billing,
|
213 |
+
'codes_found': codes,
|
214 |
+
'message': message
|
215 |
+
}
|
216 |
|
217 |
+
def handle_billing_query(self, message: str, codes: List[str]) -> str:
|
218 |
+
"""Handle healthcare billing specific queries"""
|
219 |
+
responses = []
|
220 |
+
|
221 |
+
if codes:
|
222 |
+
for code in codes[:3]: # Limit to first 3 codes
|
223 |
+
info = self.billing_db.lookup(code)
|
224 |
+
if info:
|
225 |
+
response = f"**{info.code} ({info.code_type})**\n"
|
226 |
+
response += f"π **Description:** {info.description}\n"
|
227 |
+
if info.additional_info:
|
228 |
+
response += f"βΉοΈ **Details:** {info.additional_info}\n"
|
229 |
+
if info.category:
|
230 |
+
response += f"π·οΈ **Category:** {info.category}\n"
|
231 |
+
responses.append(response)
|
232 |
+
|
233 |
+
if responses:
|
234 |
+
final_response = "I found information about the billing code(s) you mentioned:\n\n"
|
235 |
+
final_response += "\n---\n".join(responses)
|
236 |
+
final_response += "\n\nπ‘ **Need more details?** Feel free to ask specific questions about these codes!"
|
237 |
+
return final_response
|
238 |
else:
|
239 |
+
return self.get_general_response(message, billing_context=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
240 |
|
241 |
+
def get_general_response(self, message: str, billing_context: bool = False) -> str:
|
242 |
+
"""Get response from OpenRouter API for general queries"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
243 |
|
244 |
+
# Prepare system prompt
|
245 |
+
system_prompt = """You are a helpful, friendly AI assistant with expertise in healthcare billing codes.
|
246 |
+
You can assist with any topic - from casual conversation to complex questions.
|
247 |
+
When discussing medical billing codes, you provide accurate, detailed information.
|
248 |
+
Be conversational, helpful, and engaging. Use emojis occasionally to be friendly.
|
249 |
+
Keep responses concise but informative."""
|
250 |
|
251 |
+
if billing_context:
|
252 |
+
system_prompt += "\nThe user is asking about medical billing. Provide helpful information even if you don't have specific code details."
|
|
|
|
|
253 |
|
254 |
+
# Build conversation history for context
|
255 |
+
messages = [{'role': 'system', 'content': system_prompt}]
|
256 |
|
257 |
+
# Add recent conversation history (last 5 exchanges)
|
258 |
+
for msg in self.context.messages[-10:]:
|
259 |
+
messages.append(msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
260 |
|
261 |
+
# Add current message
|
262 |
+
messages.append({'role': 'user', 'content': message})
|
|
|
|
|
263 |
|
264 |
+
try:
|
265 |
+
response = requests.post(
|
266 |
+
'https://openrouter.ai/api/v1/chat/completions',
|
267 |
+
headers=self.headers,
|
268 |
+
json={
|
269 |
+
'model': 'openai/gpt-3.5-turbo',
|
270 |
+
'messages': messages,
|
271 |
+
'temperature': 0.7,
|
272 |
+
'max_tokens': 500
|
273 |
+
},
|
274 |
+
timeout=30
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
275 |
)
|
276 |
|
277 |
+
if response.status_code == 200:
|
278 |
+
result = response.json()
|
279 |
+
ai_response = result['choices'][0]['message']['content']
|
280 |
+
|
281 |
+
# Update context
|
282 |
+
self.context.messages.append({'role': 'user', 'content': message})
|
283 |
+
self.context.messages.append({'role': 'assistant', 'content': ai_response})
|
284 |
+
|
285 |
+
# Keep only last 20 messages in context
|
286 |
+
if len(self.context.messages) > 20:
|
287 |
+
self.context.messages = self.context.messages[-20:]
|
288 |
+
|
289 |
+
return ai_response
|
290 |
+
else:
|
291 |
+
logger.error(f"API error: {response.status_code}")
|
292 |
+
return self.get_fallback_response(message)
|
293 |
+
|
294 |
+
except Exception as e:
|
295 |
+
logger.error(f"Request failed: {e}")
|
296 |
+
return self.get_fallback_response(message)
|
297 |
|
298 |
+
def get_fallback_response(self, message: str) -> str:
|
299 |
+
"""Fallback responses when API fails"""
|
300 |
+
fallbacks = [
|
301 |
+
"I'm having trouble connecting right now, but I'm still here to help! Could you rephrase your question?",
|
302 |
+
"Let me think about that differently. What specific aspect would you like to know more about?",
|
303 |
+
"That's an interesting question! While I process that, is there anything specific you'd like to explore?",
|
304 |
+
"I'm here to help! Could you provide a bit more detail about what you're looking for?"
|
305 |
+
]
|
306 |
+
return random.choice(fallbacks)
|
307 |
+
|
308 |
+
def process_message(self, message: str) -> str:
|
309 |
+
"""Main method to process any message"""
|
310 |
+
if not message.strip():
|
311 |
+
return "Feel free to ask me anything! I can help with general questions or healthcare billing codes. π"
|
312 |
|
313 |
+
# Detect intent
|
314 |
+
intent = self.detect_intent(message)
|
315 |
|
316 |
+
# Route to appropriate handler
|
317 |
+
if intent['is_billing'] and intent['codes_found']:
|
318 |
+
return self.handle_billing_query(message, intent['codes_found'])
|
319 |
+
else:
|
320 |
+
return self.get_general_response(message, billing_context=intent['is_billing'])
|
321 |
+
|
322 |
+
def reset_context(self):
|
323 |
+
"""Reset conversation context"""
|
324 |
+
self.context = ConversationContext()
|
325 |
|
326 |
# ============= Gradio Interface =============
|
327 |
|
328 |
+
def create_interface():
|
329 |
+
assistant = HybridAIAssistant()
|
|
|
330 |
|
331 |
+
# ChatGPT-style CSS
|
332 |
+
custom_css = """
|
333 |
+
/* Main container */
|
334 |
+
.gradio-container {
|
335 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif !important;
|
336 |
+
max-width: 900px !important;
|
337 |
+
margin: auto !important;
|
338 |
+
background: #ffffff !important;
|
339 |
+
}
|
|
|
|
|
|
|
|
|
|
|
340 |
|
341 |
+
/* Header styling */
|
342 |
+
.header-container {
|
343 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
344 |
+
padding: 2rem;
|
345 |
+
border-radius: 15px 15px 0 0;
|
346 |
+
margin-bottom: 0;
|
347 |
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
348 |
+
}
|
349 |
|
350 |
+
.header-title {
|
351 |
+
color: white;
|
352 |
+
font-size: 2rem;
|
353 |
+
font-weight: 700;
|
354 |
+
margin: 0;
|
355 |
+
display: flex;
|
356 |
+
align-items: center;
|
357 |
+
justify-content: center;
|
358 |
+
gap: 0.5rem;
|
359 |
+
}
|
360 |
+
|
361 |
+
.header-subtitle {
|
362 |
+
color: rgba(255,255,255,0.9);
|
363 |
+
font-size: 1rem;
|
364 |
+
margin-top: 0.5rem;
|
365 |
+
text-align: center;
|
366 |
+
}
|
367 |
+
|
368 |
+
/* Chat container */
|
369 |
+
#chatbot {
|
370 |
+
height: 500px !important;
|
371 |
+
border: 1px solid #e5e7eb !important;
|
372 |
+
border-radius: 12px !important;
|
373 |
+
box-shadow: 0 2px 6px rgba(0,0,0,0.05) !important;
|
374 |
+
background: #ffffff !important;
|
375 |
+
}
|
376 |
+
|
377 |
+
/* Message styling */
|
378 |
+
.message {
|
379 |
+
padding: 1rem !important;
|
380 |
+
margin: 0.5rem !important;
|
381 |
+
border-radius: 12px !important;
|
382 |
+
font-size: 15px !important;
|
383 |
+
line-height: 1.6 !important;
|
384 |
+
}
|
385 |
+
|
386 |
+
.user-message {
|
387 |
+
background: #f3f4f6 !important;
|
388 |
+
border: 1px solid #e5e7eb !important;
|
389 |
+
margin-left: 20% !important;
|
390 |
+
}
|
391 |
+
|
392 |
+
.bot-message {
|
393 |
+
background: #ffffff !important;
|
394 |
+
border: 1px solid #e5e7eb !important;
|
395 |
+
margin-right: 20% !important;
|
396 |
+
}
|
397 |
+
|
398 |
+
/* Input area */
|
399 |
+
#input-box {
|
400 |
+
border: 2px solid #e5e7eb !important;
|
401 |
+
border-radius: 12px !important;
|
402 |
+
padding: 14px 16px !important;
|
403 |
+
font-size: 15px !important;
|
404 |
+
transition: all 0.3s ease !important;
|
405 |
+
background: #ffffff !important;
|
406 |
+
}
|
407 |
+
|
408 |
+
#input-box:focus {
|
409 |
+
border-color: #667eea !important;
|
410 |
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
|
411 |
+
outline: none !important;
|
412 |
+
}
|
413 |
+
|
414 |
+
/* Buttons */
|
415 |
+
.primary-btn {
|
416 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
417 |
+
color: white !important;
|
418 |
+
border: none !important;
|
419 |
+
border-radius: 10px !important;
|
420 |
+
padding: 12px 24px !important;
|
421 |
+
font-weight: 600 !important;
|
422 |
+
font-size: 15px !important;
|
423 |
+
cursor: pointer !important;
|
424 |
+
transition: transform 0.2s ease !important;
|
425 |
+
}
|
426 |
+
|
427 |
+
.primary-btn:hover {
|
428 |
+
transform: translateY(-1px) !important;
|
429 |
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3) !important;
|
430 |
+
}
|
431 |
+
|
432 |
+
.secondary-btn {
|
433 |
+
background: #f3f4f6 !important;
|
434 |
+
color: #374151 !important;
|
435 |
+
border: 1px solid #e5e7eb !important;
|
436 |
+
border-radius: 10px !important;
|
437 |
+
padding: 10px 20px !important;
|
438 |
+
font-weight: 500 !important;
|
439 |
+
cursor: pointer !important;
|
440 |
+
transition: all 0.2s ease !important;
|
441 |
+
}
|
442 |
+
|
443 |
+
.secondary-btn:hover {
|
444 |
+
background: #e5e7eb !important;
|
445 |
+
border-color: #d1d5db !important;
|
446 |
+
}
|
447 |
+
|
448 |
+
/* Example chips */
|
449 |
+
.example-chip {
|
450 |
+
display: inline-block !important;
|
451 |
+
background: #ffffff !important;
|
452 |
+
border: 1px solid #e5e7eb !important;
|
453 |
+
border-radius: 20px !important;
|
454 |
+
padding: 8px 16px !important;
|
455 |
+
margin: 4px !important;
|
456 |
+
font-size: 14px !important;
|
457 |
+
color: #4b5563 !important;
|
458 |
+
cursor: pointer !important;
|
459 |
+
transition: all 0.2s ease !important;
|
460 |
+
}
|
461 |
+
|
462 |
+
.example-chip:hover {
|
463 |
+
background: #f9fafb !important;
|
464 |
+
border-color: #667eea !important;
|
465 |
+
color: #667eea !important;
|
466 |
+
transform: translateY(-1px) !important;
|
467 |
+
}
|
468 |
+
|
469 |
+
/* Info cards */
|
470 |
+
.info-card {
|
471 |
+
background: linear-gradient(135deg, #f6f8fb 0%, #f1f5f9 100%);
|
472 |
+
border: 1px solid #e5e7eb;
|
473 |
+
border-radius: 12px;
|
474 |
+
padding: 1rem;
|
475 |
+
margin: 1rem 0;
|
476 |
+
}
|
477 |
+
|
478 |
+
/* Responsive design */
|
479 |
+
@media (max-width: 768px) {
|
480 |
+
.gradio-container {
|
481 |
+
padding: 0 !important;
|
482 |
+
}
|
483 |
|
484 |
+
.header-title {
|
485 |
+
font-size: 1.5rem;
|
486 |
+
}
|
487 |
+
|
488 |
+
.user-message, .bot-message {
|
489 |
+
margin-left: 5% !important;
|
490 |
+
margin-right: 5% !important;
|
491 |
+
}
|
492 |
+
}
|
493 |
+
"""
|
494 |
+
|
495 |
+
with gr.Blocks(css=custom_css, theme=gr.themes.Base()) as app:
|
496 |
+
# Header
|
497 |
+
gr.HTML("""
|
498 |
+
<div class="header-container">
|
499 |
+
<h1 class="header-title">
|
500 |
+
<span>π€</span>
|
501 |
+
<span>AI Assistant</span>
|
502 |
+
<span style="font-size: 0.8em; background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 12px;">PLUS</span>
|
503 |
+
</h1>
|
504 |
+
<p class="header-subtitle">Your intelligent companion for any question + Healthcare Billing Expert</p>
|
505 |
+
</div>
|
506 |
+
""")
|
507 |
+
|
508 |
+
# Main chat interface
|
509 |
+
chatbot_ui = gr.Chatbot(
|
510 |
+
value=[[None, "π **Hello! I'm your AI Assistant!**\n\nI can help you with:\n\nπ₯ **Healthcare Billing Codes** - I'm an expert in CPT, HCPCS, ICD-10, and DRG codes\nπ¬ **General Conversation** - Ask me anything!\nπ **Learning & Education** - Help with various topics\nβοΈ **Writing & Creation** - Stories, emails, ideas\nπ§ **Problem Solving** - Let's work through challenges together\n\n**Try asking:**\nβ’ 'What is billing code A0429?'\nβ’ 'Help me write an email'\nβ’ 'Explain quantum physics simply'\nβ’ 'What's the weather like?'\n\nHow can I assist you today? π"]],
|
511 |
+
elem_id="chatbot",
|
512 |
+
show_label=False,
|
513 |
+
type="messages",
|
514 |
+
bubble_full_width=False,
|
515 |
+
height=500
|
516 |
+
)
|
517 |
|
518 |
+
# Input section
|
519 |
with gr.Row():
|
520 |
msg = gr.Textbox(
|
521 |
+
placeholder="Ask me anything... (e.g., 'Explain code 99213' or 'Help me write a story')",
|
522 |
+
show_label=False,
|
523 |
+
elem_id="input-box",
|
524 |
+
scale=5,
|
525 |
+
lines=1,
|
526 |
+
max_lines=5
|
527 |
)
|
528 |
+
send_btn = gr.Button("Send", elem_classes="primary-btn", scale=1)
|
529 |
+
|
530 |
+
# Quick examples
|
531 |
+
gr.HTML("<div style='text-align: center; margin: 1rem 0; color: #6b7280; font-size: 14px;'>Quick Examples</div>")
|
532 |
|
533 |
with gr.Row():
|
534 |
+
ex_col1 = gr.Column(scale=1)
|
535 |
+
ex_col2 = gr.Column(scale=1)
|
536 |
+
ex_col3 = gr.Column(scale=1)
|
537 |
+
|
538 |
+
with ex_col1:
|
539 |
+
gr.HTML("<div style='color: #667eea; font-weight: 600; font-size: 13px; margin-bottom: 8px;'>π₯ Medical Billing</div>")
|
540 |
+
ex1 = gr.Button("What is code A0429?", elem_classes="example-chip", size="sm")
|
541 |
+
ex2 = gr.Button("Explain CPT 99213", elem_classes="example-chip", size="sm")
|
542 |
+
ex3 = gr.Button("DRG 470 details", elem_classes="example-chip", size="sm")
|
543 |
+
|
544 |
+
with ex_col2:
|
545 |
+
gr.HTML("<div style='color: #667eea; font-weight: 600; font-size: 13px; margin-bottom: 8px;'>π General Questions</div>")
|
546 |
+
ex4 = gr.Button("How does AI work?", elem_classes="example-chip", size="sm")
|
547 |
+
ex5 = gr.Button("Recipe for pasta", elem_classes="example-chip", size="sm")
|
548 |
+
ex6 = gr.Button("Python tutorial", elem_classes="example-chip", size="sm")
|
549 |
+
|
550 |
+
with ex_col3:
|
551 |
+
gr.HTML("<div style='color: #667eea; font-weight: 600; font-size: 13px; margin-bottom: 8px;'>βοΈ Creative Help</div>")
|
552 |
+
ex7 = gr.Button("Write a poem", elem_classes="example-chip", size="sm")
|
553 |
+
ex8 = gr.Button("Email template", elem_classes="example-chip", size="sm")
|
554 |
+
ex9 = gr.Button("Story ideas", elem_classes="example-chip", size="sm")
|
555 |
+
|
556 |
+
# Control buttons
|
557 |
+
with gr.Row():
|
558 |
+
clear_btn = gr.Button("π New Chat", elem_classes="secondary-btn", size="sm")
|
559 |
+
gr.HTML("<div style='flex-grow: 1;'></div>")
|
560 |
+
gr.HTML("""
|
561 |
+
<div style='text-align: right; color: #6b7280; font-size: 12px;'>
|
562 |
+
Powered by GPT-3.5 β’ Healthcare Billing Database
|
563 |
+
</div>
|
564 |
+
""")
|
565 |
+
|
566 |
+
# Footer info
|
567 |
+
gr.HTML("""
|
568 |
+
<div class="info-card" style="margin-top: 2rem;">
|
569 |
+
<div style="display: flex; justify-content: space-around; text-align: center;">
|
570 |
+
<div>
|
571 |
+
<div style="color: #667eea; font-size: 24px; font-weight: bold;">15+</div>
|
572 |
+
<div style="color: #6b7280; font-size: 12px;">Medical Codes</div>
|
573 |
+
</div>
|
574 |
+
<div>
|
575 |
+
<div style="color: #667eea; font-size: 24px; font-weight: bold;">β</div>
|
576 |
+
<div style="color: #6b7280; font-size: 12px;">Topics</div>
|
577 |
+
</div>
|
578 |
+
<div>
|
579 |
+
<div style="color: #667eea; font-size: 24px; font-weight: bold;">24/7</div>
|
580 |
+
<div style="color: #6b7280; font-size: 12px;">Available</div>
|
581 |
+
</div>
|
582 |
+
<div>
|
583 |
+
<div style="color: #667eea; font-size: 24px; font-weight: bold;">Fast</div>
|
584 |
+
<div style="color: #6b7280; font-size: 12px;">Responses</div>
|
585 |
+
</div>
|
586 |
+
</div>
|
587 |
+
</div>
|
588 |
+
""")
|
589 |
|
590 |
# Event handlers
|
591 |
+
def respond(message, chat_history):
|
592 |
+
if not message.strip():
|
593 |
+
return "", chat_history
|
594 |
+
|
595 |
+
# Process message
|
596 |
+
response = assistant.process_message(message)
|
597 |
+
|
598 |
+
# Update chat history
|
599 |
+
chat_history.append({"role": "user", "content": message})
|
600 |
+
chat_history.append({"role": "assistant", "content": response})
|
601 |
+
|
602 |
+
return "", chat_history
|
603 |
+
|
604 |
+
def clear_chat():
|
605 |
+
assistant.reset_context()
|
606 |
+
welcome = """π **Chat cleared! Ready for a new conversation.**
|
607 |
+
|
608 |
+
I'm here to help with anything you need - from healthcare billing codes to general questions!
|
609 |
+
|
610 |
+
What would you like to know? π"""
|
611 |
+
return [[None, welcome]]
|
612 |
+
|
613 |
+
# Connect events
|
614 |
+
msg.submit(respond, [msg, chatbot_ui], [msg, chatbot_ui])
|
615 |
+
send_btn.click(respond, [msg, chatbot_ui], [msg, chatbot_ui])
|
616 |
+
clear_btn.click(clear_chat, outputs=[chatbot_ui])
|
617 |
+
|
618 |
+
# Example button handlers
|
619 |
+
ex1.click(lambda: "What is healthcare billing code A0429?", outputs=msg)
|
620 |
+
ex2.click(lambda: "Can you explain CPT code 99213 in detail?", outputs=msg)
|
621 |
+
ex3.click(lambda: "Tell me about DRG 470", outputs=msg)
|
622 |
+
ex4.click(lambda: "How does artificial intelligence work?", outputs=msg)
|
623 |
+
ex5.click(lambda: "Give me a simple pasta recipe", outputs=msg)
|
624 |
+
ex6.click(lambda: "Teach me Python basics", outputs=msg)
|
625 |
+
ex7.click(lambda: "Write a short poem about nature", outputs=msg)
|
626 |
+
ex8.click(lambda: "Help me write a professional email template", outputs=msg)
|
627 |
+
ex9.click(lambda: "Give me creative story ideas", outputs=msg)
|
628 |
|
629 |
+
return app
|
630 |
|
631 |
+
# Launch
|
632 |
+
if __name__ == "__main__":
|
633 |
+
app = create_interface()
|
634 |
+
app.launch(
|
635 |
+
server_name="0.0.0.0",
|
636 |
+
server_port=7860,
|
637 |
+
share=False
|
638 |
+
)
|