2025-01-06 15:39:29 +00:00
import csv
2025-04-25 20:28:52 +00:00
import json
2025-05-29 22:33:12 +00:00
import logging
2025-06-04 19:49:10 +00:00
import os
2025-05-29 22:33:12 +00:00
2024-10-28 23:39:36 +00:00
import pandas as pd
2025-04-25 20:28:52 +00:00
import src . investment . aarete_derived as aarete_derived
import src . investment . dynamic_funcs as dynamic_funcs
2025-06-04 19:49:10 +00:00
import src . investment . investment_postprocessing_funcs as investment_postprocessing_funcs
2025-01-08 21:59:47 +00:00
import src . investment . one_to_n_funcs as one_to_n_funcs
2025-01-27 19:39:23 +00:00
import src . investment . one_to_one_funcs as one_to_one_funcs
2025-01-08 21:59:47 +00:00
import src . investment . postprocess as postprocess
import src . investment . preprocess as preprocess
2025-01-27 19:39:23 +00:00
import src . investment . smart_chunking_funcs as smart_chunking_funcs
import src . utils . io_utils as io_utils
2025-04-25 20:28:52 +00:00
import src . utils . string_utils as string_utils
from src import config
2025-01-27 21:27:15 +00:00
from src . config import FIELD_JSON_PATH
2025-04-25 20:28:52 +00:00
from src . investment . one_to_n_funcs import ( combine_one_to_n_answers ,
reimbursement_level )
from src . investment . tin_npi_funcs import ( merge_provider_info ,
reimbursement_tin_npi )
from src . prompts . investment_prompts import Field , FieldSet
from src . utils . string_utils import datetime_str
2025-04-08 18:29:39 +00:00
################## MERGE ##################
def merge_one_to_one_into_one_to_n ( one_to_n_results , one_to_one_results ) :
"""
Merges one_to_one_results into one_to_n_results.
Args:
one_to_n_results (pd.DataFrame): DataFrame with N rows and M columns, including FILE_NAME.
one_to_one_results (dict): Dictionary with several keys, including FILE_NAME.
Returns:
pd.DataFrame: Updated one_to_n_results with merged values from one_to_one_results.
"""
# Iterate over all key-value pairs in one_to_one_results
for k , v in one_to_one_results . items ( ) :
if k not in one_to_n_results . columns :
# If k is not a column in one_to_n_results, create it and populate all rows with v
one_to_n_results [ k ] = v
else :
# If k is a column in one_to_n_results, replace empty values with v
one_to_n_results [ k ] = one_to_n_results [ k ] . apply (
lambda row_val : v if string_utils . is_empty ( row_val ) else row_val
)
2025-06-04 19:49:10 +00:00
# Handle global lesser of injection
global_lesser_of = one_to_one_results . get ( " GLOBAL_LESSER_OF_STATEMENT " )
if global_lesser_of and global_lesser_of not in [ " N/A " , " UNKNOWN " ] :
logging . debug ( f " [ { one_to_one_results . get ( ' FILE_NAME ' , ' UNKNOWN ' ) } ] Injecting global lesser of logic: { global_lesser_of } " )
one_to_n_results = inject_global_lesser_of_rows ( one_to_n_results , global_lesser_of , one_to_one_results . get ( " FILE_NAME " , " UNKNOWN " ) )
else :
logging . debug ( f " [ { one_to_one_results . get ( ' FILE_NAME ' , ' UNKNOWN ' ) } ] No global lesser of logic found or applicable. " )
# Initialize the tracking flag when no global lesser of logic is found
one_to_n_results [ ' GLOBAL_LESSER_OF_APPLIED ' ] = " N "
2025-04-08 18:29:39 +00:00
return one_to_n_results
2025-06-04 19:49:10 +00:00
def inject_global_lesser_of_rows ( one_to_n_results : pd . DataFrame , global_lesser_of_stmt : str , filename : str ) - > pd . DataFrame :
""" Create new rows with global lesser of constraint for REIMB_IDs that lack lesser of logic.
For each row where LESSER_OF_IND= " N " , creates a duplicate row with the global lesser
of statement applied via methodology breakout. The original row is preserved with
LESSER_OF_IND updated to " Y " (since it ' s now subject to lesser of) and GLOBAL_LESSER_OF_APPLIED= " N " .
The new row has LESSER_OF_IND= " Y " and GLOBAL_LESSER_OF_APPLIED= " Y " . Both rows
share the same REIMB_ID.
Args:
one_to_n_results (pd.DataFrame): DataFrame containing one-to-n results with a ' LESSER_OF_IND ' column.
global_lesser_of_stmt (str): Global lesser of statement extracted from contract (e.g., " lesser of billed or allowable " )
filename (str): Name of the file being processed, used for logging and debugging.
Returns:
pd.DataFrame: Updated one_to_n_results with new rows injected for global lesser of logic.
"""
# Find rows that need global lesser of injection
rows_needing_injection = one_to_n_results [ one_to_n_results [ ' LESSER_OF_IND ' ] == " N " ] . copy ( )
if rows_needing_injection . empty :
logging . debug ( f " No rows needing global lesser of injection for { filename } . " )
# Still need to set tracking flag for all existing rows
one_to_n_results [ ' GLOBAL_LESSER_OF_APPLIED ' ] = " N "
return one_to_n_results
# Update original rows that lacked lesser of: they're now subject to lesser of due to
# global constraint
one_to_n_results . loc [ one_to_n_results [ ' LESSER_OF_IND ' ] == " N " , ' LESSER_OF_IND ' ] = " Y "
# Set tracking flag for all existing rows (both original injected rows and rows that already had lesser of logic) to "N"
# New rows will get "Y" for this flag in create_global_lesser_of_row
one_to_n_results [ ' GLOBAL_LESSER_OF_APPLIED ' ] = " N "
# Run methodology breakout on global statement (once)
global_methodology_breakout = one_to_one_funcs . run_global_lesser_of_breakout ( global_lesser_of_stmt , filename )
# Create new rows for each REIMB_ID that was lacking lesser of logic
new_rows = [ ]
for _ , original_row in rows_needing_injection . iterrows ( ) :
new_row = one_to_one_funcs . create_global_lesser_of_row ( original_row , global_methodology_breakout )
new_rows . append ( new_row )
# Append new rows to results
if new_rows :
new_rows_df = pd . DataFrame ( new_rows )
one_to_n_results = pd . concat ( [ one_to_n_results , new_rows_df ] , ignore_index = True )
logging . debug ( f " Injected { len ( new_rows ) } global lesser of rows for { filename } . " )
else :
logging . debug ( f " No new rows created for global lesser of injection for { filename } . " )
return one_to_n_results
2025-02-21 14:44:36 +00:00
def process_file ( file_object , all_dataset , run_timestamp ) :
2024-10-28 23:39:36 +00:00
filename , contract_text = file_object
2025-02-20 20:19:53 +00:00
print ( f " { datetime_str ( ) } Processing { filename } ... " )
2024-11-13 17:08:19 +00:00
2025-01-27 19:39:23 +00:00
################## PREPROCESS ##################
2025-01-10 17:24:44 +00:00
contract_text = preprocess . clean_text ( contract_text )
2025-01-27 19:39:23 +00:00
text_dict , top_sheet_dict = preprocess . split_text ( contract_text )
2025-04-23 22:00:02 +00:00
text_dict = preprocess . clean_tables ( text_dict , filename )
2025-06-09 21:35:20 +00:00
exhibit_dict , all_exhibit_headers = preprocess . one_to_n_exhibit_chunking ( text_dict , filename )
2025-02-20 20:19:53 +00:00
print ( f " { datetime_str ( ) } Preprocessing Complete - { filename } " )
2025-01-27 19:39:23 +00:00
################## ONE TO N ##################
if string_utils . contains_reimbursement ( contract_text ) :
2025-06-09 21:35:20 +00:00
one_to_n_results , dynamic_one_to_one_fields = run_one_to_n_prompts ( filename , exhibit_dict , all_exhibit_headers , all_dataset ) # Return df
2025-03-21 18:11:55 +00:00
one_to_n_results [ ' FILE_NAME ' ] = filename
2025-06-04 19:49:10 +00:00
one_to_n_results = investment_postprocessing_funcs . generate_reimb_ids ( one_to_n_results ) # Add reimb_id
2025-02-25 17:11:02 +00:00
print ( f " { datetime_str ( ) } One to N Complete - { filename } " )
2025-01-27 19:39:23 +00:00
else :
2025-03-21 18:11:55 +00:00
one_to_n_results = pd . DataFrame ( [ { ' FILE_NAME ' : filename } ] )
2025-04-08 18:29:39 +00:00
dynamic_one_to_one_fields = FieldSet ( )
2025-02-25 17:11:02 +00:00
print ( f " { datetime_str ( ) } No One to N Found, Skipping - { filename } " )
2025-05-22 15:04:19 +00:00
2025-01-28 20:51:19 +00:00
2025-02-25 17:11:02 +00:00
################## ONE TO ONE ##################
2025-04-23 22:00:02 +00:00
one_to_one_results = run_one_to_one_prompts ( filename , contract_text , text_dict , top_sheet_dict , dynamic_one_to_one_fields ) # Return dict
2025-03-21 18:11:55 +00:00
one_to_one_results [ ' FILE_NAME ' ] = filename
2025-02-25 17:11:02 +00:00
print ( f " { datetime_str ( ) } One to One Complete - { filename } " )
################## MERGE ##################
2025-04-08 18:29:39 +00:00
final_results = merge_one_to_one_into_one_to_n ( one_to_n_results , one_to_one_results )
2025-01-27 19:39:23 +00:00
################## POSTPROCESS ##################
final_df = postprocess . postprocess ( final_results )
2025-02-20 20:19:53 +00:00
print ( f " { datetime_str ( ) } Postprocessing Complete - { filename } " )
2025-01-27 19:39:23 +00:00
################## WRITE INDIVIDUAL ##################
if config . WRITE_TO_S3 :
io_utils . write_s3 ( final_df , filename , run_timestamp , " individual " )
else :
io_utils . write_local ( final_df , filename , " " , " individual " )
2025-02-20 20:19:53 +00:00
print ( f " { datetime_str ( ) } Writing Complete - { filename } " )
2025-01-27 19:39:23 +00:00
return final_df
2025-04-08 18:29:39 +00:00
def run_one_to_one_prompts ( filename , contract_text , text_dict , top_sheet_dict , dynamic_one_to_one_fields ) :
2024-10-28 23:39:36 +00:00
2025-01-27 19:39:23 +00:00
################## INITIALIZE FIELDS ##################
2025-04-08 18:29:39 +00:00
one_to_one_fields = FieldSet ( relationship = " one_to_one " , file_path = config . FIELD_JSON_PATH ) . combine ( dynamic_one_to_one_fields )
2024-12-02 19:50:41 +00:00
2025-01-27 19:39:23 +00:00
################## RUN REGEX ##################
2025-04-18 20:50:25 +00:00
regex_answers_dict = one_to_one_funcs . run_provider_info_fields ( contract_text , text_dict , filename )
one_to_one_results = { }
2025-04-25 20:28:52 +00:00
one_to_one_results [ ' PROV_INFO_JSON ' ] = json . dumps ( regex_answers_dict )
2025-04-18 20:50:25 +00:00
one_to_one_results = merge_provider_info ( one_to_one_results , regex_answers_dict )
2025-05-07 22:53:27 +00:00
# What if the TIN/NPI regexes don't find anything? Add PROV_GROUP_NAME_FULL to full context
if one_to_one_results . get ( " PROV_OTHER_NAME_FULL " ) == " NO_IDENTIFIERS_FOUND " : # This is how we know the regex didn't find anything
other_provider_name_field = Field . from_values (
field_name = " PROV_GROUP_NAME_FULL " ,
relationship = " one_to_one " ,
field_type = " full_context " ,
prompt = " What is the name of the group provider associated with this contract? Return the full name as it appears in the document. "
)
one_to_one_fields . add_field ( other_provider_name_field )
one_to_one_results [ " PROV_OTHER_NAME_FULL " ] = " UNKNOWN "
2025-01-10 17:24:44 +00:00
2024-12-06 18:19:58 +00:00
################## RUN SMART CHUNKED PROMPTS ##################
2025-01-27 19:39:23 +00:00
smart_chunked_answers_dict = one_to_one_funcs . run_smart_chunked_fields (
2025-05-07 18:21:09 +00:00
one_to_one_fields , contract_text , filename , text_dict
2024-12-06 18:19:58 +00:00
)
2024-10-28 23:39:36 +00:00
2024-12-06 18:19:58 +00:00
################## RUN FULL CONTEXT PROMPTS ##################
2025-01-27 19:39:23 +00:00
full_context_answers_dict = one_to_one_funcs . run_full_context_fields (
one_to_one_fields , contract_text , filename
2024-12-06 18:19:58 +00:00
)
2024-11-18 21:01:09 +00:00
2025-01-31 22:36:11 +00:00
################## COMBINE ANSWERS ################
2025-04-18 20:50:25 +00:00
one_to_one_results . update ( smart_chunked_answers_dict )
one_to_one_results . update ( full_context_answers_dict )
2025-01-31 22:36:11 +00:00
2025-04-08 18:29:39 +00:00
################## Crosswalk Fields ##################
one_to_one_results = aarete_derived . get_crosswalk_fields ( [ one_to_one_results ] )
2024-11-13 17:08:19 +00:00
2025-04-08 18:29:39 +00:00
################## Fill NA Mapping ##################
one_to_one_results = aarete_derived . fill_na_mapping ( one_to_one_results )
return one_to_one_results [ 0 ]
2024-10-28 23:39:36 +00:00
2025-06-09 21:35:20 +00:00
def run_one_to_n_prompts ( filename , exhibit_dict , all_exhibit_headers , all_dataset ) :
2025-06-03 20:47:42 +00:00
2025-01-27 19:39:23 +00:00
################## RUN PROMPTS ##################
2025-01-31 22:36:11 +00:00
one_to_n_results = [ ]
2025-06-09 21:35:20 +00:00
for exhibit_page in exhibit_dict . keys ( ) :
2025-06-03 20:47:42 +00:00
exhibit_text = exhibit_dict [ exhibit_page ]
exhibit_header = all_exhibit_headers [ exhibit_page ]
2025-05-28 16:57:23 +00:00
2025-05-13 20:22:43 +00:00
################## INITIALIZE FIELDS ##################
reimbursement_level_fields = FieldSet ( relationship = " one_to_n " , field_type = " reimbursement_level " , file_path = FIELD_JSON_PATH )
################## GET REIMBURSEMENT TIN/NPI ##################
2025-06-03 20:47:42 +00:00
tin_npi_answers , reimbursement_level_fields = reimbursement_tin_npi ( exhibit_text , reimbursement_level_fields , filename )
2025-05-13 20:22:43 +00:00
################## GET EXHIBIT-LEVEL ANSWERS ##################
2025-06-03 20:47:42 +00:00
exhibit_level_answers = one_to_n_funcs . get_exhibit_level_answers ( exhibit_text , filename )
exhibit_level_answers [ ' EXHIBIT_PAGE ' ] = exhibit_page
2025-05-13 20:22:43 +00:00
exhibit_level_answers [ ' EXHIBIT_TITLE ' ] = exhibit_header
################## GET DYNAMIC-PRIMARY ANSWERS ##################
2025-06-03 20:47:42 +00:00
dynamic_to_exhibit_level_answers , dynamic_to_reimbursement_level_fields = dynamic_funcs . get_dynamic_answers ( exhibit_text ,
2025-05-13 20:22:43 +00:00
exhibit_header ,
filename ,
FieldSet ( relationship = " one_to_n " , field_type = " dynamic " , file_path = config . FIELD_JSON_PATH ) )
exhibit_level_answers . update ( dynamic_to_exhibit_level_answers )
reimbursement_level_fields . combine ( dynamic_to_reimbursement_level_fields , inplace = True )
################## GET DYNAMIC-CODE ANSWERS ##################
2025-06-03 20:47:42 +00:00
code_to_exhibit_level_answers , code_to_reimbursement_level_fields = dynamic_funcs . get_dynamic_answers ( exhibit_text ,
2025-05-13 20:22:43 +00:00
exhibit_header ,
filename ,
FieldSet ( relationship = " one_to_n " , field_type = " dynamic_code " , file_path = config . FIELD_JSON_PATH ) )
exhibit_level_answers . update ( code_to_exhibit_level_answers )
reimbursement_level_fields . combine ( code_to_reimbursement_level_fields , inplace = True )
################## GET REIMBURSEMENT-LEVEL ANSWERS (INCLUDING DYNAMIC) ##################
2025-06-03 20:47:42 +00:00
reimbursement_level_answers = reimbursement_level ( exhibit_text , filename , reimbursement_level_fields , all_dataset , exhibit_page ) # Return list of dictionaries
2025-05-13 20:22:43 +00:00
################# COMBINE ANSWERS ##################
full_answer_dict = combine_one_to_n_answers ( exhibit_level_answers , reimbursement_level_answers , tin_npi_answers )
one_to_n_results + = full_answer_dict
2025-05-07 23:03:16 +00:00
2025-04-08 18:29:39 +00:00
################## Crosswalk Fields ##################
2025-03-31 20:07:19 +00:00
one_to_n_results = aarete_derived . get_crosswalk_fields ( one_to_n_results )
################## Fill NA Mapping ##################
one_to_n_results = aarete_derived . fill_na_mapping ( one_to_n_results )
2025-04-08 18:29:39 +00:00
2025-05-07 23:03:16 +00:00
################## Update LOB for Duals ##################
one_to_n_results = investment_postprocessing_funcs . update_lob_for_duals ( one_to_n_results )
2025-04-08 18:29:39 +00:00
################## Add N/A Dynamic or Exhibit to One-to-One ##################
dynamic_one_to_one_fields = dynamic_funcs . get_dynamic_one_to_one_fields ( one_to_n_results )
2025-01-31 22:36:11 +00:00
################## CONVERT TO DF ##################
2025-01-27 19:39:23 +00:00
one_to_n_df = pd . DataFrame ( one_to_n_results )
2025-04-08 18:29:39 +00:00
return one_to_n_df , dynamic_one_to_one_fields
2025-02-21 14:44:36 +00:00