From 4191a5616efdae7ab8bafbb7bfbddd61dace47db Mon Sep 17 00:00:00 2001 From: Alex Galarce Date: Fri, 2 May 2025 20:13:02 +0000 Subject: [PATCH] Merged in feature/rework-reimb-tin-npi (pull request #502) Feature/rework reimb tin npi * use LLM for reimb_[tin/npi/name] * Merge remote-tracking branch 'origin/main' into feature/rework-reimb-tin-npi * Merge remote-tracking branch 'origin/main' into feature/rework-reimb-tin-npi * deduplicate tin/name/npi before adding to prompt * Pull out prompt and llm_response in methodology_breakout_single_row for easier debugging * Refactor reimbursement_tin_npi to handle multiple providers and update valid TIN, NPI, and NAME fields accordingly * Reorder REIMB_PROV_NAME field in investment_prompts.json * Update REIMB_PROV_NAME prompt to specify valid values for provider names * Remove valid_values constraint from prov_name to raise accuracy * Remove debugging print statements from reimbursement_tin_npi and related functions * Remove debugging print statements from run_provider_info_fields function * Merge remote-tracking branch 'origin/main' into feature/rework-reimb-tin-npi * Remove debugging print statements from get_reimbursement_primary function * Merge remote-tracking branch 'origin/main' into feature/rework-reimb-tin-npi * Refactor reimbursement_tin_npi to always add providers (when we have any) to reimbursement_level_fields Approved-by: Katon Minhas --- .../src/investment/file_processing.py | 2 +- .../src/investment/one_to_n_funcs.py | 8 ++- .../src/investment/one_to_one_funcs.py | 5 -- .../src/investment/tin_npi_funcs.py | 56 +++++++++++-------- .../src/prompts/investment_prompts.json | 16 +++--- 5 files changed, 48 insertions(+), 39 deletions(-) diff --git a/fieldExtraction/src/investment/file_processing.py b/fieldExtraction/src/investment/file_processing.py index 6ec5c75..2359966 100644 --- a/fieldExtraction/src/investment/file_processing.py +++ b/fieldExtraction/src/investment/file_processing.py @@ -139,7 +139,7 @@ def run_one_to_n_prompts(filename, text_dict, exhibit_pages, exhibit_chunk_mappi exhibit_header = one_to_n_funcs.get_exhibit_header(exhibit_chunk, filename) ################## GET REIMBURSEMENT TIN/NPI ################## - tin_npi_answers, reimbursement_level_fields = reimbursement_tin_npi(exhibit_chunk, reimbursement_level_fields) + tin_npi_answers, reimbursement_level_fields = reimbursement_tin_npi(exhibit_chunk, reimbursement_level_fields, filename) ################## GET EXHIBIT-LEVEL ANSWERS ################## exhibit_level_answers = one_to_n_funcs.get_exhibit_level_answers(exhibit_chunk, filename) diff --git a/fieldExtraction/src/investment/one_to_n_funcs.py b/fieldExtraction/src/investment/one_to_n_funcs.py index 5d15f75..372648f 100644 --- a/fieldExtraction/src/investment/one_to_n_funcs.py +++ b/fieldExtraction/src/investment/one_to_n_funcs.py @@ -254,10 +254,11 @@ def methodology_breakout_single_row( contract_reimbursement_method = answer_dict.get("REIMB_TERM") # Methodology Breakout: AARETE_DERIVED_REIMB_METHOD, REIMB_FEE_RATE, REIMB_PCT_RATE, LESSER_OF_IND, GREATER_OF_IND, AARETE_DERIVED_PROV_TYPE if contract_reimbursement_method: - llm_response = llm_utils.invoke_claude( - methodology_prompt_template( + prompt = methodology_prompt_template( service, contract_reimbursement_method, methodology_breakout_questions - ), + ) + llm_response = llm_utils.invoke_claude( + prompt, config.MODEL_ID_CLAUDE35_SONNET, filename, ) @@ -461,6 +462,7 @@ def reimbursement_level( Returns: dict: The parsed LLM response as a list of dictionaries with unique identifiers """ + print("Inside reimbursement_level function, reimbusement_level_fields:", reimbursement_level_fields.get_prompt_dict()) reimbursement_primary_answers = get_reimbursement_primary( reimbursement_level_fields, exhibit_text, filename ) # Returns list of dicts diff --git a/fieldExtraction/src/investment/one_to_one_funcs.py b/fieldExtraction/src/investment/one_to_one_funcs.py index d4fd49a..9c8a16f 100644 --- a/fieldExtraction/src/investment/one_to_one_funcs.py +++ b/fieldExtraction/src/investment/one_to_one_funcs.py @@ -52,15 +52,11 @@ def run_provider_info_fields(contract_text: str, page_provider_info = get_provider_info(page_text, filename) combined_provider_info.extend(page_provider_info) - # print(f"Combined provider info for {filename}: {combined_provider_info}") # Debugging line - # Clean and standardize results cleaned_provider_info = clean_provider_info(combined_provider_info) - # print(f"Cleaned provider info for {filename}: {cleaned_provider_info}") # Debugging line # Deduplicate providers based on TIN and NPI deduplicated_provider_info = deduplicate_providers(cleaned_provider_info) - # print(f"Deduplicated provider info for {filename} [{len(deduplicated_provider_info)} entries]: {deduplicated_provider_info}") # Debugging line # if payer_name: # # Filter out providers that are too similar to the payer name @@ -83,7 +79,6 @@ def run_provider_info_fields(contract_text: str, elif len(deduplicated_provider_info) == 1: # If only one provider is found, mark it as the group provider deduplicated_provider_info[0]["IS_GROUP"] = True - # print(f"After group assignment for {filename}: {deduplicated_provider_info}") # Debugging line return deduplicated_provider_info diff --git a/fieldExtraction/src/investment/tin_npi_funcs.py b/fieldExtraction/src/investment/tin_npi_funcs.py index 6aa0f3e..09e148a 100644 --- a/fieldExtraction/src/investment/tin_npi_funcs.py +++ b/fieldExtraction/src/investment/tin_npi_funcs.py @@ -161,39 +161,55 @@ def merge_provider_info(one_to_one_results: dict, provider_info: list[dict]) -> one_to_one_results["PROV_OTHER_NAME_FULL"] = "|".join(other_names) if other_names else "" return one_to_one_results -def reimbursement_tin_npi(exhibit_chunk: str, reimbursement_level_fields: FieldSet) -> tuple[dict, FieldSet]: +def reimbursement_tin_npi(exhibit_chunk: str, reimbursement_level_fields: FieldSet, filename: str) -> tuple[dict, FieldSet]: """ - Extracts Reimbursement-Level TIN and NPI values from the given exhibit chunk using regex patterns. For multiple TIN/NPIs in Exhibit, assigns the field to be run as part of Reimbursement-Level Primary + Extracts Reimbursement-Level TIN and NPI values from the given exhibit chunk using an LLM. + When multiple providers are found, they're added as valid values for LLM selection + of these fields during reimbursement primary processing. Args: exhibit_chunk (str): The text chunk containing reimbursement details. reimbursement_level_fields (FieldSet): The FieldSet object to store extracted field values. + filename (str): The name of the file being processed. Returns: tuple[dict, FieldSet]: A dictionary containing extracted TIN and NPI values, and an updated FieldSet with additional fields if multiple values are found. - Fields Added: REIMB_PROV_TIN, + Fields Added: REIMB_PROV_NAME, + REIMB_PROV_TIN, REIMB_PROV_NPI """ answer_dict = {} + + # Extract all providers from the exhibit chunk + providers = get_provider_info(exhibit_chunk, filename) + + # Clean, standardize, and deduplicate extracted providers + filtered_providers = deduplicate_providers(clean_provider_info(providers)) - fields = [("REIMB_PROV_TIN", r"\b\d{2}-\d{7}\b|\b\d{9}\b"), - ("REIMB_PROV_NPI", r"\b\d{10}\b")] - - for field_tuple in fields: - field_name, field_pattern = field_tuple[0], field_tuple[1] - field_matches, page_list = regex_utils.find_regex_matches(field_pattern, {'1' : exhibit_chunk}) - - if not field_matches: - answer_dict[field_name] = "N/A" - elif len(field_matches) == 1: - answer_dict['REIMB_PROV_TIN'] = field_matches[0] - else: # Multiple tins - reimbursement_field = Field.load_from_file(config.FIELD_JSON_PATH, field_name) - reimbursement_field.update_valid_values(field_matches) - reimbursement_level_fields.add_field(reimbursement_field) + # Always add providers to reimbursement_level fields for proper term attribution + if len(filtered_providers) > 0: + tin_field = Field.load_from_file(config.FIELD_JSON_PATH, "REIMB_PROV_TIN") + tin_list = list(set([provider.get("TIN", "N/A") for provider in filtered_providers])) # Remove duplicates + tin_field.update_valid_values(tin_list) + reimbursement_level_fields.add_field(tin_field) + npi_field = Field.load_from_file(config.FIELD_JSON_PATH, "REIMB_PROV_NPI") + npi_list = list(set([provider.get("NPI", "N/A") for provider in filtered_providers])) # Remove duplicates + npi_field.update_valid_values(npi_list) + reimbursement_level_fields.add_field(npi_field) + + name_field = Field.load_from_file(config.FIELD_JSON_PATH, "REIMB_PROV_NAME") + name_list = list(set([provider.get("NAME", "N/A") for provider in filtered_providers])) # Remove duplicates + name_field.update_valid_values(name_list) + reimbursement_level_fields.add_field(name_field) + # If no providers are found, set default values + else: + answer_dict['REIMB_PROV_TIN'] = "N/A" + answer_dict['REIMB_PROV_NPI'] = "N/A" + answer_dict['REIMB_PROV_NAME'] = "N/A" + return answer_dict, reimbursement_level_fields @@ -233,11 +249,8 @@ def identify_group_provider(text_dict: dict, providers: list[dict], filename: st # Construct prompt prompt = investment_prompts.GROUP_TIN_NPI_TEMPLATE(key_sections=key_sections, provider_list=provider_list) - # print(f"Prompt for group provider identification: {prompt}") # Debugging line - # Get response and parse response = llm_utils.invoke_claude(prompt, config.MODEL_ID_CLAUDE35_SONNET, filename) - # print(f"Response from LLM for group provider identification: {response}") # Debugging line try: group_info = json.loads(string_utils.extract_text_from_delimiters(response, Delimiter.PIPE)) @@ -380,7 +393,6 @@ def get_provider_info(chunk: str, filename: str) -> list: prompt = investment_prompts.TIN_NPI_TEMPLATE(chunk, provider_fields.get_prompt_dict()) # Get response from LLM claude_answer_raw = llm_utils.invoke_claude(prompt, config.MODEL_ID_CLAUDE35_SONNET, filename, max_tokens=8192) # Sometimes rosters can have 50+ providers - print(f"Raw response from LLM: {claude_answer_raw}") # Defensive programming for any json parsing errors try: diff --git a/fieldExtraction/src/prompts/investment_prompts.json b/fieldExtraction/src/prompts/investment_prompts.json index 31ad780..495449a 100644 --- a/fieldExtraction/src/prompts/investment_prompts.json +++ b/fieldExtraction/src/prompts/investment_prompts.json @@ -62,12 +62,6 @@ "prompt": "Identify the termination date for reimbursement rates:\n- Look for dates associated with:\n • 'Termination' or 'termination date'\n • 'Expires' or 'expiration date'\n • 'Through' or 'until'\n- Dates may appear as:\n • Full dates (12/1/17, December 1, 2017)\n • Month and year (December 2017)\n • Year only (2017)", "format": "date" }, - { - "field_name": "REIMB_PROV_NAME", - "relationship": "one_to_n", - "field_type": "dynamic", - "prompt": "Identify any Provider Names present that apply to the text. Do NOT return the name of a payer or insurance plan, only the names of specific providers (doctors, hospitals, clinics, etc) listed. Do not return the word 'Provider'." - }, { "field_name": "NOT_TO_EXCEED_IND", "relationship": "one_to_n", @@ -236,13 +230,19 @@ "field_name": "REIMB_PROV_TIN", "relationship": "one_to_n", "field_type": "TBD", - "prompt": "What TIN (Tax ID Number) does the reimbursement term apply to? Choose only from the following: {valid_values}." + "prompt": "What TIN (Tax ID Number) does the reimbursement term apply to? When the term applies to multiple providers simultaneously, list ALL applicable TINs separated by commas. Look for language patterns indicating multiple providers (phrases like 'following providers', 'these facilities', 'listed providers', etc.). Choose only from the following: {valid_values}." }, { "field_name": "REIMB_PROV_NPI", "relationship": "one_to_n", "field_type": "TBD", - "prompt": "What NPI (National Provider Identifier) does the reimbursement term apply to? Choose only from the following: {valid_values}." + "prompt": "What NPI (National Provider Identifier) does the reimbursement term apply to? When the term applies to multiple providers simultaneously, list ALL applicable NPIs separated by commas. Look for language patterns indicating multiple providers (phrases like 'following providers', 'these facilities', 'listed providers', etc.). Choose only from the following: {valid_values}." + }, + { + "field_name": "REIMB_PROV_NAME", + "relationship": "one_to_n", + "field_type": "dynamic", + "prompt": "Identify any Provider Names that this reimbursement term applies to. When the term applies to multiple providers simultaneously, list ALL applicable names separated by commas. Look for language patterns indicating multiple providers. Extract only the official names of healthcare providers (doctors, hospitals, clinics, etc.) - not payers or insurance plans. If unable to determine applicable provider names, respond with 'UNKNOWN'." }, { "field_name": "AARETE_DERIVED_EFFECTIVE_DT",