From 81dfe7896b08c982e9816ffdf2a501b47bfeca01 Mon Sep 17 00:00:00 2001 From: Katon Minhas Date: Fri, 21 Feb 2025 19:55:12 +0000 Subject: [PATCH] Merged in field/stop-loss (pull request #407) StopLoss, Outliers, and Sequestration * changed any method to all method in list handling * code mapping moved outside to run once * Merge branch 'feature/daip2-97' into bugfix/proc-code-lists * merge conflict fixed * merge conflicts fixed * incorporated latest changes from main * Merged main into bugfix/proc-code-lists * Move load all dataset to io_utils * Fix list is_empty * Updated poetry.lock * Bugfix * Docstring * Docstrings * Merge branch 'main' into bugfix/proc-code-lists * Merge branch 'bugfix/proc-code-lists' into bugfix/dynamic-assignment-issue * Add outliers, stop-loss, and sequestration to reimbursement-level primary prompt template * Add stop loss fields * Add outlier fields * Add sequestration fields * Remove test * Docstrings * Update test for no default exhibit_level_answer_dict * Merge branch 'bugfix/dynamic-assignment-issue' into field/stop-loss * Merge branch 'main' into field/stop-loss * Merged main into field/stop-loss Approved-by: Alex Galarce --- .../src/constants/investment_values.py | 6 ++ .../src/investment/carveout_funcs.py | 31 +++++++ .../src/investment/file_processing.py | 3 + .../src/investment/one_to_n_funcs.py | 55 +++++++----- .../src/investment/special_case_funcs.py | 84 +++++++++++++++++++ .../src/prompts/investment_prompts.json | 52 ++++++------ .../src/prompts/investment_prompts.py | 31 +++++-- 7 files changed, 207 insertions(+), 55 deletions(-) create mode 100644 fieldExtraction/src/investment/carveout_funcs.py create mode 100644 fieldExtraction/src/investment/special_case_funcs.py diff --git a/fieldExtraction/src/constants/investment_values.py b/fieldExtraction/src/constants/investment_values.py index 5a03dc5..06e0cd4 100644 --- a/fieldExtraction/src/constants/investment_values.py +++ b/fieldExtraction/src/constants/investment_values.py @@ -139,5 +139,11 @@ VALID_CARVEOUTS = { "READMISSIONS" : "Describes payment for subsequent admission with the same diagnostic category of codes as the initial admission.", } +VALID_SPECIAL = { + "OUTLIER" : "Describes additional payments made on amounts over a certain threshold for high-cost facility claims. Look for the term 'Outlier'.", + "STOP_LOSS" : "Describes additional payments made on amounts over a certain threshold for high-cost facility claims. Look for the term 'Stop Loss'.", + "SEQUESTRATION" : "Describes a government-mandated reduction in federal spending for a period of time. Look for the term 'Sequestration'." +} + levels = ["cpt", "hcpcs", "cpt_level3", "cpt_level2", "hcpcs_level2", "cpt_level1", "hcpcs_level1"] diff --git a/fieldExtraction/src/investment/carveout_funcs.py b/fieldExtraction/src/investment/carveout_funcs.py new file mode 100644 index 0000000..e2c84c2 --- /dev/null +++ b/fieldExtraction/src/investment/carveout_funcs.py @@ -0,0 +1,31 @@ + + + + + + +def get_carveout_fields(answer_dict: dict, + carveout_answer: str, + filename: str): + """ + Updates the answer dictionary with carveout-related fields based on the given carveout answer. + + Args: + answer_dict (dict): Dictionary of answers to update. + carveout_answer (str): The carveout code or description. + filename (str): The name of the file being processed. + + Returns: + dict: The updated answer dictionary with carveout fields populated. + """ + + # Carveout code and indicator + answer_dict['CONTRACT_CARVEOUT_CD'] = carveout_answer + answer_dict['CONTRACT_CARVEOUT_IND'] = 'N' if 'N/A' in carveout_answer else 'Y' + + # Default indicator + answer_dict['CONTRACT_DEFAULT_IND'] = 'N' + + # Put Carveout-specific breakouts below + + return answer_dict diff --git a/fieldExtraction/src/investment/file_processing.py b/fieldExtraction/src/investment/file_processing.py index 6577297..25789ca 100644 --- a/fieldExtraction/src/investment/file_processing.py +++ b/fieldExtraction/src/investment/file_processing.py @@ -36,6 +36,7 @@ def process_file(file_object, all_dataset, run_timestamp): one_to_one_results = run_one_to_one_prompts(filename, contract_text, text_dict, top_sheet_dict) # Return df one_to_one_results['CONTRACT_FILE_NAME'] = filename print(f"{datetime_str()} One to One Complete - {filename}") + # one_to_one_results = {"CONTRACT_FILE_NAME" : filename} ################## ONE TO N ################## if string_utils.contains_reimbursement(contract_text): @@ -99,6 +100,8 @@ def run_one_to_n_prompts(filename, text_dict, exhibit_pages, exhibit_chunk_mappi exhibit_chunk = string_utils.get_exhibit_chunk(text_dict, exhibit_chunk_mapping, exhibit_page) if string_utils.contains_reimbursement(exhibit_chunk): + print(exhibit_page) + ################## INITIALIZE FIELDS ################## reimbursement_level_fields = FieldSet(relationship="one_to_n", field_type="reimbursement_level", file_path=FIELD_JSON_PATH) exhibit_level_fields = FieldSet(relationship="one_to_n", field_type="exhibit_level", file_path=FIELD_JSON_PATH) diff --git a/fieldExtraction/src/investment/one_to_n_funcs.py b/fieldExtraction/src/investment/one_to_n_funcs.py index 76ef150..b9d087a 100644 --- a/fieldExtraction/src/investment/one_to_n_funcs.py +++ b/fieldExtraction/src/investment/one_to_n_funcs.py @@ -10,6 +10,9 @@ from src import config, postprocessing_funcs from src.enums.delimiters import Delimiter from src.prompts.investment_prompts import FieldSet, Field import src.prompts.investment_prompts as investment_prompts +import src.constants.investment_values as investment_values +import src.investment.carveout_funcs as carveout_funcs +import src.investment.special_case_funcs as special_case_funcs from src.config import FIELD_JSON_PATH from src.regex.regex_utils import find_regex_matches @@ -147,7 +150,7 @@ def get_reimbursement_primary(reimbursement_level_fields, exhibit_text, filename llm_answer_final = string_utils.universal_json_load(llm_answer_raw) return llm_answer_final -def get_carveout_ind(reimbursement_primary_answers: list[dict], filename: str) -> list[dict]: +def get_special_cases(reimbursement_primary_answers: list[dict], filename: str) -> list[dict]: """ Determines carveout indicators for reimbursement answers using LLM-based classification. @@ -159,28 +162,36 @@ def get_carveout_ind(reimbursement_primary_answers: list[dict], filename: str) - list[dict]: Updated list with carveout indicators and default indicators added. Fields Added: CONTRACT_CARVEOUT_IND, CONTRACT_DEFAULT_IND, CONTRACT_CARVEOUT_CD """ - carveout_answers = [] + special_case_answers = [] for answer_dict in reimbursement_primary_answers: contract_service_cd, contract_reimbursement_method = answer_dict.get("CONTRACT_SERVICE_CD_OR_DESC"), answer_dict.get("CONTRACT_REIMBURSEMENT_METHOD") + print(contract_service_cd) if contract_service_cd and contract_reimbursement_method: llm_answer_final = string_utils.extract_text_from_delimiters( llm_utils.invoke_claude( - investment_prompts.CARVEOUT_CHECK(contract_service_cd, contract_reimbursement_method), + investment_prompts.SPECIAL_CASE_CHECK(contract_service_cd, contract_reimbursement_method), config.MODEL_ID_CLAUDE35_SONNET, filename ), Delimiter.PIPE) - if 'DEFAULT_TERM' in llm_answer_final: - answer_dict['CONTRACT_CARVEOUT_CD'] = 'N/A' - answer_dict['CONTRACT_DEFAULT_IND'] = 'Y' - else: - answer_dict['CONTRACT_CARVEOUT_CD'] = llm_answer_final - answer_dict['CONTRACT_CARVEOUT_IND'] = 'N' if 'N/A' in llm_answer_final else 'Y' - answer_dict['CONTRACT_DEFAULT_IND'] = 'N' - carveout_answers.append(answer_dict) + print(f"LLM Answers Final: {llm_answer_final}") - return carveout_answers + # Default + if "DEFAULT_TERM" in llm_answer_final: + answer_dict["CONTRACT_CARVEOUT_CD"] = "N/A" + answer_dict["CONTRACT_CARVEOUT_IND"] = "N" + answer_dict["CONTRACT_DEFAULT_IND"] = "Y" + # Carveout + elif any([carveout in llm_answer_final for carveout in investment_values.VALID_CARVEOUTS]): + answer_dict = carveout_funcs.get_carveout_fields(answer_dict, llm_answer_final, filename) + # Special Case (Additional Consideration) + elif any([case in llm_answer_final for case in investment_values.VALID_SPECIAL]): + answer_dict = special_case_funcs.get_special_case(answer_dict, llm_answer_final, filename) + + special_case_answers.append(answer_dict) + + return special_case_answers def get_methodology_breakout(reimbursement_primary_answers: list[dict], filename: str) -> list[dict]: """ @@ -260,7 +271,7 @@ def get_service_breakout(answer_dicts, filename): if service_methodology not in already_seen: service_breakout_dict = string_utils.universal_json_load( llm_utils.invoke_claude( - investment_prompts.SERVICE_BREAKOUT(service, methodology, service_breakout_questions), + investment_prompts.SERVICE_METHODOLOGY_BREAKOUT(service, methodology, service_breakout_questions), config.MODEL_ID_CLAUDE35_SONNET, filename ) @@ -346,11 +357,15 @@ def reimbursement_level(exhibit_text, filename, reimbursement_level_fields, all_ dict: The parsed LLM response as a list of dictionaries """ reimbursement_primary_answers = get_reimbursement_primary(reimbursement_level_fields, exhibit_text, filename) # Returns list of dicts - carveout_answers = get_carveout_ind(reimbursement_primary_answers, filename) + + if reimbursement_primary_answers: # If any reimbursements found + carveout_answers = get_special_cases(reimbursement_primary_answers, filename) + + # Later, we will only run methodology breakout for those without certain carveout indicators. We will run carveout-specific breakouts for the others + methodology_breakout_answers = get_methodology_breakout(carveout_answers, filename) + service_breakout_answers = get_service_breakout(methodology_breakout_answers, filename) + code_breakout_answers = code_funcs.get_code_breakout(service_breakout_answers, filename, all_dataset) - # Later, we will only run methodology breakout for those without certain carveout indicators. We will run carveout-specific breakouts for the others - methodology_breakout_answers = get_methodology_breakout(carveout_answers, filename) - service_breakout_answers = get_service_breakout(methodology_breakout_answers, filename) - code_breakout_answers = code_funcs.get_code_breakout(service_breakout_answers, filename, all_dataset) - - return code_breakout_answers + return code_breakout_answers + else: + return [] diff --git a/fieldExtraction/src/investment/special_case_funcs.py b/fieldExtraction/src/investment/special_case_funcs.py new file mode 100644 index 0000000..8db1d6b --- /dev/null +++ b/fieldExtraction/src/investment/special_case_funcs.py @@ -0,0 +1,84 @@ + +import src.utils.string_utils as string_utils +import src.utils.llm_utils as llm_utils +import src.prompts.investment_prompts as investment_prompts +import src.config as config +from src.prompts.investment_prompts import Field, FieldSet + +# These functions are virtually identical currently - TODO: explore combining them to one function +def get_outlier_fields(answer_dict, filename): + service, methodology = answer_dict.get("CONTRACT_SERVICE_CD_OR_DESC"), answer_dict.get("CONTRACT_REIMBURSEMENT_METHOD") + answer_dict["CONTRACT_FACILITY_OUTLIER_TERMS"] = methodology + questions = FieldSet(file_path=config.FIELD_JSON_PATH, field_type="outlier") + llm_answer_final = string_utils.universal_json_load( + llm_utils.invoke_claude( + investment_prompts.SERVICE_METHODOLOGY_BREAKOUT(service, methodology, questions), + config.MODEL_ID_CLAUDE35_SONNET, + filename + ) + ) + return {**answer_dict, **llm_answer_final} + + +def get_stop_loss_fields(answer_dict, filename): + service, methodology = answer_dict.get("CONTRACT_SERVICE_CD_OR_DESC"), answer_dict.get("CONTRACT_REIMBURSEMENT_METHOD") + answer_dict["CONTRACT_FACILITY_STOP_LOSS_TERMS"] = methodology + questions = FieldSet(file_path=config.FIELD_JSON_PATH, field_type="stop_loss") + llm_answer_final = string_utils.universal_json_load( + llm_utils.invoke_claude( + investment_prompts.SERVICE_METHODOLOGY_BREAKOUT(service, methodology, questions), + config.MODEL_ID_CLAUDE35_SONNET, + filename + ) + ) + return {**answer_dict, **llm_answer_final} + +def get_sequestration_fields(answer_dict, filename): + service, methodology = answer_dict.get("CONTRACT_SERVICE_CD_OR_DESC"), answer_dict.get("CONTRACT_REIMBURSEMENT_METHOD") + questions = FieldSet(file_path=config.FIELD_JSON_PATH, field_type="sequestration") + llm_answer_final = string_utils.universal_json_load( + llm_utils.invoke_claude( + investment_prompts.SERVICE_METHODOLOGY_BREAKOUT(service, methodology, questions), + config.MODEL_ID_CLAUDE35_SONNET, + filename + ) + ) + return {**answer_dict, **llm_answer_final} + + +def get_special_case(answer_dict: dict, + special_answer: str, + filename: str): + """ + Processes special case answers by setting default carveout and default indicators, + then populates additional fields based on the special answer type. + + Args: + answer_dict (dict): Dictionary of answers to update. + special_answer (str): A string indicating the special case type (e.g., "OUTLIER", "STOP_LOSS", "SEQUESTRATION"). + filename (str): The name of the file being processed, used in further processing functions. + + Returns: + dict: The updated answer dictionary with special case fields populated. + """ + + # Carveouts + answer_dict["CONTRACT_CARVEOUT_CD"] = "N/A" + answer_dict["CONTRACT_CARVEOUT_IND"] = "N" + + # Default + answer_dict["CONTRACT_DEFAULT_IND"] = "N" + + # Individual + if "OUTLIER" in special_answer: + answer_dict = get_outlier_fields(answer_dict, filename) + elif "STOP_LOSS" in special_answer: + answer_dict = get_stop_loss_fields(answer_dict, filename) + elif "SEQUESTRATION" in special_answer: + answer_dict = get_sequestration_fields(answer_dict, filename) + + # TODO: Note that currently - for Outlier and Stop-Loss, there is FACILITY_OUTLIER_EXCLUSION_DESC and FACILITY_STOP_LOSS_EXCLUSION_DESC. + # As of now, these are being pulled in the prompts. In the future, change these fields to be mapped using the PROC code mapping system + # They should map from FACILITY_OUTLIER_EXCLUSION_CD or FACILITY_STOP_LOSS_EXCLUSION_CD + return answer_dict + diff --git a/fieldExtraction/src/prompts/investment_prompts.json b/fieldExtraction/src/prompts/investment_prompts.json index daa1b3b..bd347fe 100644 --- a/fieldExtraction/src/prompts/investment_prompts.json +++ b/fieldExtraction/src/prompts/investment_prompts.json @@ -491,32 +491,32 @@ { "field_name": "FACILITY_OUTLIER_FIXED_LOSS_THRESHOLD", "relationship": "one_to_n", - "field_type": "TBD", - "prompt": "TBD" + "field_type": "outlier", + "prompt": "What is the threshold amount to be compared to the cost of the claim, whereby if the cost exceeds the threshold, the outlier payment is calculated on the excess amount. This may be a percentage or a dollar value." }, { "field_name": "FACILITY_OUTLIER_PERCENT_RATE_ON_EXCESS_CHARGES", "relationship": "one_to_n", - "field_type": "TBD", - "prompt": "TBD" + "field_type": "outlier", + "prompt": "What is the percent rate that is applied to costs that exceed the outlier threshold?" }, { "field_name": "FACILITY_OUTLIER_MAXIMUM", "relationship": "one_to_n", - "field_type": "TBD", - "prompt": "TBD" + "field_type": "outlier", + "prompt": "If there is a cap on the total amount to be paid, write that cap here. If there is not, write N/A." }, { "field_name": "FACILITY_OUTLIER_EXCLUSION_CD", "relationship": "one_to_n", - "field_type": "TBD", - "prompt": "TBD" + "field_type": "outlier", + "prompt": "If the text specifies that any codes are to be excluded from the outlier calculation, write those codes here. If not, write 'N/A'." }, { "field_name": "FACILITY_OUTLIER_EXCLUSION_DESC", "relationship": "one_to_n", - "field_type": "TBD", - "prompt": "TBD" + "field_type": "outlier", + "prompt": "Describe anything that is excluded from the outlier calculation. Write only the exact text from the contract. If the text does not indicate that anything is excluded, write 'N/A'." }, { "field_name": "CONTRACT_FACILITY_STOP_LOSS_TERMS", @@ -527,50 +527,50 @@ { "field_name": "FACILITY_STOP_LOSS_FIXED_LOSS_THRESHOLD", "relationship": "one_to_n", - "field_type": "TBD", - "prompt": "TBD" + "field_type": "stop_loss", + "prompt": "What is the threshold amount to be compared to the cost of the claim, whereby if the cost exceeds the threshold, the stop-loss payment is calculated on the excess amount. This may be a percentage or a dollar value." }, { "field_name": "FACILITY_STOP_LOSS_PERCENT_RATE_ON_EXCESS_CHARGES", "relationship": "one_to_n", - "field_type": "TBD", - "prompt": "TBD" + "field_type": "stop_loss", + "prompt": "What is the percent rate that is applied to costs that exceed the stop-loss threshold?" }, { "field_name": "FACILITY_STOP_LOSS_DAILY_MAXIMUM", "relationship": "one_to_n", - "field_type": "TBD", - "prompt": "TBD" + "field_type": "stop_loss", + "prompt": "If there is a cap on the total amount to be paid each day, write that cap here. If there is not, write N/A." }, { "field_name": "FACILITY_STOP_LOSS_EXCLUSION_CD", "relationship": "one_to_n", - "field_type": "TBD", - "prompt": "TBD" + "field_type": "stop_loss", + "prompt": "If the text specifies that any codes are to be excluded from the stop-loss calculation, write those codes here. If not, write 'N/A'." }, { "field_name": "FACILITY_STOP_LOSS_EXCLUSION_DESC", "relationship": "one_to_n", - "field_type": "TBD", - "prompt": "TBD" + "field_type": "stop_loss", + "prompt": "Describe anything that is excluded from the stop-loss calculation. Write only the exact text from the contract. If the text does not indicate that anything is excluded, write 'N/A'." }, { "field_name": "CONTRACT_SEQUESTRATION_PERCENT_RATE", "relationship": "one_to_n", - "field_type": "TBD", - "prompt": "TBD" + "field_type": "sequestration", + "prompt": "What is quantity of the sequestration reduction? This is typically found as a percent value." }, { "field_name": "CONTRACT_SEQUESTRATION_START_DATE", "relationship": "one_to_n", - "field_type": "TBD", - "prompt": "TBD" + "field_type": "sequestration", + "prompt": "What is the Effective Date of the sequestration reduction? If none is specified, write 'N/A'." }, { "field_name": "CONTRACT_SEQUESTRATION_END_DATE", "relationship": "one_to_n", - "field_type": "TBD", - "prompt": "TBD" + "field_type": "sequestration", + "prompt": "What is the Termination Date of the sequestration reduction? If none is specified, write 'N/A'." }, { "field_name": "CONTRACT_DISCOUNT_PERCENT_RATE", diff --git a/fieldExtraction/src/prompts/investment_prompts.py b/fieldExtraction/src/prompts/investment_prompts.py index e8d2ac5..6cd1294 100644 --- a/fieldExtraction/src/prompts/investment_prompts.py +++ b/fieldExtraction/src/prompts/investment_prompts.py @@ -1,4 +1,4 @@ -from src.constants.investment_values import VALID_LOBS, VALID_CONTRACT_NETWORKS, VALID_PROGRAMS, VALID_CONTRACT_CLAIM_TYPE, VALID_REIMBURSEMENT_METHOD, VALID_CARVEOUTS, VALID_PROV_TYPE +from src.constants.investment_values import VALID_LOBS, VALID_CONTRACT_NETWORKS, VALID_PROGRAMS, VALID_CONTRACT_CLAIM_TYPE, VALID_REIMBURSEMENT_METHOD, VALID_CARVEOUTS, VALID_PROV_TYPE, VALID_SPECIAL import src.constants.valid as valid import pandas as pd from functools import cache @@ -289,7 +289,10 @@ Return one json object for each combination of attributes seen. It is possible t Here are the attributes to be included in each dictionary, and instructions on how to correctly answer: {fields} -Ensure any relevant detail is included, including any and all codes relating to the service and any rates in tables, if applicable. Include any 'Lesser of' statement that applies to the reimbursement as part of the CONTRACT_REIMBURSEMENT_METHOD. The 'Lesser of' statement might not be found in immediate proximity to the reimbursement term and may instead be found in a paragraph above. If the methodology is presented in a table, ensure each rate in the table has it's own object. Also, concatenate any relevant lesser of statement that applies to the table with the portion of the methodology found in the table. +Ensure any relevant detail is included, including any and all codes relating to the service and any rates in tables, if applicable. +Include any 'Lesser of' statement that applies to the reimbursement as part of the CONTRACT_REIMBURSEMENT_METHOD. The 'Lesser of' statement might not be found in immediate proximity to the reimbursement term and may instead be found in a paragraph above. +If the methodology is presented in a table, ensure each rate in the table has it's own object. Also, concatenate any relevant lesser of statement that applies to the table with the portion of the methodology found in the table. +Ensure that any and all rates related to Outliers, Stop Loss, and Sequestration are captured. Here are some examples of language with additional context to be included in the CONTRACT_SERVICE_CD_OR_DESC: Text: 'Covered Services that are Medicare Covered Services and are not Medicaid Covered Services', @@ -342,7 +345,15 @@ Use the text in the methodology to populate a JSON dictionary with the following Write your JSON dictionary below: """ -def SERVICE_BREAKOUT(service, methodology, questions): +def SERVICE_METHODOLOGY_BREAKOUT(service, methodology, questions): + """ + Use this prompt for any fields that are derived directly from the combination of Service and Methodology + + Current Uses: + - SERVICE_BREAKOUT - AARETE_DERIVED_PROV_TYPE + - STOP_LOSS_BREAKOUT - 5 Stop-Loss fields + - OUTLIER_BREAKOUT - 5 Outlier fields + """ return f"""Analyze the given Service and Methodology, and answer the following question(s): Populate a JSON dictionary with the following fields: @@ -474,19 +485,21 @@ The term length appears in two places but I'm selecting... """ -def CARVEOUT_CHECK(service, methodology): +def SPECIAL_CASE_CHECK(service, methodology): carveout_definitions = {carveout : definition for carveout, definition in VALID_CARVEOUTS.items() if definition} # Get only not None - return f"""Examine the Service and Methodology combination below and determine if the terms fall under one of the following Carveout definitions: + special_definitions = {special : definition for special, definition in VALID_SPECIAL.items() if definition} + all_questions = {**carveout_definitions, **special_definitions} + return f"""Examine the Service and Methodology combination below and determine if the terms fall under one of the following special cases: -Here are the Carveouts, with their definitions: -{carveout_definitions} +Here are the Cases, with their definitions: +{all_questions} Here is the Service and Methodology: Service: {service} Methodology: {methodology} -Determine if the Service and Methodology match any of the Carveout descriptions. -If so, write the name of the Carveout. If they do not, simply write N/A. +Determine if the Service and Methodology match any of the descriptions. +If so, write the name of the Case. If they do not, simply write N/A. {PIPE_FORMAT_INSTRUCTIONS} """