2025-12-17 19:05:09 +00:00
import ast
import json
2025-12-10 20:01:52 +00:00
import logging
2026-01-09 20:18:01 +00:00
import concurrent . futures
2025-08-05 20:46:19 +00:00
import os
import re
import pandas as pd
2025-08-01 22:36:01 +00:00
import src . config as config
2025-08-11 20:47:52 +00:00
import src . prompts . prompt_templates as prompt_templates
2025-08-05 20:46:19 +00:00
import src . utils . llm_utils as llm_utils
import src . utils . string_utils as string_utils
2026-01-26 16:52:55 +00:00
from src . constants . constants import Constants
from src . constants . delimiters import Delimiter
2025-08-11 20:47:52 +00:00
from src . prompts . fieldset import FieldSet
2025-08-01 22:36:01 +00:00
2025-09-30 20:56:02 +00:00
2025-08-05 20:46:19 +00:00
def clean_service ( service , constants : Constants ) - > str :
2025-08-01 22:36:01 +00:00
"""
Cleans the service string by removing unnecessary terms and formatting it for further processing.
Args:
service (str): The service name or identifier to be cleaned.
Returns:
str: The cleaned service string. If the service is empty or contains only stop words, it returns an empty string.
"""
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
if string_utils . is_empty ( service ) :
return " "
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
service = service . strip ( ) . upper ( )
# Map common values
2025-08-05 20:46:19 +00:00
for acronym , full_form in constants . SYNONYM_MAP . items ( ) :
2025-12-17 19:05:09 +00:00
if string_utils . is_empty ( acronym ) or string_utils . is_empty ( full_form ) :
continue
2025-08-05 20:46:19 +00:00
service = re . sub ( rf " \ b { re . escape ( acronym ) } \ b " , full_form , service )
2025-08-01 22:36:01 +00:00
# Remove other values
2025-08-05 20:46:19 +00:00
for term in constants . REMOVAL_LIST :
2025-12-17 19:05:09 +00:00
if term is None :
continue
2025-08-05 20:46:19 +00:00
service = re . sub ( rf " \ b { re . escape ( term ) } \ b " , " " , service ) . strip ( )
service = re . sub ( r " \ s+ " , " " , service )
2025-12-17 19:05:09 +00:00
if service is None :
return " "
2025-08-01 22:36:01 +00:00
service = service . replace ( " - " , " - " )
2025-08-05 20:46:19 +00:00
if any ( [ v in service for v in [ " UNLISTED " , " UNCATEGORIZED " , " NON-LISTED " ] ] ) :
2025-08-01 22:36:01 +00:00
return " UNLISTED "
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
# Check if all remaining words are stop words
remaining_words = service . split ( )
2025-08-05 20:46:19 +00:00
if all ( word in constants . STOP_WORD_LIST for word in remaining_words ) :
2025-08-01 22:36:01 +00:00
return " "
return service
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
def get_regex_answers ( service , code_answer_dict ) :
"""
**UNUSED** Extracts CPT and Revenue codes from the service string using regex patterns.
2025-08-05 20:46:19 +00:00
This function may be used in the future to save tokens for simple code extractions. It can be used if there is only one code match.
2025-08-01 22:36:01 +00:00
If there are multiple matches, using this function risks losing information if we do not have a regex string for EVERY explicit code we need.
Args:
service (str): The service name or identifier.
code_answer_dict (dict): Dictionary to store the extracted codes.
Returns:
dict: A dictionary containing the extracted codes. If no codes are found, returns an empty dictionary.
"""
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
# Define the regex pattern for CPT codes and ranges
# Only accept single code or single code ranges - multiple codes may have complexities that need to be prompted for
2025-08-05 20:46:19 +00:00
cpt_pattern = (
r " \ b(?:[A-Z] \ d {4} | \ d {5} )(?: \ s*(?:-|through) \ s*(?:[A-Z] \ d {4} | \ d {5} ))? \ b "
)
2025-08-01 22:36:01 +00:00
cpt_matches = re . findall ( cpt_pattern , service )
if len ( cpt_matches ) == 1 :
2026-02-02 21:47:30 -05:00
code_answer_dict [ " PROCEDURE_CD " ] = [ cpt_matches [ 0 ] . replace ( " through " , " - " ) ]
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
if " rev " in service . lower ( ) :
2025-08-05 20:46:19 +00:00
rev_pattern = r " \ b(?: \ d {3} | \ d {2} X)(?: \ s*(?:-|through) \ s*(?: \ d {3} | \ d {2} X))? \ b "
2025-08-01 22:36:01 +00:00
rev_matches = re . findall ( rev_pattern , service )
if len ( rev_matches ) > 0 :
2026-02-02 21:47:30 -05:00
code_answer_dict [ " REVENUE_CD " ] = rev_matches
2025-08-01 22:36:01 +00:00
return code_answer_dict
2025-08-05 20:46:19 +00:00
2025-10-16 21:49:33 +00:00
def code_explicit ( service , methodology , filename ) :
2025-08-01 22:36:01 +00:00
"""
2025-08-05 20:46:19 +00:00
Processes a service string and invokes a language model to generate a response
based on the provided primary questions. The response is then parsed into a
2025-08-01 22:36:01 +00:00
dictionary format.
Args:
2025-08-05 20:46:19 +00:00
service (str): The service name or identifier. If empty, an empty dictionary
2025-08-01 22:36:01 +00:00
is returned.
2025-08-05 20:46:19 +00:00
code_primary_questions (str): The primary questions or prompts to be used
2025-08-01 22:36:01 +00:00
for generating the response.
2025-08-05 20:46:19 +00:00
filename (str): The name of the file associated with the operation, used
2025-08-01 22:36:01 +00:00
for logging or tracking purposes.
Returns:
2025-08-05 20:46:19 +00:00
dict: A dictionary containing the parsed response from the language model.
2025-08-01 22:36:01 +00:00
Returns an empty dictionary if the service string is empty.
"""
2026-01-30 23:23:37 +00:00
# Prompt - field definitions are now included in CODE_EXPLICIT_INSTRUCTION() for caching
2026-02-02 21:47:30 -05:00
prompt , _parser = prompt_templates . CODE_EXPLICIT ( service , methodology )
llm_answer_raw = llm_utils . invoke_claude (
prompt ,
2025-08-05 20:46:19 +00:00
" sonnet_latest " ,
filename ,
2026-01-30 23:23:37 +00:00
cache = True ,
instruction = prompt_templates . CODE_EXPLICIT_INSTRUCTION ( ) ,
usage_label = " CODE_EXPLICIT " ,
2025-08-01 22:36:01 +00:00
)
2026-02-02 21:47:30 -05:00
code_answer_dict = _parser ( llm_answer_raw )
2025-08-01 22:36:01 +00:00
return code_answer_dict
2025-08-05 20:46:19 +00:00
def code_category ( service , proc_category , hcpcs_level2_mapping , filename ) :
2025-08-01 22:36:01 +00:00
"""
Determines if the service explicitly names a code category, such as " A-Codes " or " J-Codes " , and returns the corresponding codes.
Args:
service (str): The service name or identifier.
proc_category (list): List of procedure categories to check against.
filename (str): The name of the file associated with the operation, used for logging or tracking purposes.
Returns:
dict: A dictionary containing the procedure codes and their descriptions. If no codes are found, returns an empty dictionary.
If the service does not explicitly name a code category, it returns an empty dictionary.
"""
code_category_list = [ v . split ( " : " ) [ 1 ] . strip ( ) for v in proc_category if " : " in v ]
matching_values = [ ]
for category in code_category_list :
2025-08-05 20:46:19 +00:00
matching_values + = [
value
for key , value in hcpcs_level2_mapping . items ( )
if key . startswith ( category )
]
2026-02-03 13:41:09 +00:00
2026-02-02 21:47:30 -05:00
prompt , _parser = prompt_templates . CODE_CATEGORY ( service , matching_values )
2026-02-03 13:41:09 +00:00
2026-02-02 21:47:30 -05:00
llm_answer_raw = llm_utils . invoke_claude (
prompt ,
2025-08-05 20:46:19 +00:00
" sonnet_latest " ,
filename ,
2026-01-30 23:23:37 +00:00
cache = True ,
instruction = prompt_templates . CODE_CATEGORY_INSTRUCTION ( ) ,
2025-08-05 20:46:19 +00:00
)
2026-02-03 13:41:09 +00:00
llm_answer_final = _parser ( llm_answer_raw ) # Returns list
2025-08-01 22:36:01 +00:00
2025-08-05 20:46:19 +00:00
code_answer_dict = { " PROCEDURE_CD " : [ ] , " PROCEDURE_CD_DESC " : [ ] }
2026-02-02 21:47:30 -05:00
for answer in llm_answer_final :
2025-08-01 22:36:01 +00:00
if " 0000 " in answer :
code_answer_dict [ " PROCEDURE_CD " ] . append ( answer )
2025-12-10 20:01:52 +00:00
code_answer_dict [ " PROCEDURE_CD_DESC " ] . append ( " " )
2025-08-05 20:46:19 +00:00
elif answer in hcpcs_level2_mapping . values ( ) :
code = list ( hcpcs_level2_mapping . keys ( ) ) [
list ( hcpcs_level2_mapping . values ( ) ) . index ( answer )
]
2025-12-17 19:05:09 +00:00
code_answer_dict [ " PROCEDURE_CD " ] . append ( str ( code ) )
code_answer_dict [ " PROCEDURE_CD_DESC " ] . append ( str ( answer ) )
2026-01-26 16:52:55 +00:00
2025-08-01 22:36:01 +00:00
return code_answer_dict
def code_implicit_special ( service , filename ) :
"""
Allow certain Service classes to be handled as special cases, such as Drugs, Vaccines, Surgery, and PT/OT/ST.
This mapping and prompt can be customized, as clients may have different requirements for these categories that are not captured by our Proc Code levels
Args:
service (str): The service name or identifier.
filename (str): The name of the file associated with the operation, used for logging or tracking purposes.
Returns:
dict: A dictionary containing the implicit codes found for the service. If no codes are found, returns an empty dictionary.
"""
2026-02-02 21:47:30 -05:00
prompt , _parser = prompt_templates . CODE_IMPLICIT_SPECIAL ( service )
2025-08-01 22:36:01 +00:00
2026-02-02 21:47:30 -05:00
llm_answer_raw = llm_utils . invoke_claude (
prompt ,
2026-01-30 23:23:37 +00:00
" sonnet_latest " ,
filename ,
cache = True ,
instruction = prompt_templates . CODE_IMPLICIT_SPECIAL_INSTRUCTION ( ) ,
2025-08-05 20:46:19 +00:00
)
2026-02-02 21:47:30 -05:00
llm_answer_final = _parser ( llm_answer_raw )
2025-08-01 22:36:01 +00:00
special_case_mapping = {
2025-12-17 19:05:09 +00:00
" Drugs " : [ " J0000-J9999 " ] ,
2026-01-26 16:52:55 +00:00
" Vaccines " : [
" J0000-J9999 " ,
" 90471-90474 " ,
" 90620-90621 " ,
" 90633 " ,
" 90647-90648 " ,
" 90651 " ,
" 90670 " ,
" 90672 " ,
" 90680-90681 " ,
" 90686 " ,
" 90696 " ,
" 90698 " ,
" 90700 " ,
" 90707 " ,
" 90710 " ,
" 90713 " ,
" 90714 " ,
] ,
2025-12-17 19:05:09 +00:00
" Surgery " : [ " 10004-69990 " ] ,
2026-01-26 16:52:55 +00:00
" PT/OT/ST " : [
" 92507-92508 " ,
" 92526 " ,
" 97014 " ,
" 97110 " ,
" 97112 " ,
" 97116 " ,
" 97150 " ,
" 97161-97168 " ,
" 97530 " ,
" 97535 " ,
] ,
2025-12-17 19:05:09 +00:00
" PT " : [ " PT Codes TBD " ] ,
" OT " : [ " OT Codes TBD " ] ,
" ST " : [ " ST Codes TBD " ] ,
2025-08-01 22:36:01 +00:00
}
code_answer_dict = { }
2026-02-02 21:47:30 -05:00
if llm_answer_final [ 0 ] in special_case_mapping :
code_answer_dict [ " PROCEDURE_CD " ] = special_case_mapping [ llm_answer_final ]
code_answer_dict [ " PROCEDURE_CD_DESC " ] = llm_answer_final
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
return code_answer_dict
2025-08-05 20:46:19 +00:00
def get_embedding_levels ( level_suffix , implicit_run_dict , constants : Constants ) :
2025-08-01 22:36:01 +00:00
"""
Retrieves the embedding levels and their corresponding mappings for a given level suffix.
Args:
level_suffix (int): The level suffix to determine which mappings to use.
implicit_run_dict (dict): Dictionary indicating which codes to run implicitly.
Returns:
tuple: A tuple containing:
- embedding_levels (list): List of embedding levels to use.
- cpt_mapping (dict): Mapping for CPT codes.
- hcpcs_mapping (dict): Mapping for HCPCS codes.
- rev_mapping (dict): Mapping for Revenue codes.
"""
if level_suffix == 1 :
2025-08-05 20:46:19 +00:00
cpt_mapping = constants . CPT_LEVEL1_MAPPING
hcpcs_mapping = constants . HCPCS_LEVEL1_MAPPING
rev_mapping = constants . REV_LEVEL1_MAPPING
2025-08-01 22:36:01 +00:00
embedding_levels = [ ]
if implicit_run_dict [ " PROCEDURE_CD " ] :
2025-08-05 20:46:19 +00:00
embedding_levels + = [
f " cpt_level { level_suffix } " ,
f " hcpcs_level { level_suffix } " ,
]
2025-08-01 22:36:01 +00:00
if implicit_run_dict [ " REVENUE_CD " ] :
2025-08-05 20:46:19 +00:00
embedding_levels + = [ f " rev_level { level_suffix } " ]
2025-08-01 22:36:01 +00:00
elif level_suffix == 2 :
2025-08-05 20:46:19 +00:00
cpt_mapping = constants . CPT_LEVEL2_MAPPING
hcpcs_mapping = constants . HCPCS_LEVEL2_MAPPING
rev_mapping = constants . REV_MAPPING
2025-08-01 22:36:01 +00:00
embedding_levels = [ ]
if implicit_run_dict [ " PROCEDURE_CD " ] :
2025-08-05 20:46:19 +00:00
embedding_levels + = [
f " cpt_level { level_suffix } " ,
f " hcpcs_level { level_suffix } " ,
]
2025-08-01 22:36:01 +00:00
if implicit_run_dict [ " REVENUE_CD " ] :
2025-08-05 20:46:19 +00:00
embedding_levels + = [ f " rev " ]
2025-08-01 22:36:01 +00:00
return embedding_levels , cpt_mapping , hcpcs_mapping , rev_mapping
2025-08-05 20:46:19 +00:00
def get_match_list ( service , embedding_levels , constants : Constants , top_k : int ) :
2025-08-01 22:36:01 +00:00
"""
Retrieves the best matches for a given service from multiple embedding levels using FAISS index search.
Args:
service (str): The service name or identifier.
embedding_levels (list): List of embedding levels to search in.
top_k (int): The number of top matches to retrieve.
Returns:
tuple: A tuple containing:
- match_list (list): List of matched service descriptions.
- highest_similarity (float): The highest similarity score among the matches.
"""
2025-08-05 20:46:19 +00:00
target_vec = constants . EMBEDDING_MODEL . encode (
[ service ] , normalize_embeddings = True
) . astype ( " float32 " )
2025-08-01 22:36:01 +00:00
match_list = [ ]
highest_similarity = 0
for level in embedding_levels :
2025-08-05 20:46:19 +00:00
embedding = constants . get_embedding ( level )
similarity_scores , match_indices = embedding . index . search ( target_vec , top_k )
2025-08-01 22:36:01 +00:00
if max ( similarity_scores [ 0 ] ) > highest_similarity :
highest_similarity = max ( similarity_scores [ 0 ] )
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
for idx , score in zip ( match_indices [ 0 ] , similarity_scores [ 0 ] ) :
2025-08-05 20:46:19 +00:00
match_list . append ( embedding . choices [ idx ] )
2025-08-01 22:36:01 +00:00
return match_list , highest_similarity
2025-08-05 20:46:19 +00:00
def code_implicit_rag ( service , implicit_run_dict , filename , constants ) :
2025-08-01 22:36:01 +00:00
"""
Processes a service string to find implicit codes using RAG (Retrieval-Augmented Generation) methodology.
Args:
service (str): The service name or identifier.
implicit_run_dict (dict): Dictionary indicating which codes to run implicitly.
filename (str): The name of the file associated with the operation, used for logging or tracking purposes.
Returns:
dict: A dictionary containing the implicit codes found for the service. If no codes are found, returns an empty dictionary.
"""
# Get Embedding Embeddings and Mappings for Levels 1 and 2
level_dicts = [ ]
for level_suffix in [ 1 , 2 ] :
# Get Mappings and Embeddings
2025-08-05 20:46:19 +00:00
embedding_levels , cpt_mapping , hcpcs_mapping , rev_mapping = (
get_embedding_levels ( level_suffix , implicit_run_dict , constants )
)
2025-08-01 22:36:01 +00:00
if not embedding_levels :
return { }
# Get Best Matches
2025-08-05 20:46:19 +00:00
match_list , highest_similarity = get_match_list (
service , embedding_levels , constants , top_k = 5
)
level_dicts . append (
{
" level_suffix " : level_suffix ,
" match_list " : match_list ,
" highest_similarity " : highest_similarity ,
}
)
2025-08-01 22:36:01 +00:00
# Sort level_answers by highest_similarity in descending order
level_dicts . sort ( key = lambda x : x [ " highest_similarity " ] , reverse = True )
# Run the Implicit prompt for each level, starting from the highest similarity
for level_dict in level_dicts :
2025-08-05 20:46:19 +00:00
embedding_levels , cpt_mapping , hcpcs_mapping , rev_mapping = (
get_embedding_levels (
level_dict [ " level_suffix " ] , implicit_run_dict , constants
)
)
2025-08-01 22:36:01 +00:00
2026-02-03 13:41:09 +00:00
prompt , _parser = prompt_templates . CODE_IMPLICIT (
service , level_dict [ " match_list " ]
)
2025-08-01 22:36:01 +00:00
# Run Prompt
2026-02-02 21:36:25 -05:00
llm_answer_raw = llm_utils . invoke_claude (
prompt ,
2025-08-05 20:46:19 +00:00
" sonnet_latest " ,
filename ,
2026-01-30 23:23:37 +00:00
cache = True ,
instruction = prompt_templates . CODE_IMPLICIT_INSTRUCTION ( ) ,
2025-08-05 20:46:19 +00:00
)
2025-08-01 22:36:01 +00:00
try :
2026-02-02 21:36:25 -05:00
llm_answer_final = _parser ( llm_answer_raw )
2025-08-01 22:36:01 +00:00
except Exception as e :
2025-08-05 20:46:19 +00:00
return { " CODE_METHODOLOGY " : e }
2025-08-01 22:36:01 +00:00
2026-02-02 21:36:25 -05:00
if not llm_answer_final :
2025-08-01 22:36:01 +00:00
continue
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
# Populate answers, if any
code_answer_dict = { }
proc_codes , rev_codes = [ ] , [ ]
proc_descs , rev_descs = [ ] , [ ]
2026-02-02 21:36:25 -05:00
for description in llm_answer_final :
2025-08-01 22:36:01 +00:00
if description == " INVALID_SERVICE " :
code_answer_dict [ " CODE_METHODOLOGY " ] = " Generic - Prompt "
continue
if description in cpt_mapping . values ( ) :
2025-08-05 20:46:19 +00:00
matching_codes = [
2025-08-06 16:47:45 +00:00
str ( key ) for key , val in cpt_mapping . items ( ) if val == description
2025-08-05 20:46:19 +00:00
]
2025-08-01 22:36:01 +00:00
if matching_codes :
2025-12-17 19:05:09 +00:00
# Split pipe-delimited codes into individual codes
for code in matching_codes :
2026-02-02 21:47:30 -05:00
proc_codes . extend ( code )
2025-08-01 22:36:01 +00:00
proc_descs . append ( description )
if description in hcpcs_mapping . values ( ) :
2025-08-05 20:46:19 +00:00
matching_codes = [
2025-08-06 16:47:45 +00:00
str ( key ) for key , val in hcpcs_mapping . items ( ) if val == description
2025-08-05 20:46:19 +00:00
]
2025-08-01 22:36:01 +00:00
if matching_codes :
2025-12-17 19:05:09 +00:00
# Split pipe-delimited codes into individual codes
for code in matching_codes :
2026-02-02 21:47:30 -05:00
proc_codes . extend ( code )
2025-08-01 22:36:01 +00:00
proc_descs . append ( description )
if description in rev_mapping . values ( ) :
2025-08-05 20:46:19 +00:00
matching_codes = [
2025-08-06 16:47:45 +00:00
str ( key ) for key , val in rev_mapping . items ( ) if val == description
2025-08-05 20:46:19 +00:00
]
2025-08-01 22:36:01 +00:00
if matching_codes :
2025-12-17 19:05:09 +00:00
# Split pipe-delimited codes into individual codes
for code in matching_codes :
2026-02-02 21:47:30 -05:00
rev_codes . extend ( code )
2025-08-01 22:36:01 +00:00
rev_descs . append ( description )
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
# Combine answers
if proc_codes :
2025-12-17 19:05:09 +00:00
code_answer_dict [ " PROCEDURE_CD " ] = proc_codes
code_answer_dict [ " PROCEDURE_CD_DESC " ] = proc_descs
2025-08-05 20:46:19 +00:00
code_answer_dict [ " CODE_METHODOLOGY " ] = (
2025-11-10 19:29:45 +00:00
f " Implicit - Level { level_dict [ ' level_suffix ' ] } "
2025-08-05 20:46:19 +00:00
)
2025-08-01 22:36:01 +00:00
if rev_codes :
2025-12-17 19:05:09 +00:00
code_answer_dict [ " REVENUE_CD " ] = rev_codes
code_answer_dict [ " REVENUE_CD_DESC " ] = rev_descs
2025-08-05 20:46:19 +00:00
code_answer_dict [ " CODE_METHODOLOGY " ] = (
2025-11-10 19:29:45 +00:00
f " Implicit - Level { level_dict [ ' level_suffix ' ] } "
2025-08-05 20:46:19 +00:00
)
2025-08-01 22:36:01 +00:00
if code_answer_dict :
return code_answer_dict
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
return { }
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
def code_last_check ( service , filename ) :
"""
For Services that did not match any codes, this function categorizes the reason as " Specific " or " Generic " .
Args:
service (str): The service term.
filename (str): The name of the file associated with the operation.
Returns:
str: " Specific " if the service is a specific case that requires manual intervention,
" Generic " if the service is a generic case that does not match any codes.
"""
2026-02-02 21:47:30 -05:00
prompt , _parser = prompt_templates . CODE_LAST_CHECK ( service )
2025-08-01 22:36:01 +00:00
2026-02-02 21:47:30 -05:00
llm_answer_raw = llm_utils . invoke_claude (
prompt ,
2026-01-30 23:23:37 +00:00
" sonnet_latest " ,
filename ,
cache = True ,
instruction = prompt_templates . CODE_LAST_CHECK_INSTRUCTION ( ) ,
2025-08-05 20:46:19 +00:00
)
2026-02-02 21:47:30 -05:00
try :
llm_answer_final = _parser ( llm_answer_raw )
return llm_answer_final [ 0 ]
except :
return " Generic "
2025-08-01 22:36:01 +00:00
2026-01-26 16:52:55 +00:00
def fill_bill_type (
service ,
answer_dict ,
BILL_TYPE_MAPPING ,
BILL_TYPE_REVERSE_MAPPING ,
reimb_term = None ,
exhibit_text = None ,
) :
2025-08-01 22:36:01 +00:00
"""
Fills the BILL_TYPE_CD and BILL_TYPE_CD_DESC fields in the answer dictionary.
Args:
service (str): The service term.
answer_dict (dict): The answer dictionary to update.
2025-12-26 18:24:57 +00:00
BILL_TYPE_MAPPING (dict): Mapping from codes to descriptions.
BILL_TYPE_REVERSE_MAPPING (dict): Mapping from descriptions to codes.
2026-01-08 22:41:52 +00:00
reimb_term (str, optional): The reimbursement term for additional context.
2025-12-26 18:24:57 +00:00
exhibit_text (str, optional): Full exhibit text for contextual analysis.
2025-08-01 22:36:01 +00:00
Returns:
dict: The updated answer dictionary.
"""
2025-08-05 20:46:19 +00:00
valid_bill_type = sorted ( list ( set ( BILL_TYPE_MAPPING . values ( ) ) ) )
2026-01-26 16:52:55 +00:00
2026-02-02 21:47:30 -05:00
prompt , _parser = prompt_templates . FILL_BILL_TYPE (
service , valid_bill_type , reimb_term = reimb_term , exhibit_text = None
)
2026-01-08 22:41:52 +00:00
# First attempt: Service term + reimbursement term
2026-02-02 21:47:30 -05:00
llm_answer_raw = llm_utils . invoke_claude (
prompt ,
2026-01-26 16:52:55 +00:00
" sonnet_latest " ,
" " ,
2026-01-30 23:23:37 +00:00
cache = True ,
instruction = prompt_templates . FILL_BILL_TYPE_INSTRUCTION ( ) ,
2025-08-05 20:46:19 +00:00
)
2026-02-03 11:01:47 -06:00
llm_answer_final = _parser ( llm_answer_raw )
2026-01-26 16:52:55 +00:00
2026-01-08 22:41:52 +00:00
# Second attempt: Service term + reimbursement term + exhibit context (if first attempt failed and context available)
2026-02-02 21:47:30 -05:00
if not llm_answer_final and exhibit_text :
prompt , _parser = prompt_templates . FILL_BILL_TYPE (
service ,
valid_bill_type ,
reimb_term = reimb_term ,
exhibit_text = exhibit_text ,
)
llm_answer_raw = llm_utils . invoke_claude (
prompt ,
2026-01-26 16:52:55 +00:00
" sonnet_latest " ,
" " ,
2026-01-30 23:23:37 +00:00
cache = True ,
instruction = prompt_templates . FILL_BILL_TYPE_INSTRUCTION ( ) ,
2025-12-26 18:24:57 +00:00
)
2026-02-02 21:47:30 -05:00
llm_answer_final = _parser ( llm_answer_raw )
2025-08-01 22:36:01 +00:00
2025-12-26 18:24:57 +00:00
# Keep legacy behavior: return answer_dict unchanged if no result found
2026-02-02 21:47:30 -05:00
if not llm_answer_final :
2025-08-01 22:36:01 +00:00
return answer_dict
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
bill_codes , bill_descs = [ ] , [ ]
2026-02-02 21:47:30 -05:00
for description in llm_answer_final :
2025-08-05 20:46:19 +00:00
if description in BILL_TYPE_REVERSE_MAPPING :
bill_codes . append ( BILL_TYPE_REVERSE_MAPPING [ description ] )
2025-08-01 22:36:01 +00:00
bill_descs . append ( description )
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
if bill_codes :
2026-02-02 21:47:30 -05:00
answer_dict [ " BILL_TYPE_CD " ] = bill_codes
answer_dict [ " BILL_TYPE_CD_DESC " ] = bill_descs
2025-08-01 22:36:01 +00:00
return answer_dict
2025-08-05 20:46:19 +00:00
2025-08-29 21:13:24 +00:00
def fill_grouper_cd_desc ( answer_dict , constants : Constants ) :
"""
Fills the GROUPER_CD_DESC field in the answer dictionary based on the GROUPER_CD.
Args:
answer_dict (dict): The answer dictionary to update.
Returns:
dict: The updated answer dictionary.
"""
grouper_cd = answer_dict . get ( " GROUPER_CD " )
if string_utils . is_empty ( grouper_cd ) :
return answer_dict
grouper_type = answer_dict . get ( " GROUPER_TYPE " )
if string_utils . is_empty ( grouper_type ) :
return answer_dict
grouper_descs = [ ]
grouper_cd = eval ( grouper_cd ) if " [ " in grouper_cd else grouper_cd
for code in grouper_cd :
2025-12-10 20:01:52 +00:00
try :
code_without_severity = int (
code . split ( " - " ) [ 0 ]
) # Remove severity level if present
except Exception as e :
logging . warning (
f " Failed to parse grouper code ' { code } ' as integer: { type ( e ) . __name__ } : { str ( e ) } . "
f " Using original code value. "
)
code_without_severity = code
2025-09-30 20:56:02 +00:00
if grouper_type == " APR-DRG " and (
code_without_severity in constants . GROUPER_APR_DRG_MAPPING
) :
grouper_descs . append (
constants . GROUPER_APR_DRG_MAPPING [ code_without_severity ]
)
elif grouper_type == " MS-DRG " and (
code_without_severity in constants . GROUPER_MS_DRG_MAPPING
) :
grouper_descs . append (
constants . GROUPER_MS_DRG_MAPPING [ code_without_severity ]
)
2025-08-29 21:13:24 +00:00
if grouper_descs :
2026-02-02 21:47:30 -05:00
answer_dict [ " GROUPER_CD_DESC " ] = list ( set ( grouper_descs ) )
2025-08-29 21:13:24 +00:00
return answer_dict
2025-08-01 22:36:01 +00:00
def get_implicit_runs ( answer_dict ) :
"""
Determines which code runs should be executed implicitly based on the Claim Type and Bill Type
Args:
answer_dict (dict): Dictionary containing, at minimum, the AARETE_DERIVED_CLAIM_TYPE_CD and BILL_TYPE_CD_DESC fields.
Returns:
dict: Dictionary containing boolean values for REVENUE_CD and PROCEDURE_CD indicating whether to run those codes implicitly.
"""
run_dict = { }
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
claim_type = answer_dict . get ( " AARETE_DERIVED_CLAIM_TYPE_CD " )
bill_type = answer_dict . get ( " BILL_TYPE_CD_DESC " )
if claim_type == " H " :
run_dict [ " REVENUE_CD " ] = True
elif claim_type == " M " :
if not string_utils . is_empty ( bill_type ) :
run_dict [ " REVENUE_CD " ] = True
else :
run_dict [ " REVENUE_CD " ] = False
else :
run_dict [ " REVENUE_CD " ] = False
# Run proc code implicit for all bill types except 2
if bill_type in [ " Inpatient Hospital " , " Skilled Nursing Facility " ] :
run_dict [ " PROCEDURE_CD " ] = False
else :
run_dict [ " PROCEDURE_CD " ] = True
return run_dict
2025-08-05 20:46:19 +00:00
2025-12-17 19:05:09 +00:00
def normalize_answer_dict_codes ( answer_dict ) :
"""
Normalizes code fields in an answer dictionary to JSON list format.
2026-01-26 16:52:55 +00:00
2025-12-17 19:05:09 +00:00
Args:
answer_dict (dict): Dictionary containing code fields to normalize.
Returns:
dict: Dictionary with normalized code fields.
"""
fields_to_normalize = [ " REVENUE_CD " , " PROCEDURE_CD " , " CPT4_PROC_CD " ]
2026-01-26 16:52:55 +00:00
2025-12-17 19:05:09 +00:00
for field in fields_to_normalize :
if field in answer_dict :
answer_dict [ field ] = string_utils . normalize_to_json_list ( answer_dict [ field ] )
2026-01-26 16:52:55 +00:00
2025-12-17 19:05:09 +00:00
return answer_dict
2025-08-05 20:46:19 +00:00
def extract_codes_from_service ( answer_dict , constants : Constants ) :
2025-08-01 22:36:01 +00:00
"""
Extracts codes from the service term in the answer dictionary.
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
Args:
answer_dict (dict): Dictionary containing, at minimum, the SERVICE_TERM field.
Returns:
dict: Dictionary containing extracted codes and their descriptions.
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
Codes Extracted:
Explicit Codes:
- PROCEDURE_CD: CPT, HCPCS
- CPT4_PROC_MOD: CPT4 Modifiers
- REVENUE_CD: Revenue codes
- DIAG_CD: Diagnosis codes (ICD-10)
- GROUPER_CD: MS and APR DRG Grouper Codes
- NDC_CD: National Drug Codes (NDC)
- CLAIM_ADMIT_TYPE_CD: Claim Admit Type Code
- CLAIM_STATUS_CD: Claim Status Code
Implicit Codes (Up to Level 2):
- PROCEDURE_CD: CPT, HCPCS
- REVENUE_CD: Revenue codes
"""
2025-08-05 20:46:19 +00:00
service , bill_type = answer_dict . get ( " SERVICE_TERM " ) , answer_dict . get (
" BILL_TYPE_CD_DESC "
)
2025-12-17 19:05:09 +00:00
filename = answer_dict . get ( " FILENAME " , " " )
methodology = answer_dict . get ( " REIMB_TERM " , " " )
2025-08-01 22:36:01 +00:00
2025-08-06 16:47:45 +00:00
if string_utils . is_empty ( service ) :
return { }
2025-08-01 22:36:01 +00:00
# Fill Bill Type if not filled
if string_utils . is_empty ( bill_type ) :
2025-12-26 18:24:57 +00:00
exhibit_text = answer_dict . get ( " EXHIBIT_TEXT " , None )
2026-01-08 22:41:52 +00:00
reimb_term = answer_dict . get ( " REIMB_TERM " , None )
2025-08-05 20:46:19 +00:00
answer_dict = fill_bill_type (
service ,
answer_dict ,
constants . BILL_TYPE_MAPPING ,
constants . BILL_TYPE_REVERSE_MAPPING ,
2026-01-08 22:41:52 +00:00
reimb_term = reimb_term ,
2025-12-26 18:24:57 +00:00
exhibit_text = exhibit_text ,
2025-08-05 20:46:19 +00:00
)
2025-08-01 22:36:01 +00:00
# Get list of codes we want implicit - based on claim type
implicit_run_dict = get_implicit_runs ( answer_dict )
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
# Preprocessing
2025-08-05 20:46:19 +00:00
service_clean = clean_service ( service , constants )
2025-08-01 22:36:01 +00:00
# Exit point for unlisted
if service_clean == " UNLISTED " :
answer_dict [ " PROCEDURE_CD_DESC " ] = " UNLISTED "
answer_dict [ " CODE_METHODOLOGY " ] = " UNLISTED "
2025-12-17 19:05:09 +00:00
return normalize_answer_dict_codes ( answer_dict )
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
# Exit point if N/A or in DO_NOT_RUN list
2025-08-05 20:46:19 +00:00
if string_utils . is_empty ( service_clean ) or any (
[ v in service_clean for v in constants . DO_NOT_RUN ]
) :
2025-08-01 22:36:01 +00:00
answer_dict [ " CODE_METHODOLOGY " ] = " Generic - Before Prompts "
2025-12-17 19:05:09 +00:00
return normalize_answer_dict_codes ( answer_dict )
2025-08-01 22:36:01 +00:00
# Explicit Codes (run always)
2025-10-16 21:49:33 +00:00
code_answer_dict = code_explicit ( service_clean , methodology , filename )
2025-08-05 20:46:19 +00:00
if any (
not string_utils . is_empty ( value ) for value in code_answer_dict . values ( )
) and all (
[ " Category " not in v for v in code_answer_dict . get ( " PROCEDURE_CD " , " " ) ]
) : # if any code value is not empty, return
2025-08-01 22:36:01 +00:00
if " NOT_ESTABLISHED " in code_answer_dict . get ( " PROCEDURE_CD " , " " ) :
code_answer_dict [ " CODE_METHODOLOGY " ] = " Implicit - Not Established "
code_answer_dict [ " PROCEDURE_CD " ] = " [] "
else :
code_answer_dict [ " CODE_METHODOLOGY " ] = " Explicit "
answer_dict . update ( code_answer_dict )
2025-12-17 19:05:09 +00:00
return normalize_answer_dict_codes ( answer_dict )
2025-08-01 22:36:01 +00:00
# Implicit Codes: Code Categories
if any ( [ " Category: " in v for v in code_answer_dict . get ( " PROCEDURE_CD " , " " ) ] ) :
2025-08-05 20:46:19 +00:00
code_answer_dict = code_category (
service_clean ,
code_answer_dict . get ( " PROCEDURE_CD " , " " ) ,
constants . HCPCS_LEVEL2_MAPPING ,
" " ,
)
if any (
not string_utils . is_empty ( value ) for value in code_answer_dict . values ( )
) : # if any code value is not empty, return
2025-08-01 22:36:01 +00:00
code_answer_dict [ " CODE_METHODOLOGY " ] = " Implicit - Letter Category "
answer_dict . update ( code_answer_dict )
2025-12-17 19:05:09 +00:00
return normalize_answer_dict_codes ( answer_dict )
2025-08-01 22:36:01 +00:00
# Implicit Codes: Special Categories
code_answer_dict = code_implicit_special ( service_clean , " " )
if code_answer_dict :
code_answer_dict [ " CODE_METHODOLOGY " ] = " Implicit - Special Case "
answer_dict . update ( code_answer_dict )
2025-12-17 19:05:09 +00:00
return normalize_answer_dict_codes ( answer_dict )
2025-08-01 22:36:01 +00:00
# Implicit Codes: Levels 1 and 2
2025-08-05 20:46:19 +00:00
code_answer_dict = code_implicit_rag (
service_clean , implicit_run_dict , " " , constants
)
if any (
not string_utils . is_empty ( value ) for value in code_answer_dict . values ( )
) : # if any code value is not empty, return
2025-08-01 22:36:01 +00:00
answer_dict . update ( code_answer_dict )
2025-12-17 19:05:09 +00:00
return normalize_answer_dict_codes ( answer_dict )
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
# No Match - Why? Generic or Specific
last_check_answer = code_last_check ( service_clean , " " )
if last_check_answer == " Generic " :
answer_dict [ " CODE_METHODOLOGY " ] = " Generic - No Match "
elif last_check_answer == " Specific " :
answer_dict [ " CODE_METHODOLOGY " ] = " Specific - No Match "
2025-12-17 19:05:09 +00:00
return normalize_answer_dict_codes ( answer_dict )
2025-08-01 22:36:01 +00:00
2025-08-05 20:46:19 +00:00
def code_breakout ( merged_results : pd . DataFrame , constants : Constants ) :
2025-08-01 22:36:01 +00:00
"""
Processes a list of answer dictionaries to extract and fill in code-related information.
Wrapper function to be run from main Doczy
Args:
merged_results (pd.DataFrame): DataFrame containing merged results from the processing pipeline
Returns:
pd.DataFrame: DataFrame with updated code fields.
"""
2026-01-09 20:18:01 +00:00
import concurrent . futures
2025-08-05 20:46:19 +00:00
2026-02-03 01:38:49 -05:00
answer_dicts = merged_results . to_dict ( orient = " records " )
2025-08-01 22:36:01 +00:00
2026-01-09 20:18:01 +00:00
# Parallelize code extraction
def process_single_code ( answer_dict ) :
2025-08-05 20:46:19 +00:00
code_answer_dict = extract_codes_from_service ( answer_dict , constants )
2025-08-01 22:36:01 +00:00
answer_dict . update ( code_answer_dict )
2026-01-09 20:18:01 +00:00
return answer_dict
max_workers = min ( len ( answer_dicts ) , 10 )
with concurrent . futures . ThreadPoolExecutor ( max_workers = max_workers ) as executor :
answer_dicts_with_code = list ( executor . map ( process_single_code , answer_dicts ) )
2025-08-26 15:43:53 +00:00
2025-08-29 21:13:24 +00:00
# Fill in GROUPER_CD_DESC based on GROUPER_TYPE and GROUPER_CD
answer_dicts_with_code = [
2025-09-30 20:56:02 +00:00
fill_grouper_cd_desc ( answer_dict , constants )
for answer_dict in answer_dicts_with_code
2025-08-29 21:13:24 +00:00
]
2025-12-17 19:05:09 +00:00
df = pd . DataFrame ( answer_dicts_with_code )
# Normalize code columns to JSON list format
2026-01-26 16:52:55 +00:00
for col in [
" PROCEDURE_CD " ,
" CPT4_PROC_MOD " ,
" REVENUE_CD " ,
" DIAG_CD " ,
" GROUPER_CD " ,
" NDC_CD " ,
" CLAIM_ADMIT_TYPE_CD " ,
" CLAIM_STATUS_CD " ,
] :
2025-12-17 19:05:09 +00:00
if col in df . columns :
df [ col ] = df [ col ] . apply ( string_utils . normalize_to_json_list )
2025-08-26 15:43:53 +00:00
2025-12-17 19:05:09 +00:00
return df
2025-08-26 15:43:53 +00:00
2026-01-26 16:52:55 +00:00
2025-08-26 15:43:53 +00:00
def grouper_breakout ( results_with_code : pd . DataFrame ) :
"""
Processes a DataFrame to extract and fill in grouper-related information in cases where we got a grouper_cd from code_breakout
but didn ' t get grouper fields from methodology + grouper breakout as in special case grouper was not identified and extracted.
Args:
results_with_code (pd.DataFrame): DataFrame containing the answer dictionaries with code fields
Returns:
pd.DataFrame: DataFrame with updated grouper fields.
"""
# try grouper fields again if they are empty and grouper_cd is not empty
answer_dicts_with_code = results_with_code . to_dict ( orient = " records " )
2026-01-09 20:18:01 +00:00
# Separate dicts that need grouper breakout from those that don't
needs_breakout = [ ]
no_breakout_needed = [ ]
2025-08-26 15:43:53 +00:00
for answer_dict in answer_dicts_with_code :
2025-09-30 20:56:02 +00:00
if string_utils . is_empty (
answer_dict . get ( " GROUPER_TYPE " )
) and not string_utils . is_empty ( answer_dict . get ( " GROUPER_CD " ) ) :
2026-01-09 20:18:01 +00:00
needs_breakout . append ( answer_dict )
else :
no_breakout_needed . append ( answer_dict )
2026-01-26 16:52:55 +00:00
logging . debug (
f " Grouper breakout: { len ( needs_breakout ) } rows need LLM processing, { len ( no_breakout_needed ) } rows skip "
)
2026-01-09 20:18:01 +00:00
# Parallelize LLM calls for rows that need breakout
def process_grouper_breakout ( answer_dict ) :
2026-02-03 11:01:47 -06:00
grouper_breakout_prompt , grouper_parser = prompt_templates . GROUPER_BREAKOUT (
2026-01-09 20:18:01 +00:00
answer_dict . get ( " SERVICE_TERM " , " " ) ,
answer_dict . get ( " REIMB_TERM " , " " ) ,
)
claude_answer_raw = llm_utils . invoke_claude (
2026-01-30 23:23:37 +00:00
grouper_breakout_prompt ,
" sonnet_latest " ,
" " ,
cache = True ,
instruction = prompt_templates . GROUPER_BREAKOUT_INSTRUCTION ( ) ,
2026-01-09 20:18:01 +00:00
)
try :
2026-02-03 11:01:47 -06:00
grouper_answer = grouper_parser ( claude_answer_raw )
2026-01-09 20:18:01 +00:00
except Exception as e :
grouper_answer = { " GROUPER_TYPE " : e }
answer_dict . update ( grouper_answer )
return answer_dict
if needs_breakout :
max_workers = min ( len ( needs_breakout ) , 10 )
with concurrent . futures . ThreadPoolExecutor ( max_workers = max_workers ) as executor :
2026-01-26 16:52:55 +00:00
processed_breakout = list (
executor . map ( process_grouper_breakout , needs_breakout )
)
2026-01-09 20:18:01 +00:00
else :
processed_breakout = [ ]
2025-08-26 15:43:53 +00:00
2026-01-09 20:18:01 +00:00
# Combine all results
final_answer_dicts = no_breakout_needed + processed_breakout
2025-08-05 20:46:19 +00:00
2026-01-09 20:18:01 +00:00
return pd . DataFrame ( final_answer_dicts )