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
This commit is contained in:
Katon Minhas
2025-02-21 19:55:12 +00:00
parent b296cd187b
commit 81dfe7896b
7 changed files with 207 additions and 55 deletions
@@ -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"]
@@ -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
@@ -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)
@@ -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 []
@@ -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
@@ -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",
@@ -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}
"""