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
This commit is contained in:
Alex Galarce
2025-05-29 22:33:12 +00:00
parent 9db0fb328e
commit 806c272dc5
5 changed files with 59 additions and 30 deletions
@@ -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
@@ -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
+5 -3
View File
@@ -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
+7 -3
View File
@@ -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
)
@@ -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('"', "'")}