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
from constants . constants import Constants
from 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-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 ( ) :
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 :
service = re . sub ( rf " \ b { re . escape ( term ) } \ b " , " " , service ) . strip ( )
service = re . sub ( r " \ s+ " , " " , service )
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 :
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 :
code_answer_dict [ " REVENUE_CD " ] = " | " . join ( rev_matches )
return code_answer_dict
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
def code_explicit ( service , filename ) :
"""
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.
"""
# Prompt
2025-08-05 20:46:19 +00:00
code_primary_questions = FieldSet (
file_path = config . FIELD_JSON_PATH , field_type = " code_primary_breakout "
2025-08-11 20:47:52 +00:00
) . print_prompt_dict ( )
2025-08-01 22:36:01 +00:00
claude_answer_raw = llm_utils . invoke_claude (
2025-08-11 20:47:52 +00:00
prompt_templates . CODE_EXPLICIT ( service , code_primary_questions ) ,
2025-08-05 20:46:19 +00:00
" sonnet_latest " ,
filename ,
2025-08-01 22:36:01 +00:00
)
code_answer_dict = string_utils . universal_json_load ( claude_answer_raw )
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 )
]
2025-08-01 22:36:01 +00:00
claude_answer_raw = llm_utils . invoke_claude (
2025-08-11 20:47:52 +00:00
prompt_templates . CODE_CATEGORY ( service , matching_values ) ,
2025-08-05 20:46:19 +00:00
" sonnet_latest " ,
filename ,
)
claude_answer_final = string_utils . universal_json_load (
claude_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 " : [ ] }
2025-08-01 22:36:01 +00:00
for answer in claude_answer_final :
if " 0000 " in answer :
code_answer_dict [ " PROCEDURE_CD " ] . append ( answer )
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-08-01 22:36:01 +00:00
code_answer_dict [ " PROCEDURE_CD " ] . append ( code )
code_answer_dict [ " PROCEDURE_CD_DESC " ] . append ( answer )
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.
"""
claude_answer_raw = llm_utils . invoke_claude (
2025-08-11 20:47:52 +00:00
prompt_templates . CODE_IMPLICIT_SPECIAL ( service ) , " sonnet_latest " , filename
2025-08-05 20:46:19 +00:00
)
claude_answer_final = string_utils . extract_text_from_delimiters (
claude_answer_raw , Delimiter . PIPE
)
2025-08-01 22:36:01 +00:00
special_case_mapping = {
2025-08-05 20:46:19 +00:00
" Drugs " : " J0000-J9999 " ,
" Vaccines " : " J0000-J9999|90471‑ 90474|90620‑ 90621|90633|90647‑ 90648|90651|90670|90672|90680‑ 90681|90686|90696|90698|90700|90707|90710|90713|90714 " ,
" Surgery " : " 10004-69990 " ,
" PT/OT/ST " : " 92507-92508|92526|97014|97110|97112|97116|97150|97161-97168|97530|97535 " ,
" PT " : " PT Codes TBD " ,
" OT " : " OT Codes TBD " ,
" ST " : " ST Codes TBD " ,
2025-08-01 22:36:01 +00:00
}
code_answer_dict = { }
2025-08-11 20:47:52 +00:00
if claude_answer_final in special_case_mapping :
2025-08-01 22:36:01 +00:00
code_answer_dict [ " PROCEDURE_CD " ] = special_case_mapping [ claude_answer_final ]
code_answer_dict [ " PROCEDURE_CD_DESC " ] = claude_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
# Run Prompt
claude_answer_raw = llm_utils . invoke_claude (
2025-08-11 20:47:52 +00:00
prompt_templates . CODE_IMPLICIT ( service , level_dict [ " match_list " ] ) ,
2025-08-05 20:46:19 +00:00
" sonnet_latest " ,
filename ,
)
2025-08-01 22:36:01 +00:00
try :
claude_answer_final = string_utils . universal_json_load ( claude_answer_raw )
except Exception as e :
2025-08-05 20:46:19 +00:00
return { " CODE_METHODOLOGY " : e }
2025-08-01 22:36:01 +00:00
if not claude_answer_final :
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 = [ ] , [ ]
for description in claude_answer_final :
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 :
proc_codes . append ( " | " . join ( matching_codes ) )
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 :
proc_codes . append ( " | " . join ( matching_codes ) )
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 :
rev_codes . append ( " | " . join ( matching_codes ) )
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 :
code_answer_dict [ " PROCEDURE_CD " ] = " | " . join ( proc_codes )
code_answer_dict [ " PROCEDURE_CD_DESC " ] = " | " . join ( proc_descs )
2025-08-05 20:46:19 +00:00
code_answer_dict [ " CODE_METHODOLOGY " ] = (
f " Implicit - Level { level_dict [ " level_suffix " ] } "
)
2025-08-01 22:36:01 +00:00
if rev_codes :
code_answer_dict [ " REVENUE_CD " ] = " | " . join ( rev_codes )
code_answer_dict [ " REVENUE_CD_DESC " ] = " | " . join ( rev_descs )
2025-08-05 20:46:19 +00:00
code_answer_dict [ " CODE_METHODOLOGY " ] = (
f " Implicit - Level { level_dict [ " level_suffix " ] } "
)
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.
"""
claude_answer_raw = llm_utils . invoke_claude (
2025-08-11 20:47:52 +00:00
prompt_templates . CODE_LAST_CHECK ( service ) , " sonnet_latest " , filename
2025-08-05 20:46:19 +00:00
)
claude_answer_final = string_utils . extract_text_from_delimiters (
claude_answer_raw , Delimiter . PIPE
)
2025-08-01 22:36:01 +00:00
return claude_answer_final
2025-08-05 20:46:19 +00:00
def fill_bill_type ( service , answer_dict , BILL_TYPE_MAPPING , BILL_TYPE_REVERSE_MAPPING ) :
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.
Returns:
dict: The updated answer dictionary.
"""
2025-08-05 20:46:19 +00:00
valid_bill_type = sorted ( list ( set ( BILL_TYPE_MAPPING . values ( ) ) ) )
2025-08-01 22:36:01 +00:00
claude_answer_raw = llm_utils . invoke_claude (
2025-08-11 20:47:52 +00:00
prompt_templates . FILL_BILL_TYPE ( service , valid_bill_type ) , " sonnet_latest " , " "
2025-08-05 20:46:19 +00:00
)
2025-08-01 22:36:01 +00:00
claude_answer_final = string_utils . universal_json_load ( claude_answer_raw )
if not claude_answer_final :
return answer_dict
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
bill_codes , bill_descs = [ ] , [ ]
for description in claude_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 :
answer_dict [ " BILL_TYPE_CD " ] = " | " . join ( bill_codes )
answer_dict [ " BILL_TYPE_CD_DESC " ] = " | " . join ( bill_descs )
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 :
code_without_severity = int ( code . split ( " - " ) [ 0 ] ) # Remove severity level if present
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 ] )
if grouper_descs :
answer_dict [ " GROUPER_CD_DESC " ] = " | " . join ( list ( set ( grouper_descs ) ) )
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
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-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-08-05 20:46:19 +00:00
answer_dict = fill_bill_type (
service ,
answer_dict ,
constants . BILL_TYPE_MAPPING ,
constants . BILL_TYPE_REVERSE_MAPPING ,
)
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 "
return 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 "
return answer_dict
# Explicit Codes (run always)
code_answer_dict = code_explicit ( service_clean , " " )
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 )
return answer_dict
# 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 )
return answer_dict
# 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 )
return answer_dict
# 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 )
return 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 "
return answer_dict
def fill_claim_type ( answer_dicts ) :
"""
Fills in the AARETE_DERIVED_CLAIM_TYPE_CD field in each answer_dict with the mode of the existing values.
If the field is already populated, it remains unchanged.
Args:
answer_dicts (list[dict]): List of dictionaries containing 1:1 and 1:N fields (after merge process)
Returns:
list[dict]: List of dictionaries with AARETE_DERIVED_CLAIM_TYPE
"""
2025-08-05 20:46:19 +00:00
claim_types = [
d . get ( " AARETE_DERIVED_CLAIM_TYPE_CD " )
for d in answer_dicts
if d . get ( " AARETE_DERIVED_CLAIM_TYPE_CD " )
]
2025-08-01 22:36:01 +00:00
if not claim_types :
return answer_dicts
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
mode_claim_type = max ( set ( claim_types ) , key = claim_types . count )
for answer_dict in answer_dicts :
if not answer_dict . get ( " AARETE_DERIVED_CLAIM_TYPE_CD " ) :
answer_dict [ " AARETE_DERIVED_CLAIM_TYPE_CD " ] = mode_claim_type
return answer_dicts
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.
"""
2025-08-05 20:46:19 +00:00
2025-08-01 22:36:01 +00:00
# Fill empty AARETE_DERIVED_CLAIM_TYPE_CD with mode
2025-08-05 20:46:19 +00:00
answer_dicts = fill_claim_type ( merged_results . to_dict ( orient = " records " ) )
2025-08-01 22:36:01 +00:00
2025-08-26 15:43:53 +00:00
answer_dicts_with_code = [ ]
2025-08-01 22:36:01 +00:00
for answer_dict in answer_dicts :
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 )
2025-08-26 15:43:53 +00:00
answer_dicts_with_code . append ( answer_dict )
2025-08-29 21:13:24 +00:00
# Fill in GROUPER_CD_DESC based on GROUPER_TYPE and GROUPER_CD
answer_dicts_with_code = [
fill_grouper_cd_desc ( answer_dict , constants ) for answer_dict in answer_dicts_with_code
]
2025-08-26 15:43:53 +00:00
return pd . DataFrame ( answer_dicts_with_code )
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 " )
final_answer_dicts = [ ]
GROUPER_QUESTIONS = FieldSet (
file_path = config . FIELD_JSON_PATH , field_type = " grouper "
) . print_prompt_dict ( )
for answer_dict in answer_dicts_with_code :
if (
string_utils . is_empty ( answer_dict . get ( " GROUPER_TYPE " ) )
and not string_utils . is_empty ( answer_dict . get ( " GROUPER_CD " ) )
) :
# run groper breakout prompt
grouper_breakout_prompt = prompt_templates . GROUPER_BREAKOUT (
answer_dict . get ( " SERVICE_TERM " , " " ) ,
answer_dict . get ( " REIMB_TERM " , " " ) ,
GROUPER_QUESTIONS
)
claude_answer_raw = llm_utils . invoke_claude (
grouper_breakout_prompt , " sonnet_latest " , " "
)
try :
grouper_answer = string_utils . universal_json_load ( claude_answer_raw )
except Exception as e :
grouper_answer = { " GROUPER_TYPE " : e }
answer_dict . update ( grouper_answer )
2025-08-01 22:36:01 +00:00
final_answer_dicts . append ( answer_dict )
2025-08-05 20:46:19 +00:00
2025-08-26 15:43:53 +00:00
return pd . DataFrame ( final_answer_dicts )
2025-08-01 22:36:01 +00:00