69f23653a1
Bugfix/dynamic issues * Add debug print statements for dynamic primary field discovery and 1:1 escalation tracking * Add reimb_term to bill type CD determination context - Updated fill_bill_type() to accept reimb_term parameter - Modified FILL_BILL_TYPE prompt to include reimbursement term in context - First attempt now uses: service term + reimbursement term - Second attempt uses: service term + reimbursement term + exhibit text - Updated extract_codes_from_service() to pass REIMB_TERM to fill_bill_type() * Remove debug print statements from dynamic_funcs.py and file_processing.py - Removed all DEBUG print statements for dynamic primary discovery - Removed debug prints for field discovery status and 1:1 escalation evaluation - Removed debug prints for exhibit level answers and final summaries - Removed debug comment about one_to_one_fields creation - Cleaned up all temporary debugging code * Merge branch 'main' into bugfix/dynamic_issues * Remove duplicate one_to_one_fields FieldSet initialization from process_file Approved-by: Katon Minhas
785 lines
30 KiB
Python
785 lines
30 KiB
Python
import ast
|
||
import json
|
||
import logging
|
||
import os
|
||
import re
|
||
|
||
import pandas as pd
|
||
import src.config as config
|
||
import src.prompts.prompt_templates as prompt_templates
|
||
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
|
||
from src.prompts.fieldset import FieldSet
|
||
|
||
|
||
def clean_service(service, constants: Constants) -> str:
|
||
"""
|
||
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.
|
||
"""
|
||
|
||
if string_utils.is_empty(service):
|
||
return ""
|
||
|
||
service = service.strip().upper()
|
||
|
||
# Map common values
|
||
for acronym, full_form in constants.SYNONYM_MAP.items():
|
||
if string_utils.is_empty(acronym) or string_utils.is_empty(full_form):
|
||
continue
|
||
service = re.sub(rf"\b{re.escape(acronym)}\b", full_form, service)
|
||
|
||
# Remove other values
|
||
for term in constants.REMOVAL_LIST:
|
||
if term is None:
|
||
continue
|
||
service = re.sub(rf"\b{re.escape(term)}\b", "", service).strip()
|
||
service = re.sub(r"\s+", " ", service)
|
||
if service is None:
|
||
return ""
|
||
service = service.replace(" - ", "-")
|
||
|
||
if any([v in service for v in ["UNLISTED", "UNCATEGORIZED", "NON-LISTED"]]):
|
||
return "UNLISTED"
|
||
|
||
# Check if all remaining words are stop words
|
||
remaining_words = service.split()
|
||
if all(word in constants.STOP_WORD_LIST for word in remaining_words):
|
||
return ""
|
||
|
||
return service
|
||
|
||
|
||
def get_regex_answers(service, code_answer_dict):
|
||
"""
|
||
**UNUSED** Extracts CPT and Revenue codes from the service string using regex patterns.
|
||
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.
|
||
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.
|
||
"""
|
||
|
||
# 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
|
||
cpt_pattern = (
|
||
r"\b(?:[A-Z]\d{4}|\d{5})(?:\s*(?:-|through)\s*(?:[A-Z]\d{4}|\d{5}))?\b"
|
||
)
|
||
cpt_matches = re.findall(cpt_pattern, service)
|
||
if len(cpt_matches) == 1:
|
||
code_answer_dict["PROCEDURE_CD"] = cpt_matches[0].replace("through", "-")
|
||
|
||
if "rev" in service.lower():
|
||
rev_pattern = r"\b(?:\d{3}|\d{2}X)(?:\s*(?:-|through)\s*(?:\d{3}|\d{2}X))?\b"
|
||
rev_matches = re.findall(rev_pattern, service)
|
||
if len(rev_matches) > 0:
|
||
code_answer_dict["REVENUE_CD"] = str(rev_matches)
|
||
|
||
return code_answer_dict
|
||
|
||
|
||
def code_explicit(service, methodology, filename):
|
||
"""
|
||
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
|
||
dictionary format.
|
||
Args:
|
||
service (str): The service name or identifier. If empty, an empty dictionary
|
||
is returned.
|
||
code_primary_questions (str): The primary questions or prompts to be used
|
||
for generating the response.
|
||
filename (str): The name of the file associated with the operation, used
|
||
for logging or tracking purposes.
|
||
Returns:
|
||
dict: A dictionary containing the parsed response from the language model.
|
||
Returns an empty dictionary if the service string is empty.
|
||
"""
|
||
|
||
# Prompt
|
||
code_primary_questions = FieldSet(
|
||
file_path=config.FIELD_JSON_PATH, field_type="code_primary_breakout"
|
||
).print_prompt_dict()
|
||
claude_answer_raw = llm_utils.invoke_claude(
|
||
prompt_templates.CODE_EXPLICIT(service, methodology, code_primary_questions),
|
||
"sonnet_latest",
|
||
filename,
|
||
)
|
||
code_answer_dict = string_utils.universal_json_load(claude_answer_raw)
|
||
return code_answer_dict
|
||
|
||
|
||
def code_category(service, proc_category, hcpcs_level2_mapping, filename):
|
||
"""
|
||
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:
|
||
matching_values += [
|
||
value
|
||
for key, value in hcpcs_level2_mapping.items()
|
||
if key.startswith(category)
|
||
]
|
||
claude_answer_raw = llm_utils.invoke_claude(
|
||
prompt_templates.CODE_CATEGORY(service, matching_values),
|
||
"sonnet_latest",
|
||
filename,
|
||
)
|
||
claude_answer_final = string_utils.universal_json_load(
|
||
claude_answer_raw
|
||
) # Returns list
|
||
|
||
code_answer_dict = {"PROCEDURE_CD": [], "PROCEDURE_CD_DESC": []}
|
||
for answer in claude_answer_final:
|
||
if "0000" in answer:
|
||
code_answer_dict["PROCEDURE_CD"].append(answer)
|
||
code_answer_dict["PROCEDURE_CD_DESC"].append("")
|
||
elif answer in hcpcs_level2_mapping.values():
|
||
code = list(hcpcs_level2_mapping.keys())[
|
||
list(hcpcs_level2_mapping.values()).index(answer)
|
||
]
|
||
code_answer_dict["PROCEDURE_CD"].append(str(code))
|
||
code_answer_dict["PROCEDURE_CD_DESC"].append(str(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(
|
||
prompt_templates.CODE_IMPLICIT_SPECIAL(service), "sonnet_latest", filename
|
||
)
|
||
claude_answer_final = string_utils.extract_text_from_delimiters(
|
||
claude_answer_raw, Delimiter.PIPE
|
||
)
|
||
|
||
special_case_mapping = {
|
||
"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"],
|
||
}
|
||
code_answer_dict = {}
|
||
if claude_answer_final in special_case_mapping:
|
||
code_answer_dict["PROCEDURE_CD"] = special_case_mapping[claude_answer_final]
|
||
code_answer_dict["PROCEDURE_CD_DESC"] = claude_answer_final
|
||
|
||
return code_answer_dict
|
||
|
||
|
||
def get_embedding_levels(level_suffix, implicit_run_dict, constants: Constants):
|
||
"""
|
||
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:
|
||
cpt_mapping = constants.CPT_LEVEL1_MAPPING
|
||
hcpcs_mapping = constants.HCPCS_LEVEL1_MAPPING
|
||
rev_mapping = constants.REV_LEVEL1_MAPPING
|
||
embedding_levels = []
|
||
if implicit_run_dict["PROCEDURE_CD"]:
|
||
embedding_levels += [
|
||
f"cpt_level{level_suffix}",
|
||
f"hcpcs_level{level_suffix}",
|
||
]
|
||
if implicit_run_dict["REVENUE_CD"]:
|
||
embedding_levels += [f"rev_level{level_suffix}"]
|
||
|
||
elif level_suffix == 2:
|
||
cpt_mapping = constants.CPT_LEVEL2_MAPPING
|
||
hcpcs_mapping = constants.HCPCS_LEVEL2_MAPPING
|
||
rev_mapping = constants.REV_MAPPING
|
||
|
||
embedding_levels = []
|
||
if implicit_run_dict["PROCEDURE_CD"]:
|
||
embedding_levels += [
|
||
f"cpt_level{level_suffix}",
|
||
f"hcpcs_level{level_suffix}",
|
||
]
|
||
if implicit_run_dict["REVENUE_CD"]:
|
||
embedding_levels += [f"rev"]
|
||
|
||
return embedding_levels, cpt_mapping, hcpcs_mapping, rev_mapping
|
||
|
||
|
||
def get_match_list(service, embedding_levels, constants: Constants, top_k: int):
|
||
"""
|
||
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.
|
||
"""
|
||
target_vec = constants.EMBEDDING_MODEL.encode(
|
||
[service], normalize_embeddings=True
|
||
).astype("float32")
|
||
match_list = []
|
||
highest_similarity = 0
|
||
for level in embedding_levels:
|
||
embedding = constants.get_embedding(level)
|
||
|
||
similarity_scores, match_indices = embedding.index.search(target_vec, top_k)
|
||
if max(similarity_scores[0]) > highest_similarity:
|
||
highest_similarity = max(similarity_scores[0])
|
||
|
||
for idx, score in zip(match_indices[0], similarity_scores[0]):
|
||
match_list.append(embedding.choices[idx])
|
||
|
||
return match_list, highest_similarity
|
||
|
||
|
||
def code_implicit_rag(service, implicit_run_dict, filename, constants):
|
||
"""
|
||
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
|
||
embedding_levels, cpt_mapping, hcpcs_mapping, rev_mapping = (
|
||
get_embedding_levels(level_suffix, implicit_run_dict, constants)
|
||
)
|
||
if not embedding_levels:
|
||
return {}
|
||
|
||
# Get Best Matches
|
||
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,
|
||
}
|
||
)
|
||
|
||
# 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:
|
||
embedding_levels, cpt_mapping, hcpcs_mapping, rev_mapping = (
|
||
get_embedding_levels(
|
||
level_dict["level_suffix"], implicit_run_dict, constants
|
||
)
|
||
)
|
||
|
||
# Run Prompt
|
||
claude_answer_raw = llm_utils.invoke_claude(
|
||
prompt_templates.CODE_IMPLICIT(service, level_dict["match_list"]),
|
||
"sonnet_latest",
|
||
filename,
|
||
)
|
||
|
||
try:
|
||
claude_answer_final = string_utils.universal_json_load(claude_answer_raw)
|
||
except Exception as e:
|
||
return {"CODE_METHODOLOGY": e}
|
||
|
||
if not claude_answer_final:
|
||
continue
|
||
|
||
# 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():
|
||
matching_codes = [
|
||
str(key) for key, val in cpt_mapping.items() if val == description
|
||
]
|
||
if matching_codes:
|
||
# Split pipe-delimited codes into individual codes
|
||
for code in matching_codes:
|
||
proc_codes.extend(code.split("|"))
|
||
proc_descs.append(description)
|
||
if description in hcpcs_mapping.values():
|
||
matching_codes = [
|
||
str(key) for key, val in hcpcs_mapping.items() if val == description
|
||
]
|
||
if matching_codes:
|
||
# Split pipe-delimited codes into individual codes
|
||
for code in matching_codes:
|
||
proc_codes.extend(code.split("|"))
|
||
proc_descs.append(description)
|
||
if description in rev_mapping.values():
|
||
matching_codes = [
|
||
str(key) for key, val in rev_mapping.items() if val == description
|
||
]
|
||
if matching_codes:
|
||
# Split pipe-delimited codes into individual codes
|
||
for code in matching_codes:
|
||
rev_codes.extend(code.split("|"))
|
||
rev_descs.append(description)
|
||
|
||
# Combine answers
|
||
if proc_codes:
|
||
code_answer_dict["PROCEDURE_CD"] = proc_codes
|
||
code_answer_dict["PROCEDURE_CD_DESC"] = proc_descs
|
||
code_answer_dict["CODE_METHODOLOGY"] = (
|
||
f"Implicit - Level {level_dict['level_suffix']}"
|
||
)
|
||
if rev_codes:
|
||
code_answer_dict["REVENUE_CD"] = rev_codes
|
||
code_answer_dict["REVENUE_CD_DESC"] = rev_descs
|
||
code_answer_dict["CODE_METHODOLOGY"] = (
|
||
f"Implicit - Level {level_dict['level_suffix']}"
|
||
)
|
||
|
||
if code_answer_dict:
|
||
return code_answer_dict
|
||
|
||
return {}
|
||
|
||
|
||
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(
|
||
prompt_templates.CODE_LAST_CHECK(service), "sonnet_latest", filename
|
||
)
|
||
claude_answer_final = string_utils.extract_text_from_delimiters(
|
||
claude_answer_raw, Delimiter.PIPE
|
||
)
|
||
return claude_answer_final
|
||
|
||
|
||
def fill_bill_type(service, answer_dict, BILL_TYPE_MAPPING, BILL_TYPE_REVERSE_MAPPING, reimb_term=None, exhibit_text=None):
|
||
"""
|
||
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.
|
||
BILL_TYPE_MAPPING (dict): Mapping from codes to descriptions.
|
||
BILL_TYPE_REVERSE_MAPPING (dict): Mapping from descriptions to codes.
|
||
reimb_term (str, optional): The reimbursement term for additional context.
|
||
exhibit_text (str, optional): Full exhibit text for contextual analysis.
|
||
|
||
Returns:
|
||
dict: The updated answer dictionary.
|
||
"""
|
||
|
||
valid_bill_type = sorted(list(set(BILL_TYPE_MAPPING.values())))
|
||
|
||
# First attempt: Service term + reimbursement term
|
||
claude_answer_raw = llm_utils.invoke_claude(
|
||
prompt_templates.FILL_BILL_TYPE(service, valid_bill_type, reimb_term=reimb_term, exhibit_text=None),
|
||
"sonnet_latest",
|
||
""
|
||
)
|
||
claude_answer_final = string_utils.universal_json_load(claude_answer_raw)
|
||
|
||
# Second attempt: Service term + reimbursement term + exhibit context (if first attempt failed and context available)
|
||
if not claude_answer_final and exhibit_text:
|
||
claude_answer_raw = llm_utils.invoke_claude(
|
||
prompt_templates.FILL_BILL_TYPE(service, valid_bill_type, reimb_term=reimb_term, exhibit_text=exhibit_text),
|
||
"sonnet_latest",
|
||
""
|
||
)
|
||
claude_answer_final = string_utils.universal_json_load(claude_answer_raw)
|
||
|
||
# Keep legacy behavior: return answer_dict unchanged if no result found
|
||
if not claude_answer_final:
|
||
return answer_dict
|
||
|
||
bill_codes, bill_descs = [], []
|
||
for description in claude_answer_final:
|
||
if description in BILL_TYPE_REVERSE_MAPPING:
|
||
bill_codes.append(BILL_TYPE_REVERSE_MAPPING[description])
|
||
bill_descs.append(description)
|
||
|
||
if bill_codes:
|
||
answer_dict["BILL_TYPE_CD"] = "|".join(bill_codes)
|
||
answer_dict["BILL_TYPE_CD_DESC"] = "|".join(bill_descs)
|
||
|
||
return answer_dict
|
||
|
||
|
||
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:
|
||
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
|
||
|
||
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
|
||
|
||
|
||
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 = {}
|
||
|
||
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
|
||
|
||
|
||
def normalize_answer_dict_codes(answer_dict):
|
||
"""
|
||
Normalizes code fields in an answer dictionary to JSON list format.
|
||
|
||
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"]
|
||
|
||
for field in fields_to_normalize:
|
||
if field in answer_dict:
|
||
answer_dict[field] = string_utils.normalize_to_json_list(answer_dict[field])
|
||
|
||
return answer_dict
|
||
|
||
|
||
def extract_codes_from_service(answer_dict, constants: Constants):
|
||
"""
|
||
Extracts codes from the service term in the answer dictionary.
|
||
|
||
Args:
|
||
answer_dict (dict): Dictionary containing, at minimum, the SERVICE_TERM field.
|
||
Returns:
|
||
dict: Dictionary containing extracted codes and their descriptions.
|
||
|
||
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
|
||
"""
|
||
|
||
service, bill_type = answer_dict.get("SERVICE_TERM"), answer_dict.get(
|
||
"BILL_TYPE_CD_DESC"
|
||
)
|
||
filename = answer_dict.get("FILENAME", "")
|
||
methodology = answer_dict.get("REIMB_TERM", "")
|
||
|
||
if string_utils.is_empty(service):
|
||
return {}
|
||
|
||
# Fill Bill Type if not filled
|
||
if string_utils.is_empty(bill_type):
|
||
exhibit_text = answer_dict.get("EXHIBIT_TEXT", None)
|
||
reimb_term = answer_dict.get("REIMB_TERM", None)
|
||
answer_dict = fill_bill_type(
|
||
service,
|
||
answer_dict,
|
||
constants.BILL_TYPE_MAPPING,
|
||
constants.BILL_TYPE_REVERSE_MAPPING,
|
||
reimb_term=reimb_term,
|
||
exhibit_text=exhibit_text,
|
||
)
|
||
|
||
# Get list of codes we want implicit - based on claim type
|
||
implicit_run_dict = get_implicit_runs(answer_dict)
|
||
|
||
# Preprocessing
|
||
service_clean = clean_service(service, constants)
|
||
|
||
# Exit point for unlisted
|
||
if service_clean == "UNLISTED":
|
||
answer_dict["PROCEDURE_CD_DESC"] = "UNLISTED"
|
||
answer_dict["CODE_METHODOLOGY"] = "UNLISTED"
|
||
return normalize_answer_dict_codes(answer_dict)
|
||
|
||
# Exit point if N/A or in DO_NOT_RUN list
|
||
if string_utils.is_empty(service_clean) or any(
|
||
[v in service_clean for v in constants.DO_NOT_RUN]
|
||
):
|
||
answer_dict["CODE_METHODOLOGY"] = "Generic - Before Prompts"
|
||
return normalize_answer_dict_codes(answer_dict)
|
||
|
||
# Explicit Codes (run always)
|
||
code_answer_dict = code_explicit(service_clean, methodology, filename)
|
||
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
|
||
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 normalize_answer_dict_codes(answer_dict)
|
||
|
||
# Implicit Codes: Code Categories
|
||
if any(["Category:" in v for v in code_answer_dict.get("PROCEDURE_CD", "")]):
|
||
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
|
||
code_answer_dict["CODE_METHODOLOGY"] = "Implicit - Letter Category"
|
||
answer_dict.update(code_answer_dict)
|
||
return normalize_answer_dict_codes(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 normalize_answer_dict_codes(answer_dict)
|
||
|
||
# Implicit Codes: Levels 1 and 2
|
||
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
|
||
answer_dict.update(code_answer_dict)
|
||
return normalize_answer_dict_codes(answer_dict)
|
||
|
||
# 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 normalize_answer_dict_codes(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
|
||
"""
|
||
claim_types = [
|
||
d.get("AARETE_DERIVED_CLAIM_TYPE_CD")
|
||
for d in answer_dicts
|
||
if d.get("AARETE_DERIVED_CLAIM_TYPE_CD")
|
||
]
|
||
if not claim_types:
|
||
return answer_dicts
|
||
|
||
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
|
||
|
||
|
||
def code_breakout(merged_results: pd.DataFrame, constants: Constants):
|
||
"""
|
||
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.
|
||
"""
|
||
|
||
# Fill empty AARETE_DERIVED_CLAIM_TYPE_CD with mode
|
||
answer_dicts = fill_claim_type(merged_results.to_dict(orient="records"))
|
||
|
||
answer_dicts_with_code = []
|
||
for answer_dict in answer_dicts:
|
||
code_answer_dict = extract_codes_from_service(answer_dict, constants)
|
||
answer_dict.update(code_answer_dict)
|
||
answer_dicts_with_code.append(answer_dict)
|
||
|
||
# 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
|
||
]
|
||
|
||
df = pd.DataFrame(answer_dicts_with_code)
|
||
|
||
# Normalize code columns to JSON list format
|
||
for col in ["PROCEDURE_CD", "CPT4_PROC_MOD", "REVENUE_CD", "DIAG_CD", "GROUPER_CD", "NDC_CD", "CLAIM_ADMIT_TYPE_CD", "CLAIM_STATUS_CD"]:
|
||
if col in df.columns:
|
||
df[col] = df[col].apply(string_utils.normalize_to_json_list)
|
||
|
||
return df
|
||
|
||
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_breakout"
|
||
).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)
|
||
final_answer_dicts.append(answer_dict)
|
||
|
||
return pd.DataFrame(final_answer_dicts) |