From 806c272dc550616282cadb45258ccc10f62be36b Mon Sep 17 00:00:00 2001 From: Alex Galarce Date: Thu, 29 May 2025 22:33:12 +0000 Subject: [PATCH] Merged in feature/further-reimb-primary-improvements (pull request #546) Feature/further reimb primary improvements * Refactor identify_reimbursement_exhibits to return both EXPLICIT and IMPLICIT values * Moving from # Debugging lines to `logger` * Enhance "lesser of" and exception language instructions in reimbursement terms * clarify unclear print/logging statements * Replace debugging print statements with logging in get_reimbursement_primary function * Add logging for page processing in run_one_to_n_prompts function Approved-by: Katon Minhas --- .../src/investment/file_processing.py | 9 +++-- .../src/investment/one_to_n_funcs.py | 35 ++++++++++--------- fieldExtraction/src/investment/preprocess.py | 8 +++-- fieldExtraction/src/preprocessing_funcs.py | 10 ++++-- .../src/prompts/investment_prompts.py | 27 +++++++++++--- 5 files changed, 59 insertions(+), 30 deletions(-) diff --git a/fieldExtraction/src/investment/file_processing.py b/fieldExtraction/src/investment/file_processing.py index ed7cd25..e609598 100644 --- a/fieldExtraction/src/investment/file_processing.py +++ b/fieldExtraction/src/investment/file_processing.py @@ -2,6 +2,8 @@ import csv import json import os +import logging + import pandas as pd import src.investment.aarete_derived as aarete_derived import src.investment.dynamic_funcs as dynamic_funcs @@ -57,8 +59,8 @@ def process_file(file_object, all_dataset, run_timestamp): text_dict, top_sheet_dict = preprocess.split_text(contract_text) text_dict = preprocess.clean_tables(text_dict, filename) exhibit_pages, exhibit_chunk_mapping = preprocess.one_to_n_exhibit_chunking(text_dict, filename) - # print(f"Exhibit Pages: {exhibit_pages}") # Debugging line - # print(f"Exhibit Chunk Mapping: {exhibit_chunk_mapping}") # Debugging line + logging.debug(f"Exhibit Pages: {exhibit_pages}") + logging.debug(f"Exhibit Chunk Mapping: {exhibit_chunk_mapping}") print(f"{datetime_str()} Preprocessing Complete - {filename}") ################## ONE TO N ################## @@ -150,11 +152,12 @@ def run_one_to_n_prompts(filename, text_dict, exhibit_pages, exhibit_chunk_mappi reimbursement_exhibits = one_to_n_funcs.identify_reimbursement_exhibits(all_exhibit_headers, filename) pages_to_process = preprocess.get_pages_to_process(reimbursement_exhibits, exhibit_chunk_mapping) - # print(f"Pages to process: {pages_to_process}") # Debugging line + logging.debug(f"Pages to process: {pages_to_process}") ################## RUN PROMPTS ################## one_to_n_results = [] for page in pages_to_process: + logging.debug(f"Processing page: {page}") # Use individual page text for subpages, full chunk for regular pages if '.' in page: # Subpage - get the text for this specific page diff --git a/fieldExtraction/src/investment/one_to_n_funcs.py b/fieldExtraction/src/investment/one_to_n_funcs.py index 34d3575..3915065 100644 --- a/fieldExtraction/src/investment/one_to_n_funcs.py +++ b/fieldExtraction/src/investment/one_to_n_funcs.py @@ -1,6 +1,7 @@ import hashlib import re from typing import Callable +import logging import src.constants.investment_values as investment_values import src.investment.code_funcs as code_funcs @@ -68,10 +69,9 @@ def identify_reimbursement_exhibits(exhibit_headers: dict, filename: str) -> lis Returns: list[str]: List of page numbers (as strings) that likely contain reimbursement terms. """ - # print(exhibit_headers) # Debugging line - # print(type(exhibit_headers)) # Debugging line prompt = investment_prompts.IDENTIFY_REIMBURSEMENT_EXHIBITS_PROMPT(exhibit_headers, yaml_output=True) # Trying YAML output - # print(prompt) # Debugging line + logging.debug(f"""Identifying reimbursement exhibits for {filename} with prompt: +{prompt}""") llm_answer_raw = llm_utils.invoke_claude( prompt, config.MODEL_ID_CLAUDE35_SONNET, filename, 2000 ) @@ -83,32 +83,33 @@ def identify_reimbursement_exhibits(exhibit_headers: dict, filename: str) -> lis if lines[0].strip().lower() == 'yaml': yaml_content = '\n'.join(lines[1:]) - # print(llm_answer_raw) # Debugging line + logging.debug(f"""LLM raw output for {filename}: +{llm_answer_raw}""") # print("Extracted YAML content:\n", yaml_content) # Debugging line + logging.debug(f"""YAML content for {filename}: +{yaml_content}""") try: # Parse the YAML output into a dictionary exhibits_dict = yaml.safe_load(yaml_content) - # If any are values are EXPLICIT, return ONLY those that are EXPLICIT - if any(v == 'EXPLICIT' for v in exhibits_dict.values()): - return [str(page) for page, value in exhibits_dict.items() # Convert back to string - if value == 'EXPLICIT'] - else: - return [str(page) for page, value in exhibits_dict.items() # Convert back to string - if value == 'IMPLICIT'] + # Return both EXPLICIT and IMPLICIT exhibits (exclude only 'NO') + return [str(page) for page, value in exhibits_dict.items() + if value in ['EXPLICIT', 'IMPLICIT']] except yaml.YAMLError as e: - print(f"Error parsing YAML: {e}") - print(f"Raw LLM output: {llm_answer_raw}") + logging.error(f"Error parsing YAML: {e}") + logging.error(f"Raw LLM output: {llm_answer_raw}") return list(exhibit_headers.keys()) # Return all keys of the exhibit headers if parsing fails def get_reimbursement_primary(reimbursement_level_fields, exhibit_text, filename): field_prompts = reimbursement_level_fields.get_prompt_dict() prompt = investment_prompts.REIMBURSEMENT_LEVEL_PRIMARY(exhibit_text, field_prompts) - # print(prompt) # Debugging line + logging.debug(f"""Running reimbursement primary prompt for {filename} with prompt: +{prompt}""") llm_answer_raw = llm_utils.invoke_claude( prompt, config.MODEL_ID_CLAUDE35_SONNET, filename, max_tokens=25000 ) - # print(llm_answer_raw) # Debugging line + logging.debug(f"""LLM raw output for {filename}: +{llm_answer_raw}""") # Check for the special "no results" case if "NO_REIMBURSEMENT_TERMS_FOUND" in llm_answer_raw: @@ -117,8 +118,8 @@ def get_reimbursement_primary(reimbursement_level_fields, exhibit_text, filename try: llm_answer_final = string_utils.universal_json_load(llm_answer_raw) except ValueError as e: - print(f"Error parsing LLM response: {e}") - print(f"Raw LLM output: {llm_answer_raw}") + logging.error(f"Error parsing LLM response: {e}") + logging.error(f"Raw LLM output: {llm_answer_raw}") raise return llm_answer_final diff --git a/fieldExtraction/src/investment/preprocess.py b/fieldExtraction/src/investment/preprocess.py index 70f4fe4..1ef71ad 100644 --- a/fieldExtraction/src/investment/preprocess.py +++ b/fieldExtraction/src/investment/preprocess.py @@ -3,6 +3,8 @@ from src import config, keywords, preprocessing_funcs import src.investment.smart_chunking_funcs as smart_chunking_funcs from src import keywords, preprocessing_funcs +import logging + def clean_text(contract_text): @@ -100,17 +102,17 @@ def get_pages_to_process(reimbursement_exhibits: list, exhibit_chunk_mapping: di # Find all pages mapped to this exhibit exhibit_pages_in_chunk = [page for page, mapped_exhibit in exhibit_chunk_mapping.items() if mapped_exhibit == exhibit_page] - + logging.debug(f"Exhibit {exhibit_page} contains pages: {exhibit_pages_in_chunk}") # Check if any pages are subpages (contain '.') has_subpages = any('.' in page for page in exhibit_pages_in_chunk) if has_subpages: # Split into individual subpages - print(f"Exhibit {exhibit_page} has subpages - processing individually: {exhibit_pages_in_chunk}") + logging.debug(f"Exhibit {exhibit_page} has subpages - will process individually: {exhibit_pages_in_chunk}") pages_to_process.extend(exhibit_pages_in_chunk) else: # Keep as one chunk (use the exhibit_page as representative) - print(f"Exhibit {exhibit_page} is regular - processing as chunk") + logging.debug(f"Exhibit {exhibit_page} is regular - will process as a single chunk") pages_to_process.append(exhibit_page) return pages_to_process diff --git a/fieldExtraction/src/preprocessing_funcs.py b/fieldExtraction/src/preprocessing_funcs.py index c49af1d..ab325b5 100644 --- a/fieldExtraction/src/preprocessing_funcs.py +++ b/fieldExtraction/src/preprocessing_funcs.py @@ -1,4 +1,5 @@ import re +import logging from src.prompts import investment_prompts import src.utils.llm_utils as llm_utils @@ -269,12 +270,15 @@ def get_exhibit_pages(text_dict: dict[str, str], filename: str) -> list[str]: for subpage in subpages: page_content = text_dict[subpage] prompt = investment_prompts.EXHIBIT_CHECK(page_content[0:200]) - # print(f"Prompting LLM for exhibit check on page {subpage}...") # Debugging line - # print("Prompt:", prompt) # Debugging line + logging.debug(f"Prompting LLM for exhibit check on page {subpage}...") + logging.debug(f"""Prompt for exhibit check: +{prompt} + """) claude_answer_raw = llm_utils.invoke_claude( prompt, config.MODEL_ID_CLAUDE3_HAIKU, filename, max_tokens=150 ) - # print("Claude answer:", claude_answer_raw) # Debugging line + logging.debug(f"""Claude answer for exhibit check on page {subpage}: +{claude_answer_raw}""") claude_answer_extracted = string_utils.extract_text_from_delimiters( claude_answer_raw, Delimiter.PIPE ) diff --git a/fieldExtraction/src/prompts/investment_prompts.py b/fieldExtraction/src/prompts/investment_prompts.py index f5543ae..73f8ceb 100644 --- a/fieldExtraction/src/prompts/investment_prompts.py +++ b/fieldExtraction/src/prompts/investment_prompts.py @@ -388,10 +388,15 @@ If no reimbursement terms are found, return the text "NO_REIMBURSEMENT_TERMS_FOU [C. CRITICAL INSTRUCTIONS] LESSER OF LANGUAGE: When a contract contains "lesser of" statements: - - Include with EVERY affected rate/service - - Include even when separated from the actual rate information - - Format consistently as "lesser of X and Y" - - When a contract contains a global "lesser of" statement (such as "the lesser of the rates listed below or Provider's billed charges"), this statement applies to EVERY SINGLE RATE in the ENTIRE fee schedule that follows, regardless of format (percentage vs. dollar amount), location in the table (top, middle, or bottom), service type (OB, anesthesia, lab services, etc.), or grouping or formatting within the table + - SPECIFIC "LESSER OF": Statements like "Payment shall be based on the lesser of X or Y" + - GLOBAL CAPS: Statements like "At no time shall Plan pay more than..." or "shall not exceed Provider's billed charges" + - When exception phrases appear (e.g., "with the exception of"), they only apply to the SPECIFIC "lesser of" statement, NOT to global caps + - Global caps apply to ALL rates throughout the contract, regardless of exceptions + +EXCEPTION LANGUAGE: When contracts contain exception clauses: + - Exception phrases create carve-outs from SPECIFIC "lesser of" statements only + - Exception items should combine: [their listed rate] + [any global caps that still apply] + - Global caps (like "not to exceed billed charges") remain in effect even for exception items CONTEXTUAL COMPLETENESS: The full payment context must be preserved: - "The maximum compensation shall be..." + "lesser of" statements @@ -436,6 +441,20 @@ Answer: "[{{'SERVICE_TERM' : 'Covered Services that are Medicare Covered Service Text: 'Covered Services rendered to Covered Person who are eligible for Medicare and enrolled in a MMP Plan that may include coverage for both Medicare and Medicaid Covered Services' Answer: "[{{'SERVICE_TERM' : 'Covered Services rendered to Covered Person who are eligible for Medicare and enrolled in a MMP Plan that may include coverage for both Medicare and Medicaid Covered Services', ...}}]" +[EXAMPLE 5] +Here is an example with a "lesser of" statement followed by exceptions, but NO additional global caps: +Text: 'Payment for services shall be based on the lesser of 75% of AHCCCS Fee for Service rates or Provider's charges less any applicable Co-Payments, with the exception of those services listed below. +{{'HOPOS': ['A6544', 'L3201'], 'Description': ['COMPRESSION STOCKING', 'ORTHOPEDIC SHOE'], 'Amount': ['$ 25.00', '$ 30.00']}}' + +Answer: "[{{'SERVICE_TERM': 'COMPRESSION STOCKING A6544', 'REIMB_TERM': '$ 25.00', ...}}, +{{'SERVICE_TERM': 'ORTHOPEDIC SHOE L3201', 'REIMB_TERM': '$ 30.00', ...}}]" + +[EXAMPLE 6] +Text: 'Payment shall be based on the lesser of 75% of AHCCCS rates or Provider's charges, with the exception of services below. At no time shall Plan pay more than Provider's billed charges. +A6544: $25.00' + +Answer: "[{{'SERVICE_TERM': 'A6544', 'REIMB_TERM': 'lesser of $25.00 and Provider's billed charges', ...}}]" + [E. ANALYSIS CONTEXT] [Start Context] {context.replace('"', "'")}