From ebc7960dbdecb42e9b3ad226d8cbe9c41fa5822a Mon Sep 17 00:00:00 2001 From: Katon Minhas Date: Tue, 7 Oct 2025 21:59:31 +0000 Subject: [PATCH] Merged in bugfix/mi-issue-fixes (pull request #727) Bugfix/mi issue fixes * Update mapping to allow for , or | lists * Pipe-separation for full-context * Update VALIDATE_REIMBURSEMENTS * Designate Provider Submission Form as cover sheet language - make cover sheet detection more efficient * remove deprecated functions * remove test Approved-by: Faizan Mohiuddin --- .../src/investment/aarete_derived.py | 12 +- .../src/investment/preprocessing_funcs.py | 143 ++---------------- .../src/investment/prompt_calls.py | 3 +- .../src/prompts/prompt_templates.py | 46 +++--- 4 files changed, 49 insertions(+), 155 deletions(-) diff --git a/fieldExtraction/src/investment/aarete_derived.py b/fieldExtraction/src/investment/aarete_derived.py index d691c7e..593e34d 100644 --- a/fieldExtraction/src/investment/aarete_derived.py +++ b/fieldExtraction/src/investment/aarete_derived.py @@ -72,9 +72,19 @@ def get_crosswalk_fields(answer_dicts: list, constants: Constants): if not string_utils.is_empty( from_field_value ) and string_utils.is_empty(to_field_value): - from_field_value_list = from_field_value.split("|") + # Get list of values to map + if "|" in from_field_value: + from_field_value_list = from_field_value.split("|") + elif "," in from_field_value: + from_field_value_list = from_field_value.split(",") + else: + from_field_value_list = [from_field_value] + + # Map each value in the list to_field_answer_list = [] for individual_from_field_value in from_field_value_list: + individual_from_field_value = individual_from_field_value.strip() + if individual_from_field_value in crosswalk.mapping.keys(): to_field_answer_list.append(crosswalk.mapping.get( individual_from_field_value diff --git a/fieldExtraction/src/investment/preprocessing_funcs.py b/fieldExtraction/src/investment/preprocessing_funcs.py index 5a0ed16..fbe681c 100644 --- a/fieldExtraction/src/investment/preprocessing_funcs.py +++ b/fieldExtraction/src/investment/preprocessing_funcs.py @@ -27,7 +27,6 @@ def remove_page_indicators(contract_text: str) -> str: return cleaned_text -# TODO: write unit tests def split_text(text: str) -> dict[str, str]: """Split text on pages by the string `Start of Page No. = ' @@ -69,123 +68,6 @@ def clean_law_symbols(contract_text): return contract_text -# ORIGINAL -def chunk_consecutive_og(text_dict, exhibit_pages): - # If needed - extend page 1 to page 1 and 2, then cut chunking off after (edge case: compensation terms not found on first page of exhibit) - - reimbursement_pages = [ - page_num - for page_num in text_dict.keys() - if string_utils.contains_reimbursement(text_dict, page_num) - ] - - page_dict = {} - current_exhibit = None - for page_num in text_dict.keys(): - - # Page is Reimbursement AND Exhibit - if page_num in reimbursement_pages and page_num in exhibit_pages: - current_exhibit = page_num - page_dict[page_num] = [page_num] - - # Page is Reimbursement NOT Exhibit - elif page_num in reimbursement_pages and page_num not in exhibit_pages: - if current_exhibit: - page_dict[current_exhibit].append(page_num) - else: - page_dict[page_num] = [page_num] - - # Page is Exhibit NOT Reimbursement - elif page_num in exhibit_pages and page_num not in reimbursement_pages: - current_exhibit = page_num - page_dict[page_num] = [page_num] - - # Page is NOT Exhibit NOT Reimbursment - elif page_num not in exhibit_pages and page_num not in reimbursement_pages: - current_exhibit = None - page_dict[page_num] = [page_num] - - final_dict = {page_num: "" for page_num in page_dict.keys()} - for page_num in page_dict.keys(): - for p in page_dict[page_num]: - final_dict[page_num] += text_dict[p] - return final_dict - - -def chunk_consecutive(text_dict, exhibit_pages): - exhibit_pages = set(str(page) for page in exhibit_pages) - reimbursement_pages = { - page_num - for page_num in text_dict.keys() - if string_utils.contains_reimbursement(text_dict, page_num) - } - - page_dict = {} - current_chunk_start = None - last_exhibit = None - in_exhibit = False - - def word_count(text): - return len(text.split()) - - for page_str in sorted(text_dict.keys(), key=int): - page_num = int(page_str) - - if page_str in exhibit_pages: - current_chunk_start = page_str - last_exhibit = page_str - in_exhibit = True - page_dict[current_chunk_start] = [page_str] - # print(f"found page {page_num} in exhibit, starting new chunk") - - if page_str in reimbursement_pages: - if not in_exhibit or current_chunk_start is None: - # Check if this page has less than 200 words and should be added to the previous chunk - if current_chunk_start and word_count(text_dict[page_str]) < 200: - page_dict[current_chunk_start].append(page_str) - # print(f"found page {page_num} in reimbursement with less than 200 words, adding to previous chunk") - else: - current_chunk_start = page_str - page_dict[current_chunk_start] = [page_str] - # print(f"found page {page_num} in reimbursement, starting new chunk") - else: - page_dict[current_chunk_start].append(page_str) - # print(f"found page {page_num} in reimbursement, adding to current chunk") - else: - if in_exhibit and current_chunk_start is not None: - page_dict[current_chunk_start].append(page_str) - # print(f"found non-reimbursement page {page_num} in exhibit, adding to current chunk") - else: - in_exhibit = False - - # Check next page if it's non-reimbursement and not in exhibit - next_page_str = str(page_num + 1) - if ( - next_page_str in text_dict - and next_page_str not in reimbursement_pages - and next_page_str not in exhibit_pages - ): - if current_chunk_start is not None: - page_dict[current_chunk_start].append(next_page_str) - # print(f'next page {next_page_str} was found to be a non-reimbursement and added') - - # If we've moved past the last exhibit page, reset in_exhibit - if in_exhibit and int(page_str) > int(last_exhibit): - in_exhibit = False - - # Remove duplicates and sort page numbers in each chunk - for key in page_dict: - page_dict[key] = sorted(list(set(page_dict[key])), key=int) - - final_dict = {page_num: "" for page_num in page_dict.keys()} - for page_num in page_dict.keys(): - for p in page_dict[page_num]: - final_dict[page_num] += text_dict[p] - - # print("Final page_dict:", {k: v for k, v in page_dict.items()}) - return final_dict - - def filter_quick_review(text_dict): """ Cover Sheets (aka Top Sheets or Quick Review pages) are pages stapled to the front of the contract that contain manually written summaries of the contract's contents. @@ -204,19 +86,18 @@ def filter_quick_review(text_dict): - The first dictionary contains pages that do not have the specified keywords. - The second dictionary includes pages that contain any of the specified keywords. """ - return { - page_num: page_text - for page_num, page_text in text_dict.items() - if "QUICK REVIEW" not in page_text.upper() - and "TOP SHEET" not in page_text.upper() - and "COVER SHEET" not in page_text.upper() - }, { - page_num: page_text - for page_num, page_text in text_dict.items() - if "QUICK REVIEW" in page_text.upper() - or "TOP SHEET" in page_text.upper() - or "COVER SHEET" in page_text.upper() - } + keywords = ["QUICK REVIEW", "TOP SHEET", "COVER SHEET", "PROVIDER SUBMISSION FORM"] + regular_pages = {} + quick_review_pages = {} + + for page_num, page_text in text_dict.items(): + page_text_upper = page_text.upper() + if any(keyword in page_text_upper for keyword in keywords): + quick_review_pages[page_num] = page_text + else: + regular_pages[page_num] = page_text + + return regular_pages, quick_review_pages def get_exhibit_pages( diff --git a/fieldExtraction/src/investment/prompt_calls.py b/fieldExtraction/src/investment/prompt_calls.py index 8208414..3f0836e 100644 --- a/fieldExtraction/src/investment/prompt_calls.py +++ b/fieldExtraction/src/investment/prompt_calls.py @@ -389,8 +389,9 @@ def validate_reimbursements_for_llm(answer_dict: dict, filename: str) -> bool: llm_response = llm_utils.invoke_claude( prompt, "sonnet_latest", - filename, # Use sonnet for now but if we can simplify our VALIDATE_REIMBURSEMENTS_PROMPT, we can switch back to haiku_latest + filename, ) + print(llm_response) logging.debug( f"LLM response for reimbursement validation in {filename}:\n{llm_response}" diff --git a/fieldExtraction/src/prompts/prompt_templates.py b/fieldExtraction/src/prompts/prompt_templates.py index d2c3d92..eb23cd5 100644 --- a/fieldExtraction/src/prompts/prompt_templates.py +++ b/fieldExtraction/src/prompts/prompt_templates.py @@ -775,9 +775,9 @@ def GROUP_TIN_NPI_TEMPLATE(key_sections: str, provider_list: str) -> str: def FULL_CONTEXT_ADDITIONAL_INSTRUCTIONS(allow_na): if allow_na: - return " Only provide an answer if that answer clearly applies to the ENTIRE contract. Otherwise, answer N/A." + return " Only provide an answer if that answer clearly applies to the ENTIRE contract. Otherwise, answer N/A. If there are multiple correct answers, return them in a pipe (|) separated list." else: - return " Do NOT leave this field N/A. Choose the most appropriate answer based on the context." + return " Do NOT leave this field N/A. Choose the most appropriate answer based on the context. If there are multiple answers, return them in a pipe (|) separated list." def ONE_TO_ONE_MULTI_FIELD_TEMPLATE(context, questions: dict[str, str]): @@ -879,14 +879,11 @@ def VALIDATE_REIMBURSEMENTS_PROMPT(service_term: str, reimb_term: str) -> str: Returns: str: A prompt for the AI to determine if the pair is a complete reimbursement methodology. """ - return f"""Analyze this service-reimbursement pair from a healthcare contract: + return f"""Analyze service-reimbursement pair from a healthcare contract: -SERVICE: {service_term} -REIMBURSEMENT TERM: {reimb_term} +Determine if the term represents a COMPLETE reimbursement methodology that specifies how payment will be calculated. -Determine if this represents a COMPLETE reimbursement methodology that specifies how payment will be calculated. - -VALID reimbursement methodologies include: +VALID entries - return YES: - Specific payment rates (percentages, dollar amounts) - Fee schedule references with rates - Calculation formulas @@ -895,27 +892,32 @@ VALID reimbursement methodologies include: - Capitation rates (PMPM, PMPY, per member costs) - Flat rates per service or episode -INVALID entries (return NO): +INVALID entries - return NO unless there is VALID language as well: - Service definitions without payment terms - Authorization/coverage requirements only -- Non-reimbursement statements ("not covered", "excluded", "not payable") -- General benefit descriptions - Processing instructions, funding arrangements, and administrative statements without specific rates -- Risk-sharing arrangements, surplus/deficit distributions, settlement payments -- Performance bonuses and incentive payments, or administrative compensation - Member cost responsibilities (including Medicare Member Cost Share) +HARD INVALID entries - return NO even if there is VALID reimbursement language present as well: +- Any reference to Coordination of Benefits or general benefit descriptions +- Risk-sharing arrangements, surplus/deficit distributions, settlement payments +- Performance bonuses and incentive payments, or administrative compensation +- Non-reimbursement statements ("not covered", "excluded", "not payable") Examples: -✅ "reimbursed at 100% of DMAP fee schedule" → YES (clear payment method) -✅ "flat rate of $20.00 per frame" → YES (specific rate) -✅ "$5.58 PMPM" → YES (capitation rate) -✅ "80% of Medicare allowable" → YES (percentage with reference) -❌ "Standard eyeglass lenses as defined by DMAP are CR39 lenses..." → NO (definition only) -❌ "Contact lenses must be prior authorized" → NO (authorization only) -❌ "Not payable per contract" → NO (no reimbursement) -❌ "payment to IPA equal to fifty percent (50%) of the Surplus" → NO (risk-sharing distribution) -❌ "Community shall process Clean Claims based on the then current Medicare or Medicaid Fee Schedule cost index as applicable to services rendered and according to Medicare reimbursement methodology, Including the geographic practice (GPCI), or Medicaid reimbursement methodology as applicable." → NO (definition only) +- "reimbursed at 100% of DMAP fee schedule" → YES (clear payment method) +- "flat rate of $20.00 per frame" → YES (specific rate) +- "$5.58 PMPM" → YES (capitation rate) +- "80% of Medicare allowable" → YES (percentage with reference) +- "Standard eyeglass lenses as defined by DMAP are CR39 lenses..." → NO (definition only) +- "Contact lenses must be prior authorized" → NO (authorization only) +- "Not payable per contract" → NO (no reimbursement) +- "payment to IPA equal to fifty percent (50%) of the Surplus" → NO (risk-sharing distribution) +- "... shall process Clean Claims based on the then current Medicare or Medicaid Fee Schedule cost index as applicable to services rendered and according to Medicare reimbursement methodology, Including the geographic practice (GPCI), or Medicaid reimbursement methodology as applicable." → NO (definition only) + +Here is the Term to analyze: +SERVICE: {service_term} +REIMBURSEMENT TERM: {reimb_term} Briefly explain your reasoning, then return YES or NO in |pipes|."""