From f6cefc62e3e97773f053f778f7d433615e4b1f47 Mon Sep 17 00:00:00 2001 From: Katon Minhas Date: Tue, 19 Aug 2025 16:55:09 +0000 Subject: [PATCH] Merged in bugfix/dynamic-primary-issues (pull request #669) Bugfix/dynamic primary issues * add LOB_RELATIONSHIP prompt * Add test script * Integrate prompt * Update one-to-one passing rules * Merged main into bugfix/dynamic-primary-issues * Remove promt * replace quotes Approved-by: Alex Galarce --- .../src/investment/aarete_derived.py | 2 + .../src/investment/dynamic_funcs.py | 29 ++++++------ .../src/investment/file_processing.py | 3 ++ .../src/investment/one_to_n_funcs.py | 25 +++++++++++ .../src/prompts/prompt_templates.py | 44 +++++++++++++++++++ 5 files changed, 90 insertions(+), 13 deletions(-) diff --git a/fieldExtraction/src/investment/aarete_derived.py b/fieldExtraction/src/investment/aarete_derived.py index 24a63bd..9161c65 100644 --- a/fieldExtraction/src/investment/aarete_derived.py +++ b/fieldExtraction/src/investment/aarete_derived.py @@ -40,6 +40,7 @@ def fill_na_mapping(answer_dicts): "AARETE_DERIVED_PROGRAM", "constants/mappings/crosswalk_program_lob.json", ) + answer_dict["LOB_RELATIONSHIP"] = "Exclusive" # Fill AARETE_DERIVED_LOB from PRODUCT if string_utils.is_empty(answer_dict.get("AARETE_DERIVED_LOB")): @@ -49,6 +50,7 @@ def fill_na_mapping(answer_dicts): "PRODUCT", "constants/mappings/crosswalk_product_lob.json", ) + answer_dict["LOB_RELATIONSHIP"] = "Exclusive" return answer_dicts diff --git a/fieldExtraction/src/investment/dynamic_funcs.py b/fieldExtraction/src/investment/dynamic_funcs.py index 3171ff5..66dbaef 100644 --- a/fieldExtraction/src/investment/dynamic_funcs.py +++ b/fieldExtraction/src/investment/dynamic_funcs.py @@ -26,20 +26,23 @@ def prompt_dynamic(text, field_prompts, filename): return llm_answer_final -def add_full_context_field(one_to_one_fields, field_to_add): +def add_full_context_field(one_to_one_fields, field_to_add, answer_dicts): field_to_add.field_type = "full_context" field_to_add.relationship = "one_to_one" - if field_to_add.allow_na: - field_to_add.prompt = ( - field_to_add.prompt - + " Only answer if the answer applies to the ENTIRE contract. Otherwise, answer N/A." - ) - else: - field_to_add.prompt = ( - field_to_add.prompt - + " Do NOT leave this field N/A. Choose the most appropriate answer based on the context." - ) + # Special Handling for Program and Product: + # If there are multiple non-empty AARETE_DERIVED_LOB values in the contract, don't ask for Program or Product full context + if field_to_add.field_name in ["PROGRAM", "PRODUCT"]: + lob_values = [answer_dict.get("AARETE_DERIVED_LOB", "") for answer_dict in answer_dicts] + non_empty_lobs = [lob for lob in lob_values if not string_utils.is_empty(lob)] + if len(non_empty_lobs) > 1: + return one_to_one_fields + + field_to_add.prompt = ( + field_to_add.prompt + + prompt_templates.FULL_CONTEXT_ADDITIONAL_INSTRUCTIONS(field_to_add.allow_na) + ) + one_to_one_fields.add_field(field_to_add) return one_to_one_fields @@ -94,7 +97,7 @@ def get_dynamic_one_to_one_fields(answer_dicts): field_name=field.base_field, ) one_to_one_fields = add_full_context_field( - one_to_one_fields, field_to_add + one_to_one_fields, field_to_add, answer_dicts ) continue @@ -113,7 +116,7 @@ def get_dynamic_one_to_one_fields(answer_dicts): file_path=config.FIELD_JSON_PATH, field_name=field.base_field, ) - one_to_one_fields = add_full_context_field(one_to_one_fields, field_to_add) + one_to_one_fields = add_full_context_field(one_to_one_fields, field_to_add, answer_dicts) return one_to_one_fields diff --git a/fieldExtraction/src/investment/file_processing.py b/fieldExtraction/src/investment/file_processing.py index 833b95f..327a380 100644 --- a/fieldExtraction/src/investment/file_processing.py +++ b/fieldExtraction/src/investment/file_processing.py @@ -177,6 +177,9 @@ def run_one_to_n_prompts(filename, exhibit_dict, all_exhibit_headers, constants) ################## Crosswalk Fields ################## one_to_n_results = aarete_derived.get_crosswalk_fields(one_to_n_results) + ################## Determine LOB Relationship ################## + one_to_n_results = one_to_n_funcs.get_lob_relationship(one_to_n_results, exhibit_dict, filename) + ################## Fill NA Mapping ################## one_to_n_results = aarete_derived.fill_na_mapping(one_to_n_results) diff --git a/fieldExtraction/src/investment/one_to_n_funcs.py b/fieldExtraction/src/investment/one_to_n_funcs.py index 2e2e153..b6efcf0 100644 --- a/fieldExtraction/src/investment/one_to_n_funcs.py +++ b/fieldExtraction/src/investment/one_to_n_funcs.py @@ -1367,3 +1367,28 @@ def outlier_terms_breakout(outlier_term, filename: str) -> dict: llm_response = llm_utils.invoke_claude(prompt, "sonnet_latest", filename) outlier_answers = string_utils.universal_json_load(llm_response) or {} return outlier_answers + + +def get_lob_relationship(answer_dicts, exhibit_dict, filename): + + for answer_dict in answer_dicts: + # Process relationship if LOB is present and either Program or Product is present + if not string_utils.is_empty(answer_dict.get("AARETE_DERIVED_LOB")) and (not string_utils.is_empty(answer_dict.get("AARETE_DERIVED_PROGRAM")) or not string_utils.is_empty(answer_dict.get("AARETE_DERIVED_PRODUCT"))): + # Get values + exhibit_text = exhibit_dict.get(answer_dict.get("EXHIBIT_PAGE")) + programs_and_products = answer_dict.get("AARETE_DERIVED_PRODUCT", "") + "|" + answer_dict.get("AARETE_DERIVED_PROGRAM", "") + dynamic_primary_values = f"LOB: {answer_dict.get("AARETE_DERIVED_LOB")}\nPROGRAM: {programs_and_products}" + + # Prompt LLM + prompt = prompt_templates.LOB_RELATIONSHIP(exhibit_text, dynamic_primary_values) + llm_answer_raw = llm_utils.invoke_claude(prompt, model_id="sonnet_latest", filename=filename) + llm_answer_final = string_utils.extract_text_from_delimiters(llm_answer_raw, Delimiter.PIPE) + + # Assign answer + answer_dict["LOB_RELATIONSHIP"] = llm_answer_final + + return answer_dicts + + + + diff --git a/fieldExtraction/src/prompts/prompt_templates.py b/fieldExtraction/src/prompts/prompt_templates.py index 83438b3..68df3f6 100644 --- a/fieldExtraction/src/prompts/prompt_templates.py +++ b/fieldExtraction/src/prompts/prompt_templates.py @@ -1091,3 +1091,47 @@ Return your answers in valid JSON format using the exact field names from the qu **Important:** Base your answers ONLY on the outlier terms provided. Do not make assumptions or add information not present in the contract language. """ + + +def LOB_RELATIONSHIP(exhibit_text, dynamic_primary_values): + return f"""You will be presented with an exhibit from a Payer-Provider contract. + +[BACKGROUND] +- In healthcare, there are generally multiple Programs for any given LOB. + - For example, under the Medicaid LOB, there can be the Programs CHIP, STAR, Medi-Cal, etc. +- The Exhibit shown contains a number of reimbursement terms, which apply to different LOB and Programs. + +It was previously identified that the Exhibit contains the following LOB and Programs: +{dynamic_primary_values} + +[INSTRUCTIONS] +- Determine if the Exhibit presents an "Inclusive" relationship or "Exclusive" relationship between LOB and Program. +- Inclusive Relationship: When the contract refers to an LOB, including (but not limited to) the programs in the list. + - Some examples of language indicating an inclusive relationship are: "Medicaid and CHIP", "Medicare, including Medicare Advantage", and "Duals, including all special needs programs" +- Exclusive Relationship: When the contract refers to an LOB and Programs, but the information applies to JUST those specific programs. + - Some examples of language indicating an exlusive relationship are "Medicaid: CHIP", "Medicare advantage only", and "Duals - only the following SNP programs" + - If the programs are represented with different sections of the Exhibit under the same higher-level LOB section, this is indicative of an Exclusive relationship. +- Closely read the text, then write either |Inclusive| or |Exclusive| to indicate the nature of the relationship. + +[CONTEXT] +Here is the Exhibit to be analyzed: +{exhibit_text.replace('"', "'")} + +[OUTPUT] +Briefly explain your reasoning, then put your final answer in |pipes|. +""" + + +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." + else: + return " Do NOT leave this field N/A. Choose the most appropriate answer based on the context." + + + + + + + +