From 90bed0d5f64c2f8b95c16970d3d12efa5e266af0 Mon Sep 17 00:00:00 2001 From: Faizan Mohiuddin Date: Fri, 30 Jan 2026 23:23:37 +0000 Subject: [PATCH] Merged in feature/update-caching (pull request #856) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Feature/update caching * Enable prompt caching for CODE_LAST_CHECK, FILL_BILL_TYPE, DUAL_LOB_CHECK, and GROUPER_BREAKOUT - Add CODE_LAST_CHECK_INSTRUCTION() for service specificity classification - Add FILL_BILL_TYPE_INSTRUCTION() for bill type code determination - Add DUAL_LOB_CHECK_INSTRUCTION() for Medicare/Medicaid classification - Update code_funcs.py to use caching for CODE_LAST_CHECK, FILL_BILL_TYPE, GROUPER_BREAKOUT - Update postprocessing_funcs.py to use caching for DUAL_LOB_CHECK - Add new instructions to get_cacheable_instructions() for cache warming - Add unit tests for new instruction functions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * Fix postprocessing_funcs to remove invalid columns * Merge branch 'main' into feature/lesser-table-caching-refactor-hybrid * Apply black formatting to 11 files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * Fix import paths in testbed files * Merge main into feature/update-caching * Add SPLIT_REIMB_DATES_INSTRUCTION for prompt caching * Merged main into feature/update-caching * Merge remote-tracking branch 'origin/main' into feature/update-caching * Merge branch 'feature/update-caching' of https://bitbucket.org/aarete/doczy.ai into feature/update-caching * Add CODE_* instruction functions for prompt caching * Merged main into feature/update-caching * Merged main into feature/update-caching * Maximize prompt caching by moving static content to instructions - Refactored 12+ prompt functions to move static rules/examples to _INSTRUCTION() - VALIDATE_REIMBURSEMENTS: Moved 45+ lines of validation rules to instruction - DATE_FIX: Moved conversion rules and examples to instruction - EXHIBIT_LEVEL, EXHIBIT_TITLE_MATCH, EXHIBIT_HEADER, EXHIBIT_LINKAGE: Moved rules - FEE_SCHEDULE_BREAKOUT, GROUPER_BREAKOUT: Moved extraction rules - CHECK_PROVIDER_NAME_MATCH: Moved matching rules to instruction - LOB_RELATIONSHIP: Consolidated split instructions - DERIVED_TERM_DATE: Moved calculation logic to instruction - DYNAMIC_PRIMARY: Created separate HEADER and TEXT instructions - Created new REIMB_DATES_ASSIGNMENT_INSTRUCTION with all date rules - Updated get_cacheable_instructions() with new instruction functions - Updated all callers in saas, clover, bcbs_promise pipelines This enables ~90% cost savings via Claude API prompt caching by ensuring static instruction text is cached and only dynamic content varies. * Remove Dynamic Primary Header * Standardize prompt caching across all LLM prompts for ~90% cost savings Move static content to _INSTRUCTION() functions for caching: - METHODOLOGY_BREAKOUT: 8 field definitions with resolved valid_values - FEE_SCHEDULE_BREAKOUT: 3 field definitions - GROUPER_BREAKOUT: 11 field definitions - OUTLIER_BREAKOUT: 8 field definitions - CODE_EXPLICIT: 8 field definitions - EXHIBIT_HEADER: static header markers list - CARVEOUT_CHECK: carveout and special case definitions - SPECIAL_CASE_ASSIGNMENT: task rules and output format Update callers to use cache=True, instruction, and usage_label params. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * Apply black formatting to prompt_templates.py * Update unit tests for prompt caching changes - Fix function signatures in tests (METHODOLOGY_BREAKOUT, FEE_SCHEDULE_BREAKOUT, GROUPER_BREAKOUT, CODE_EXPLICIT) - Add tests for CARVEOUT_CHECK and EXHIBIT_HEADER prompts - Add TestInstructionFieldContent class to verify instruction functions contain field definitions - Add tests for CARVEOUT_CHECK_INSTRUCTION and CODE_EXPLICIT_INSTRUCTION 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * Refactor instructions to load fields from investment_prompts.json and Constants Address PR review feedback: - Add _get_constants() helper for lazy Constants instantiation - Add _get_fields_text() helper to load fields from investment_prompts.json - METHODOLOGY_BREAKOUT_INSTRUCTION: Load fields via FieldSet - FEE_SCHEDULE_BREAKOUT_INSTRUCTION: Load fields via FieldSet - GROUPER_BREAKOUT_INSTRUCTION: Load fields via FieldSet - OUTLIER_BREAKOUT_INSTRUCTION: Load fields via FieldSet - EXHIBIT_HEADER_INSTRUCTION: Load markers from Constants.EXHIBIT_HEADER_MARKERS - CARVEOUT_CHECK_INSTRUCTION: Load carveouts from Constants.VALID_CARVEOUTS, special cases from investment_prompts.json - CODE_EXPLICIT_INSTRUCTION: Keep hardcoded (no code_explicit field_type in JSON) Update tests to match actual field names from investment_prompts.json. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * Merge main into feature/update-caching * Remove deprecated dynamic_primary_header * Update CODE_EXPLICIT_INSTRUCTION to load fields from investment_prompts.json Use field_type=code_primary_breakout to load code fields dynamically. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * Merged main into feature/update-caching * Merge main into feature/update-caching * Merge branch 'feature/update-caching' of https://bitbucket.org/aarete/doczy.ai into feature/update-caching Approved-by: Katon Minhas --- src/codes/code_funcs.py | 36 +- .../bcbs_promise/prompts/prompt_calls.py | 74 +- .../clients/clover/prompts/prompt_calls.py | 48 +- src/pipelines/saas/prompts/prompt_calls.py | 48 +- .../shared/extraction/one_to_n_funcs.py | 21 +- .../postprocessing/postprocessing_funcs.py | 7 +- .../preprocessing/preprocessing_funcs.py | 5 +- src/prompts/prompt_templates.py | 1044 +++++++++-------- src/scripts/qa_qc.py | 1 - src/tests/test_prompt_caching.py | 161 ++- 10 files changed, 887 insertions(+), 558 deletions(-) diff --git a/src/codes/code_funcs.py b/src/codes/code_funcs.py index b2929fa..57f99d4 100644 --- a/src/codes/code_funcs.py +++ b/src/codes/code_funcs.py @@ -105,14 +105,14 @@ def code_explicit(service, methodology, filename): Returns an empty dictionary if the service string is empty. """ - # Prompt - code_primary_questions = FieldSet( - file_path=config.FIELD_JSON_PATH, field_type="code_primary_breakout" - ).print_prompt_dict() + # Prompt - field definitions are now included in CODE_EXPLICIT_INSTRUCTION() for caching claude_answer_raw = llm_utils.invoke_claude( - prompt_templates.CODE_EXPLICIT(service, methodology, code_primary_questions), + prompt_templates.CODE_EXPLICIT(service, methodology), "sonnet_latest", filename, + cache=True, + instruction=prompt_templates.CODE_EXPLICIT_INSTRUCTION(), + usage_label="CODE_EXPLICIT", ) code_answer_dict = string_utils.universal_json_load(claude_answer_raw) return code_answer_dict @@ -143,6 +143,8 @@ def code_category(service, proc_category, hcpcs_level2_mapping, filename): prompt_templates.CODE_CATEGORY(service, matching_values), "sonnet_latest", filename, + cache=True, + instruction=prompt_templates.CODE_CATEGORY_INSTRUCTION(), ) claude_answer_final = string_utils.universal_json_load( claude_answer_raw @@ -176,7 +178,11 @@ def code_implicit_special(service, filename): """ claude_answer_raw = llm_utils.invoke_claude( - prompt_templates.CODE_IMPLICIT_SPECIAL(service), "sonnet_latest", filename + prompt_templates.CODE_IMPLICIT_SPECIAL(service), + "sonnet_latest", + filename, + cache=True, + instruction=prompt_templates.CODE_IMPLICIT_SPECIAL_INSTRUCTION(), ) claude_answer_final = string_utils.extract_text_from_delimiters( claude_answer_raw, Delimiter.PIPE @@ -350,6 +356,8 @@ def code_implicit_rag(service, implicit_run_dict, filename, constants): prompt_templates.CODE_IMPLICIT(service, level_dict["match_list"]), "sonnet_latest", filename, + cache=True, + instruction=prompt_templates.CODE_IMPLICIT_INSTRUCTION(), ) try: @@ -430,7 +438,11 @@ def code_last_check(service, filename): """ claude_answer_raw = llm_utils.invoke_claude( - prompt_templates.CODE_LAST_CHECK(service), "sonnet_latest", filename + prompt_templates.CODE_LAST_CHECK(service), + "sonnet_latest", + filename, + cache=True, + instruction=prompt_templates.CODE_LAST_CHECK_INSTRUCTION(), ) claude_answer_final = string_utils.extract_text_from_delimiters( claude_answer_raw, Delimiter.PIPE @@ -470,6 +482,8 @@ def fill_bill_type( ), "sonnet_latest", "", + cache=True, + instruction=prompt_templates.FILL_BILL_TYPE_INSTRUCTION(), ) claude_answer_final = string_utils.universal_json_load(claude_answer_raw) @@ -484,6 +498,8 @@ def fill_bill_type( ), "sonnet_latest", "", + cache=True, + instruction=prompt_templates.FILL_BILL_TYPE_INSTRUCTION(), ) claude_answer_final = string_utils.universal_json_load(claude_answer_raw) @@ -843,7 +859,11 @@ def grouper_breakout(results_with_code: pd.DataFrame): GROUPER_QUESTIONS, ) claude_answer_raw = llm_utils.invoke_claude( - grouper_breakout_prompt, "sonnet_latest", "" + grouper_breakout_prompt, + "sonnet_latest", + "", + cache=True, + instruction=prompt_templates.GROUPER_BREAKOUT_INSTRUCTION(), ) try: grouper_answer = string_utils.universal_json_load(claude_answer_raw) diff --git a/src/pipelines/clients/bcbs_promise/prompts/prompt_calls.py b/src/pipelines/clients/bcbs_promise/prompts/prompt_calls.py index 2a769a4..7aca165 100644 --- a/src/pipelines/clients/bcbs_promise/prompts/prompt_calls.py +++ b/src/pipelines/clients/bcbs_promise/prompts/prompt_calls.py @@ -132,14 +132,15 @@ def prompt_reimbursement_primary( def prompt_methodology_breakout( service_term: str, reimb_term: str, - methodology_breakout_questions: FieldSet, - constants: Constants, filename: str, ): + """ + Call METHODOLOGY_BREAKOUT prompt with cached instruction. + Note: Field definitions are now included in METHODOLOGY_BREAKOUT_INSTRUCTION() for caching. + """ prompt = prompt_templates.METHODOLOGY_BREAKOUT( service_term, reimb_term, - methodology_breakout_questions.print_prompt_dict(constants), ) logging.debug(f"Methodology Breakout Prompt for {filename}: {prompt}") llm_response = llm_utils.invoke_claude( @@ -148,6 +149,7 @@ def prompt_methodology_breakout( filename, cache=True, instruction=prompt_templates.METHODOLOGY_BREAKOUT_INSTRUCTION(), + usage_label="METHODOLOGY_BREAKOUT", ) logging.debug(f"LLM Response for {filename}: {llm_response}") try: @@ -161,20 +163,24 @@ def prompt_methodology_breakout( def prompt_fee_schedule_breakout( methodology_breakout_dict: dict, reimbursement_method: str, - fs_breakout_questions: FieldSet, - constants: Constants, filename: str, ): + """ + Call FEE_SCHEDULE_BREAKOUT prompt with cached instruction. + Note: Field definitions are now included in FEE_SCHEDULE_BREAKOUT_INSTRUCTION() for caching. + """ prompt = prompt_templates.FEE_SCHEDULE_BREAKOUT( reimbursement_method, methodology_breakout_dict.get("FEE_SCHEDULE"), - fs_breakout_questions.print_prompt_dict(constants), ) logging.debug(f"Fee Schedule Breakout Prompt for {filename}: {prompt}") llm_answer_raw = llm_utils.invoke_claude( prompt, "sonnet_latest", filename, + cache=True, + instruction=prompt_templates.FEE_SCHEDULE_BREAKOUT_INSTRUCTION(), + usage_label="FEE_SCHEDULE_BREAKOUT", ) logging.debug(f"LLM Response for {filename}: {llm_answer_raw}") try: @@ -187,20 +193,24 @@ def prompt_fee_schedule_breakout( def prompt_grouper_breakout( service: str, reimbursement_method: str, - grouper_breakout_questions: FieldSet, - constants: Constants, filename: str, ): + """ + Call GROUPER_BREAKOUT prompt with cached instruction. + Note: Field definitions are now included in GROUPER_BREAKOUT_INSTRUCTION() for caching. + """ prompt = prompt_templates.GROUPER_BREAKOUT( service, reimbursement_method, - grouper_breakout_questions.print_prompt_dict(constants), ) logging.debug(f"Grouper Breakout Prompt for {filename}: {prompt}") llm_answer_raw = llm_utils.invoke_claude( prompt, "sonnet_latest", filename, + cache=True, + instruction=prompt_templates.GROUPER_BREAKOUT_INSTRUCTION(), + usage_label="GROUPER_BREAKOUT", ) logging.debug(f"LLM Response for {filename}: {llm_answer_raw}") try: @@ -214,13 +224,13 @@ def prompt_grouper_breakout( def prompt_carveout_check( service_term: str, reimb_term: str, - carveout_definitions, - special_case_definitions, filename: str, ): - carveout_prompt = prompt_templates.CARVEOUT_CHECK( - service_term, reimb_term, carveout_definitions, special_case_definitions - ) + """ + Call CARVEOUT_CHECK prompt with cached instruction. + Note: Carveout and special case definitions are now included in CARVEOUT_CHECK_INSTRUCTION() for caching. + """ + carveout_prompt = prompt_templates.CARVEOUT_CHECK(service_term, reimb_term) logging.debug( f"Running carveout check for {filename} with prompt:\n{carveout_prompt}" ) @@ -327,6 +337,9 @@ def prompt_special_case_assignment( ), "sonnet_latest", filename, + cache=True, + instruction=prompt_templates.SPECIAL_CASE_ASSIGNMENT_INSTRUCTION(), + usage_label="SPECIAL_CASE_ASSIGNMENT", ) index_answer = string_utils.extract_text_from_delimiters( llm_answer_raw, Delimiter.PIPE @@ -468,25 +481,29 @@ def prompt_exhibit_linkage(page_header, previous_header, filename): return exhibit_is_different -def prompt_exhibit_header(page_content, EXHIBIT_HEADER_MARKERS, filename): +def prompt_exhibit_header(page_content, filename): """Extract exhibit header identifier from page content using pattern matching. Uses LLM to identify exhibit headers in page content based on predefined markers and extract the exhibit identifier. + Note: Header markers are now included in EXHIBIT_HEADER_INSTRUCTION() for caching. Args: page_content (str): First 400 characters of page content to analyze. - EXHIBIT_HEADER_MARKERS (list): List of exhibit header pattern markers. filename (str): Source filename for logging and LLM attribution. Returns: str: Extracted exhibit header identifier or marker indicating no header found. """ - prompt = prompt_templates.EXHIBIT_HEADER( - page_content[0:400], EXHIBIT_HEADER_MARKERS - ) + prompt = prompt_templates.EXHIBIT_HEADER(page_content[0:400]) llm_answer_raw = llm_utils.invoke_claude( - prompt, "sonnet_latest", filename, max_tokens=300 + prompt, + "sonnet_latest", + filename, + max_tokens=300, + cache=True, + instruction=prompt_templates.EXHIBIT_HEADER_INSTRUCTION(), + usage_label="EXHIBIT_HEADER", ) llm_answer_extracted = string_utils.extract_text_from_delimiters( llm_answer_raw, Delimiter.PIPE @@ -506,7 +523,14 @@ def prompt_date_fix(date: str) -> str: if string_utils.is_empty(date): return "N/A" prompt = prompt_templates.DATE_FIX_PROMPT(date) - response = llm_utils.invoke_claude(prompt, "haiku_latest", "date_fix") + response = llm_utils.invoke_claude( + prompt, + "haiku_latest", + "date_fix", + cache=True, + instruction=prompt_templates.DATE_FIX_INSTRUCTION(), + usage_label="DATE_FIX", + ) return string_utils.extract_text_from_delimiters(response, Delimiter.PIPE) @@ -550,6 +574,8 @@ def prompt_dynamic_assignment( prompt = prompt_templates.REIMB_DATES_ASSIGNMENT( service_term, reimb_term, field_prompt, exhibit_text_simplified, page_num ) + instruction = prompt_templates.REIMB_DATES_ASSIGNMENT_INSTRUCTION() + usage_label = "REIMB_DATES_ASSIGNMENT" else: # Use generic DYNAMIC_ASSIGNMENT for other fields prompt = prompt_templates.DYNAMIC_ASSIGNMENT( @@ -560,14 +586,16 @@ def prompt_dynamic_assignment( exhibit_text_simplified, page_num, ) + instruction = prompt_templates.DYNAMIC_ASSIGNMENT_INSTRUCTION() + usage_label = "DYNAMIC_ASSIGNMENT" llm_answer_raw = llm_utils.invoke_claude( prompt, model_id="sonnet_latest", filename=filename, cache=True, - instruction=prompt_templates.DYNAMIC_ASSIGNMENT_INSTRUCTION(), - usage_label="DYNAMIC_ASSIGNMENT", + instruction=instruction, + usage_label=usage_label, ) try: diff --git a/src/pipelines/clients/clover/prompts/prompt_calls.py b/src/pipelines/clients/clover/prompts/prompt_calls.py index 9f57c2e..947b59f 100644 --- a/src/pipelines/clients/clover/prompts/prompt_calls.py +++ b/src/pipelines/clients/clover/prompts/prompt_calls.py @@ -138,14 +138,15 @@ def prompt_reimbursement_primary( def prompt_methodology_breakout( service_term: str, reimb_term: str, - methodology_breakout_questions: FieldSet, - constants: Constants, filename: str, ): + """ + Call METHODOLOGY_BREAKOUT prompt with cached instruction. + Note: Field definitions are now included in METHODOLOGY_BREAKOUT_INSTRUCTION() for caching. + """ prompt = prompt_templates.METHODOLOGY_BREAKOUT( service_term, reimb_term, - methodology_breakout_questions.print_prompt_dict(constants), ) logging.debug(f"Methodology Breakout Prompt for {filename}: {prompt}") llm_response = llm_utils.invoke_claude( @@ -154,6 +155,7 @@ def prompt_methodology_breakout( filename, cache=True, instruction=prompt_templates.METHODOLOGY_BREAKOUT_INSTRUCTION(), + usage_label="METHODOLOGY_BREAKOUT", ) logging.debug(f"LLM Response for {filename}: {llm_response}") try: @@ -167,14 +169,15 @@ def prompt_methodology_breakout( def prompt_fee_schedule_breakout( methodology_breakout_dict: dict, reimbursement_method: str, - fs_breakout_questions: FieldSet, - constants: Constants, filename: str, ): + """ + Call FEE_SCHEDULE_BREAKOUT prompt with cached instruction. + Note: Field definitions are now included in FEE_SCHEDULE_BREAKOUT_INSTRUCTION() for caching. + """ prompt = prompt_templates.FEE_SCHEDULE_BREAKOUT( reimbursement_method, methodology_breakout_dict.get("FEE_SCHEDULE"), - fs_breakout_questions.print_prompt_dict(constants), ) logging.debug(f"Fee Schedule Breakout Prompt for {filename}: {prompt}") llm_answer_raw = llm_utils.invoke_claude( @@ -196,14 +199,15 @@ def prompt_fee_schedule_breakout( def prompt_grouper_breakout( service: str, reimbursement_method: str, - grouper_breakout_questions: FieldSet, - constants: Constants, filename: str, ): + """ + Call GROUPER_BREAKOUT prompt with cached instruction. + Note: Field definitions are now included in GROUPER_BREAKOUT_INSTRUCTION() for caching. + """ prompt = prompt_templates.GROUPER_BREAKOUT( service, reimbursement_method, - grouper_breakout_questions.print_prompt_dict(constants), ) logging.debug(f"Grouper Breakout Prompt for {filename}: {prompt}") llm_answer_raw = llm_utils.invoke_claude( @@ -226,13 +230,13 @@ def prompt_grouper_breakout( def prompt_carveout_check( service_term: str, reimb_term: str, - carveout_definitions, - special_case_definitions, filename: str, ): - carveout_prompt = prompt_templates.CARVEOUT_CHECK( - service_term, reimb_term, carveout_definitions, special_case_definitions - ) + """ + Call CARVEOUT_CHECK prompt with cached instruction. + Note: Case definitions are now included in CARVEOUT_CHECK_INSTRUCTION() for caching. + """ + carveout_prompt = prompt_templates.CARVEOUT_CHECK(service_term, reimb_term) logging.debug( f"Running carveout check for {filename} with prompt:\n{carveout_prompt}" ) @@ -509,23 +513,21 @@ def prompt_exhibit_linkage(page_header, previous_header, filename): return exhibit_is_different -def prompt_exhibit_header(page_content, EXHIBIT_HEADER_MARKERS, filename): +def prompt_exhibit_header(page_content, filename): """Extract exhibit header identifier from page content using pattern matching. Uses LLM to identify exhibit headers in page content based on predefined markers and extract the exhibit identifier. + Note: Header markers are now included in EXHIBIT_HEADER_INSTRUCTION() for caching. Args: page_content (str): First 400 characters of page content to analyze. - EXHIBIT_HEADER_MARKERS (list): List of exhibit header pattern markers. filename (str): Source filename for logging and LLM attribution. Returns: str: Extracted exhibit header identifier or marker indicating no header found. """ - prompt = prompt_templates.EXHIBIT_HEADER( - page_content[0:400], EXHIBIT_HEADER_MARKERS - ) + prompt = prompt_templates.EXHIBIT_HEADER(page_content[0:400]) llm_answer_raw = llm_utils.invoke_claude( prompt, "sonnet_latest", @@ -611,6 +613,8 @@ def prompt_dynamic_assignment( prompt = prompt_templates.REIMB_DATES_ASSIGNMENT( service_term, reimb_term, field_prompt, exhibit_text_simplified, page_num ) + instruction = prompt_templates.REIMB_DATES_ASSIGNMENT_INSTRUCTION() + usage_label = "REIMB_DATES_ASSIGNMENT" else: # Use generic DYNAMIC_ASSIGNMENT for other fields prompt = prompt_templates.DYNAMIC_ASSIGNMENT( @@ -621,14 +625,16 @@ def prompt_dynamic_assignment( exhibit_text_simplified, page_num, ) + instruction = prompt_templates.DYNAMIC_ASSIGNMENT_INSTRUCTION() + usage_label = "DYNAMIC_ASSIGNMENT" llm_answer_raw = llm_utils.invoke_claude( prompt, model_id="sonnet_latest", filename=filename, cache=True, - instruction=prompt_templates.DYNAMIC_ASSIGNMENT_INSTRUCTION(), - usage_label="DYNAMIC_ASSIGNMENT", + instruction=instruction, + usage_label=usage_label, ) try: diff --git a/src/pipelines/saas/prompts/prompt_calls.py b/src/pipelines/saas/prompts/prompt_calls.py index 76c86be..788250a 100644 --- a/src/pipelines/saas/prompts/prompt_calls.py +++ b/src/pipelines/saas/prompts/prompt_calls.py @@ -137,14 +137,15 @@ def prompt_reimbursement_primary( def prompt_methodology_breakout( service_term: str, reimb_term: str, - methodology_breakout_questions: FieldSet, - constants: Constants, filename: str, ): + """ + Call METHODOLOGY_BREAKOUT prompt with cached instruction. + Note: Field definitions are now included in METHODOLOGY_BREAKOUT_INSTRUCTION() for caching. + """ prompt = prompt_templates.METHODOLOGY_BREAKOUT( service_term, reimb_term, - methodology_breakout_questions.print_prompt_dict(constants), ) logging.debug(f"Methodology Breakout Prompt for {filename}: {prompt}") llm_response = llm_utils.invoke_claude( @@ -153,6 +154,7 @@ def prompt_methodology_breakout( filename, cache=True, instruction=prompt_templates.METHODOLOGY_BREAKOUT_INSTRUCTION(), + usage_label="METHODOLOGY_BREAKOUT", ) logging.debug(f"LLM Response for {filename}: {llm_response}") try: @@ -166,14 +168,15 @@ def prompt_methodology_breakout( def prompt_fee_schedule_breakout( methodology_breakout_dict: dict, reimbursement_method: str, - fs_breakout_questions: FieldSet, - constants: Constants, filename: str, ): + """ + Call FEE_SCHEDULE_BREAKOUT prompt with cached instruction. + Note: Field definitions are now included in FEE_SCHEDULE_BREAKOUT_INSTRUCTION() for caching. + """ prompt = prompt_templates.FEE_SCHEDULE_BREAKOUT( reimbursement_method, methodology_breakout_dict.get("FEE_SCHEDULE"), - fs_breakout_questions.print_prompt_dict(constants), ) logging.debug(f"Fee Schedule Breakout Prompt for {filename}: {prompt}") llm_answer_raw = llm_utils.invoke_claude( @@ -195,14 +198,15 @@ def prompt_fee_schedule_breakout( def prompt_grouper_breakout( service: str, reimbursement_method: str, - grouper_breakout_questions: FieldSet, - constants: Constants, filename: str, ): + """ + Call GROUPER_BREAKOUT prompt with cached instruction. + Note: Field definitions are now included in GROUPER_BREAKOUT_INSTRUCTION() for caching. + """ prompt = prompt_templates.GROUPER_BREAKOUT( service, reimbursement_method, - grouper_breakout_questions.print_prompt_dict(constants), ) logging.debug(f"Grouper Breakout Prompt for {filename}: {prompt}") llm_answer_raw = llm_utils.invoke_claude( @@ -225,13 +229,13 @@ def prompt_grouper_breakout( def prompt_carveout_check( service_term: str, reimb_term: str, - carveout_definitions, - special_case_definitions, filename: str, ): - carveout_prompt = prompt_templates.CARVEOUT_CHECK( - service_term, reimb_term, carveout_definitions, special_case_definitions - ) + """ + Call CARVEOUT_CHECK prompt with cached instruction. + Note: Case definitions are now included in CARVEOUT_CHECK_INSTRUCTION() for caching. + """ + carveout_prompt = prompt_templates.CARVEOUT_CHECK(service_term, reimb_term) logging.debug( f"Running carveout check for {filename} with prompt:\n{carveout_prompt}" ) @@ -489,23 +493,21 @@ def prompt_exhibit_linkage(page_header, previous_header, filename): return exhibit_is_different -def prompt_exhibit_header(page_content, EXHIBIT_HEADER_MARKERS, filename): +def prompt_exhibit_header(page_content, filename): """Extract exhibit header identifier from page content using pattern matching. Uses LLM to identify exhibit headers in page content based on predefined markers and extract the exhibit identifier. + Note: Header markers are now included in EXHIBIT_HEADER_INSTRUCTION() for caching. Args: page_content (str): First 400 characters of page content to analyze. - EXHIBIT_HEADER_MARKERS (list): List of exhibit header pattern markers. filename (str): Source filename for logging and LLM attribution. Returns: str: Extracted exhibit header identifier or marker indicating no header found. """ - prompt = prompt_templates.EXHIBIT_HEADER( - page_content[0:400], EXHIBIT_HEADER_MARKERS - ) + prompt = prompt_templates.EXHIBIT_HEADER(page_content[0:400]) llm_answer_raw = llm_utils.invoke_claude( prompt, "sonnet_latest", @@ -591,6 +593,8 @@ def prompt_dynamic_assignment( prompt = prompt_templates.REIMB_DATES_ASSIGNMENT( service_term, reimb_term, field_prompt, exhibit_text_simplified, page_num ) + instruction = prompt_templates.REIMB_DATES_ASSIGNMENT_INSTRUCTION() + usage_label = "REIMB_DATES_ASSIGNMENT" else: # Use generic DYNAMIC_ASSIGNMENT for other fields prompt = prompt_templates.DYNAMIC_ASSIGNMENT( @@ -601,14 +605,16 @@ def prompt_dynamic_assignment( exhibit_text_simplified, page_num, ) + instruction = prompt_templates.DYNAMIC_ASSIGNMENT_INSTRUCTION() + usage_label = "DYNAMIC_ASSIGNMENT" llm_answer_raw = llm_utils.invoke_claude( prompt, model_id="sonnet_latest", filename=filename, cache=True, - instruction=prompt_templates.DYNAMIC_ASSIGNMENT_INSTRUCTION(), - usage_label="DYNAMIC_ASSIGNMENT", + instruction=instruction, + usage_label=usage_label, ) try: diff --git a/src/pipelines/shared/extraction/one_to_n_funcs.py b/src/pipelines/shared/extraction/one_to_n_funcs.py index c5db04a..8dd7e74 100644 --- a/src/pipelines/shared/extraction/one_to_n_funcs.py +++ b/src/pipelines/shared/extraction/one_to_n_funcs.py @@ -166,7 +166,12 @@ def breakout( def process_single_carveout( answer_dict, carveout_definitions, special_case_definitions, filename ): - """Process a single carveout/special case in parallel""" + """Process a single carveout/special case in parallel. + + Note: carveout_definitions and special_case_definitions are still passed to this function + for the categorization logic below, but are no longer passed to prompt_carveout_check + since they are now included in CARVEOUT_CHECK_INSTRUCTION() for caching. + """ service_term, reimb_term = answer_dict.get("SERVICE_TERM"), answer_dict.get( "REIMB_TERM" ) @@ -174,8 +179,6 @@ def process_single_carveout( reimbursement_categorization = prompt_calls.prompt_carveout_check( service_term, reimb_term, - carveout_definitions, - special_case_definitions, filename, ) @@ -341,12 +344,11 @@ def methodology_breakout_single_row( ) # Methodology Breakout: AARETE_DERIVED_REIMB_METHOD, REIMB_FEE_RATE, REIMB_PCT_RATE, LESSER_OF_IND, GREATER_OF_IND, AARETE_DERIVED_PROV_TYPE + # Note: Field definitions are now included in METHODOLOGY_BREAKOUT_INSTRUCTION() for caching if reimb_term: methodology_breakout_answers = prompt_calls.prompt_methodology_breakout( service_term, reimb_term, - methodology_breakout_questions, - constants, filename, ) if isinstance( @@ -420,13 +422,12 @@ def methodology_breakout_secondary( "AARETE_DERIVED_REIMB_METHOD", "" ) # Fee Schedule Breakout + # Note: Field definitions are now included in FEE_SCHEDULE_BREAKOUT_INSTRUCTION() for caching if AARETE_DERIVED_REIMB_METHOD == "Fee Schedule": # Prompt for FS breakout, fill grouper with N/A fs_breakout_dict = prompt_calls.prompt_fee_schedule_breakout( methodology_breakout_dict, reimb_term, - fs_breakout_questions, - constants, filename, ) fs_breakout_dict.update( @@ -437,12 +438,11 @@ def methodology_breakout_secondary( ) return fs_breakout_dict # Grouper Breakout + # Note: Field definitions are now included in GROUPER_BREAKOUT_INSTRUCTION() for caching elif AARETE_DERIVED_REIMB_METHOD == "Grouper": grouper_breakout_dict = prompt_calls.prompt_grouper_breakout( service_term, reimb_term, - grouper_breakout_questions, - constants, filename, ) grouper_breakout_dict.update( @@ -681,6 +681,9 @@ def split_reimb_dates(one_to_n_results: list, filename: str) -> list: model_id="sonnet_latest", filename=filename, max_tokens=200, + cache=True, + instruction=prompt_templates.SPLIT_REIMB_DATES_INSTRUCTION(), + usage_label="SPLIT_REIMB_DATES", ) # Extract the effective and termination dates from the LLM output diff --git a/src/pipelines/shared/postprocessing/postprocessing_funcs.py b/src/pipelines/shared/postprocessing/postprocessing_funcs.py index 7a0ca41..e1f43ce 100644 --- a/src/pipelines/shared/postprocessing/postprocessing_funcs.py +++ b/src/pipelines/shared/postprocessing/postprocessing_funcs.py @@ -543,7 +543,12 @@ def update_lob_for_duals(answer_dicts): prompt = prompt_templates.DUAL_LOB_CHECK(service_term) logging.debug(f"Prompt for dual LOB check: {prompt}") claude_answer_raw = llm_utils.invoke_claude( - prompt, model_id="legacy_sonnet", filename="", max_tokens=200 + prompt, + model_id="legacy_sonnet", + filename="", + max_tokens=200, + cache=True, + instruction=prompt_templates.DUAL_LOB_CHECK_INSTRUCTION(), ) logging.debug(f"Claude response for dual LOB check: {claude_answer_raw}") claude_answer_extracted = string_utils.extract_text_from_delimiters( diff --git a/src/pipelines/shared/preprocessing/preprocessing_funcs.py b/src/pipelines/shared/preprocessing/preprocessing_funcs.py index 13c88c6..f89b901 100644 --- a/src/pipelines/shared/preprocessing/preprocessing_funcs.py +++ b/src/pipelines/shared/preprocessing/preprocessing_funcs.py @@ -187,9 +187,8 @@ def get_exhibit_pages( page_content = page_dict[page_num] if "." not in page_num or ".0" in page_num: - exhibit_header = prompt_calls.prompt_exhibit_header( - page_content, EXHIBIT_HEADER_MARKERS, filename - ) + # Note: Header markers are now included in EXHIBIT_HEADER_INSTRUCTION() for caching + exhibit_header = prompt_calls.prompt_exhibit_header(page_content, filename) if "N/A" in exhibit_header: is_exhibit = False else: diff --git a/src/prompts/prompt_templates.py b/src/prompts/prompt_templates.py index 7f088c3..f8e7104 100644 --- a/src/prompts/prompt_templates.py +++ b/src/prompts/prompt_templates.py @@ -1,9 +1,40 @@ import src.config as config from src.prompts.fieldset import FieldSet +from src.constants.constants import Constants from src.utils import string_utils PIPE_FORMAT_INSTRUCTIONS = "Briefly explain your answer, then enclose your final answer in |pipes|. If the requested information doesn't apply to this context, return |N/A|. If the information should exist but cannot be clearly identified, return |UNKNOWN|." +# Module-level cached Constants instance for instruction functions +# This is lazily initialized on first use and reused across all instruction calls +_cached_constants = None + + +def _get_constants() -> Constants: + """Get or create the cached Constants instance for instruction functions.""" + global _cached_constants + if _cached_constants is None: + _cached_constants = Constants() + return _cached_constants + + +def _get_fields_text(field_type: str) -> str: + """Load fields from investment_prompts.json and format them with resolved valid_values. + + Args: + field_type: The field_type to filter by (e.g., 'methodology_breakout', 'fee_schedule_breakout') + + Returns: + Formatted string of field definitions with resolved valid_values + """ + constants = _get_constants() + fields = FieldSet( + file_path="src/prompts/investment_prompts.json", + relationship="one_to_n", + field_type=field_type, + ) + return fields.print_prompt_dict(constants) + def get_cacheable_instructions(): """ @@ -72,10 +103,16 @@ def get_cacheable_instructions(): if config.FIELDS in ["all", "one_to_n"]: try: cacheable_instructions["DYNAMIC_PRIMARY"] = DYNAMIC_PRIMARY_INSTRUCTION() + cacheable_instructions["DYNAMIC_PRIMARY_TEXT"] = ( + DYNAMIC_PRIMARY_TEXT_INSTRUCTION() + ) cacheable_instructions["CARVEOUT_CHECK"] = CARVEOUT_CHECK_INSTRUCTION() cacheable_instructions["DYNAMIC_ASSIGNMENT"] = ( DYNAMIC_ASSIGNMENT_INSTRUCTION() ) + cacheable_instructions["REIMB_DATES_ASSIGNMENT"] = ( + REIMB_DATES_ASSIGNMENT_INSTRUCTION() + ) cacheable_instructions["LESSER_OF_DISTRIBUTION"] = ( LESSER_OF_DISTRIBUTION_INSTRUCTION() ) @@ -122,6 +159,20 @@ def get_cacheable_instructions(): cacheable_instructions["SPECIAL_CASE_ASSIGNMENT"] = ( SPECIAL_CASE_ASSIGNMENT_INSTRUCTION() ) + cacheable_instructions["SPLIT_REIMB_DATES"] = SPLIT_REIMB_DATES_INSTRUCTION() + except Exception: + pass + + # Code extraction instructions + try: + cacheable_instructions["CODE_EXPLICIT"] = CODE_EXPLICIT_INSTRUCTION() + cacheable_instructions["CODE_CATEGORY"] = CODE_CATEGORY_INSTRUCTION() + cacheable_instructions["CODE_IMPLICIT_SPECIAL"] = ( + CODE_IMPLICIT_SPECIAL_INSTRUCTION() + ) + cacheable_instructions["CODE_IMPLICIT"] = CODE_IMPLICIT_INSTRUCTION() + cacheable_instructions["CODE_LAST_CHECK"] = CODE_LAST_CHECK_INSTRUCTION() + cacheable_instructions["FILL_BILL_TYPE"] = FILL_BILL_TYPE_INSTRUCTION() except Exception: pass @@ -133,114 +184,96 @@ def get_cacheable_instructions(): ##################################################################################### -def EXHIBIT_LEVEL(context, fields): - return f"""Extract attributes from a section of a contract. +def EXHIBIT_LEVEL_INSTRUCTION() -> str: + """Static instruction for EXHIBIT_LEVEL prompt caching. + Contains all formatting rules and general instructions. + """ + return """[OBJECTIVE] +Extract attributes from a section of a contract. [FORMATTING INSTRUCTIONS AND DESIRED OUTPUT] -Return a json dictionary with the following attributes. It is possible that the page will not have some of the attributes. When this is the case, return N/A. +Return a JSON dictionary with the following attributes. It is possible that the page will not have some of the attributes. When this is the case, return N/A. +[GENERAL INSTRUCTIONS] +- For each field, return ALL correct answers found. There may be more than one correct answer. If there are multiple, return them in a pipe-separated list ("|"). +- If a list of valid values is provided, ONLY answer with the EXACT value from that list. +- If there are no correct answers for a given field, write 'N/A' as the answer for that field. +- Be consistent and deterministic; avoid creative paraphrasing. + +[OUTPUT FORMAT] +Briefly explain your answer, then put the final answer in the properly-formatted JSON dictionary.""" + + +def EXHIBIT_LEVEL(context, fields): + """Returns ONLY dynamic content for exhibit level extraction. + Call EXHIBIT_LEVEL_INSTRUCTION() separately for the cached instruction. + """ + return f"""[ATTRIBUTES] Here are the attributes to be included in the dictionary, and instructions on how to correctly answer: {fields} -[GENERAL INSTRUCTIONS] -For each field, return ALL correct answers found. There may be more than one correct answer. If there are multiple, return them in a pipe-separated list ("|"). -If a list of valid values is provided, ONLY answer with the EXACT value from that list. -If there are no correct answers for a given field, write 'N/A' as the answer for that field. - +[CONTEXT] Here is the text to analyze: - -{context.replace('"', "'")} - -Briefly explain your answer, then put the final answer in the properly-formatted JSON dictionary. -""" +{context.replace('"', "'")}""" -def EXHIBIT_LEVEL_INSTRUCTION() -> str: - return ( - "[OBJECTIVE]\n" - "Extract structured attributes from contract text. Follow formatting and output rules in the prompt.\n" - "Be consistent and deterministic; avoid creative paraphrasing." - ) - - -def DYNAMIC_PRIMARY_HEADER(context, field_name, field_prompt): - return f"""Extract attributes from the provided text, paying close attention to detail. - -[ATTRIBUTE TO EXTRACT] -Here is the attribute, and instructions on how to correctly answer: -{field_name} : {field_prompt} +def DYNAMIC_PRIMARY_TEXT_INSTRUCTION() -> str: + """Static instruction for DYNAMIC_PRIMARY_TEXT prompt caching. + Contains extraction rules for text-based fields (allows obvious assumptions). + """ + return """[OBJECTIVE] +Extract attribute values for dynamic fields from contract text, paying close attention to detail. [GENERAL INSTRUCTIONS] - Return all valid, explicitly stated values. If multiple values are found, list them separated by commas. - If none of the valid values appear explicitly in the text, return N/A. -- Do not make assumptions. Only write an answer if it is clearly and explicitly stated. +- Make appropriate and obvious assumptions, but don't take huge leaps of logic. + - For example, you can assume that "Children's Health Insurance Program Perinate (CHIP-P)" is equivalent to "CHIP Perinate", but do not assume that "Children's Health Insurance Program Perinate (CHIP-P)" is equivalent to "Medicaid". -Here is the text to analyze: - -{context.replace('"', "'")} - -Briefly explain your answer before putting the final answer in |pipes|. -""" +[OUTPUT FORMAT] +Briefly explain your answer before putting the final answer in |pipes|.""" def DYNAMIC_PRIMARY_TEXT(context, field_name, field_prompt): - return f"""Extract attributes from the provided text, paying close attention to detail. - -[ATTRIBUTE TO EXTRACT] -Here is the attribute, and instructions on how to correctly answer: + """Returns ONLY dynamic content for text-based dynamic primary extraction. + Call DYNAMIC_PRIMARY_TEXT_INSTRUCTION() separately for the cached instruction. + """ + return f"""[ATTRIBUTE TO EXTRACT] {field_name} : {field_prompt} -[GENERAL INSTRUCTIONS] -- Return all valid, explicitly stated values. If multiple values are found, list them separated by commas. -- If none of the valid values appear explicitly in the text, return N/A. -- Make appropriate and obvious assumptions, but don't take huge leaps of logic. - - For example, you can assume that "Children's Health Insurance Program Perinate (CHIP-P)" is equivalent to "CHIP Perinate", but do not assume that "Children's Health Insurance Program Perinate (CHIP-P)" is equivalent to "Medicaid". - +[CONTEXT] Here is the text to analyze: - -{context.replace('"', "'")} - -Briefly explain your answer before putting the final answer in |pipes|. -""" +{context.replace('"', "'")}""" def DYNAMIC_PRIMARY_INSTRUCTION() -> str: + """Legacy instruction for backward compatibility. + Use DYNAMIC_PRIMARY_HEADER_INSTRUCTION or DYNAMIC_PRIMARY_TEXT_INSTRUCTION instead. + """ return ( "[OBJECTIVE]\n" "Extract attribute values for dynamic fields from contract text. Use exact values and return in pipes where instructed." ) -def REIMB_DATES_ASSIGNMENT( - service_term: str, - reimb_term: str, - field_prompt: str, - exhibit_text_simplified: str, - page_num: str, -): +def REIMB_DATES_ASSIGNMENT_INSTRUCTION() -> str: + """Static instruction for REIMB_DATES_ASSIGNMENT prompt caching. + Contains all date assignment rules and examples. """ - Specialized prompt for assigning REIMB_DATES to specific reimbursement terms. - - This is MORE SPECIFIC than the discovery prompt and focuses on assigning the - correct date range to each individual reimbursement term based on context. - """ - - prompt = f"""[OBJECTIVE] + return """[OBJECTIVE] Identify which date range applies to a specific Reimbursement Term. - -[INSTRUCTIONS] You will be shown a Reimbursement Term and the section of a contract from which that term was extracted. Your task is to determine which date range applies to THIS SPECIFIC reimbursement term. -CRITICAL RULES FOR DATE ASSIGNMENT: +[CRITICAL RULES FOR DATE ASSIGNMENT] 1. **TABLE/SECTION GROUPING** - MOST IMPORTANT: If multiple reimbursement terms appear in the SAME TABLE or under the SAME SECTION HEADER, they ALL share the SAME date range. - + Example: "For services during the period January 16, 2018 through June 30, 2018. [Table with: Ambulatory Surgery Services Rate: 193.75%, Cost Items Rate: 100%, Default Pricing Rate: 50%]" - + → ALL three rates (193.75%, 100%, 50%) get "January 16, 2018 through June 30, 2018" → Even if you see OTHER date ranges elsewhere in the contract, these terms are in THIS table, so they get THIS date range ONLY @@ -252,13 +285,13 @@ CRITICAL RULES FOR DATE ASSIGNMENT: 3. **AVOID CROSS-SECTION CONTAMINATION**: Contracts often have multiple sections with different date ranges: - + "For services during January 16, 2018 through June 30, 2018: [Table 1 with rates A, B, C] - + For services during July 1, 2018 through June 30, 2019: [Table 2 with rates D, E, F]" - + → Terms in Table 1 (A, B, C) get ONLY "January 16, 2018 through June 30, 2018" → Terms in Table 2 (D, E, F) get ONLY "July 1, 2018 through June 30, 2019" → DO NOT assign both date ranges to the same term @@ -281,7 +314,28 @@ CRITICAL RULES FOR DATE ASSIGNMENT: If a regular date exists in the section, use it. If ONLY CY/FY format is available, return "N/A". -[REIMB_DATES FIELD DEFINITION] +[OUTPUT FORMAT] +First, identify: +1. What section or table is this reimbursement term in? +2. What date range introduces or applies to that specific section/table? +3. Are there other date ranges in different sections? (These should NOT be assigned to this term) + +Then, return a properly-formatted JSON object where the key is "REIMB_DATES" and the value is the correct date range for THIS SPECIFIC term. +Return ONLY the single date range that applies to this term. Do NOT return multiple date ranges separated by commas. +If there is no date range for this term, return "N/A".""" + + +def REIMB_DATES_ASSIGNMENT( + service_term: str, + reimb_term: str, + field_prompt: str, + exhibit_text_simplified: str, + page_num: str, +): + """Returns ONLY dynamic content for REIMB_DATES assignment. + Call REIMB_DATES_ASSIGNMENT_INSTRUCTION() separately for the cached instruction. + """ + return f"""[REIMB_DATES FIELD DEFINITION] {field_prompt} [CONTEXT] @@ -293,20 +347,7 @@ Here is the section of the contract. Examine this section closely and identify w [REIMBURSEMENT TERM TO ANALYZE] This is the term you need to find the date range for. It appears on page {page_num} of the text. Service Term: "{service_term}" -Reimbursement Term: "{reimb_term}" - -[OUTPUT FORMAT] -First, identify: -1. What section or table is this reimbursement term in? -2. What date range introduces or applies to that specific section/table? -3. Are there other date ranges in different sections? (These should NOT be assigned to this term) - -Then, return a properly-formatted JSON object where the key is "REIMB_DATES" and the value is the correct date range for THIS SPECIFIC term. -Return ONLY the single date range that applies to this term. Do NOT return multiple date ranges separated by commas. -If there is no date range for this term, return "N/A". -""" - - return prompt +Reimbursement Term: "{reimb_term}" """ def DYNAMIC_ASSIGNMENT_INSTRUCTION() -> str: @@ -588,24 +629,14 @@ Briefly explain, then return JSON in |pipes|.""" def EXHIBIT_TITLE_MATCH_INSTRUCTION() -> str: - return ( - "[OBJECTIVE]\n" - "Determine if two exhibit references refer to the same exhibit. Consider exact, partial, and descriptive matches.\n" - "Return YES or NO in pipes." - ) - - -def EXHIBIT_TITLE_MATCH(current_exhibit_title: str, target_exhibit_reference: str): + """Static instruction for EXHIBIT_TITLE_MATCH prompt caching. + Contains all matching rules and examples. """ - Returns prompt for exhibit title matching. - Call EXHIBIT_TITLE_MATCH_INSTRUCTION() separately for the cached instruction. - """ - return f"""[CONTEXT] -Current Exhibit Title: "{current_exhibit_title}" -Target Exhibit Reference: "{target_exhibit_reference}" + return """[OBJECTIVE] +Determine if two exhibit references refer to the same exhibit. -[INSTRUCTIONS] -Determine if these refer to the same exhibit. Consider: +[MATCHING RULES] +Consider the following when matching: - Exact matches: "Exhibit B" matches "Exhibit B" - Partial matches: "Exhibit B" matches "Exhibit B - Outpatient Services" - Descriptive matches: "Outpatient Services" matches "Exhibit B - Outpatient Services" @@ -613,10 +644,16 @@ Determine if these refer to the same exhibit. Consider: - Different services: "Inpatient Services" does NOT match "Outpatient Services" [OUTPUT FORMAT] -Return ONLY "YES" or "NO" in |pipes|. +Briefly explain your reasoning, then return ONLY "YES" or "NO" in |pipes|.""" -Briefly explain your reasoning, then return your answer in |pipes|. -""" + +def EXHIBIT_TITLE_MATCH(current_exhibit_title: str, target_exhibit_reference: str): + """Returns ONLY dynamic content for exhibit title matching. + Call EXHIBIT_TITLE_MATCH_INSTRUCTION() separately for the cached instruction. + """ + return f"""[CONTEXT] +Current Exhibit Title: "{current_exhibit_title}" +Target Exhibit Reference: "{target_exhibit_reference}" """ ##################################################################################### @@ -624,28 +661,30 @@ Briefly explain your reasoning, then return your answer in |pipes|. ##################################################################################### -def METHODOLOGY_BREAKOUT(service_term: str, reimb_term: str, questions: str): +def METHODOLOGY_BREAKOUT(service_term: str, reimb_term: str): """ - Returns prompt for methodology breakout extraction. + Returns ONLY dynamic content for methodology breakout extraction. Call METHODOLOGY_BREAKOUT_INSTRUCTION() separately for the cached instruction. - All fields run by this prompt will end up becoming their own row, rather than being distributed across the rows of the exhibit + All fields run by this prompt will end up becoming their own row, rather than being distributed across the rows of the exhibit. + Note: questions/fields are now included in METHODOLOGY_BREAKOUT_INSTRUCTION() for caching. """ - prompt = f"""Here is the text to analyze and respond to: + return f"""Here is the text to analyze and respond to: Service Term: {service_term} Reimbursement Term: {reimb_term} -[OUTPUT FORMAT] Explain your reasoning as you work through the context. After your explanation, include the heading "FINAL REIMBURSEMENT METHODOLOGY:" followed by a properly formatted JSON list with your final output.""" - # Include dynamic fields in the prompt (not instruction) for cache reuse - prompt += f"\n\n[FIELDS]\nPopulate a list of JSON dictionaries with the following fields:\n{questions}\n" - - return prompt - def METHODOLOGY_BREAKOUT_INSTRUCTION() -> str: - return """[OBJECTIVE] + """Static instruction for METHODOLOGY_BREAKOUT prompt caching. + Contains all extraction rules, field definitions with resolved valid_values. + Field definitions are loaded from investment_prompts.json. + """ + # Load fields from investment_prompts.json with resolved valid_values + fields_text = _get_fields_text("methodology_breakout") + + return f"""[OBJECTIVE] Analyze a given term from a Payer-Provider contract and extract key fields. [INSTRUCTION] @@ -665,6 +704,10 @@ Analyze a given term from a Payer-Provider contract and extract key fields. If AARETE_DERIVED_REIMB_METHOD is Grouper, the dollar amount refers to REIMB_CONVERSION_FACTOR. If AARETE_DERIVED_REIMB_METHOD is Flat Rate or similar, the dollar amount refers to a REIMB_FEE_RATE. +[FIELDS] +Populate a list of JSON dictionaries with the following fields: +{fields_text} + [OUTPUT FORMAT] The output should always be a JSON list/array of dictionaries, even when there is only one methodology. For any fields that don't apply to a particular methodology, use "N/A" as the value. @@ -673,46 +716,47 @@ For any fields that don't apply to a particular methodology, use "N/A" as the va def FEE_SCHEDULE_BREAKOUT_INSTRUCTION() -> str: - return ( - "[OBJECTIVE]\n" - "Extract fee schedule-specific attributes from a reimbursement methodology into JSON fields.\n" - "Use explicit contract language only and return 'N/A' when information is missing." - ) + """Static instruction for FEE_SCHEDULE_BREAKOUT prompt caching. + Contains objective, field definitions with resolved valid_values, and output format rules. + Field definitions are loaded from investment_prompts.json. + """ + # Load fields from investment_prompts.json with resolved valid_values + fields_text = _get_fields_text("fee_schedule_breakout") - -def FEE_SCHEDULE_BREAKOUT(methodology, fee_schedule, questions): return f"""[OBJECTIVE] -Analyze a given reimbursement methodology from a Payer-Provider contract: +Analyze a given reimbursement methodology from a Payer-Provider contract and extract fee schedule-specific attributes into JSON fields. +Use explicit contract language only and return 'N/A' when information is missing. [FIELDS] Populate a JSON dictionary with the following fields: -{questions} -Use the text in the methodology to populate a JSON dictionary with the following fields, specifically for {fee_schedule} Fee Schedule: - -[CONTEXT] -Here is the text to analyze and respond to: -Methodology: {methodology.replace('"', "'")} +{fields_text} [OUTPUT FORMAT] -Write your JSON dictionary below: -""" +Briefly explain your reasoning, then write your JSON dictionary.""" + + +def FEE_SCHEDULE_BREAKOUT(methodology, fee_schedule): + """Returns ONLY dynamic content for fee schedule breakout. + Call FEE_SCHEDULE_BREAKOUT_INSTRUCTION() separately for the cached instruction. + Note: Field definitions are now included in FEE_SCHEDULE_BREAKOUT_INSTRUCTION() for caching. + """ + return f"""[CONTEXT] +Analyze and respond to the following text, specifically for {fee_schedule} Fee Schedule: +Methodology: {methodology.replace('"', "'")}""" def GROUPER_BREAKOUT_INSTRUCTION() -> str: - return ( - "[OBJECTIVE]\n" - "Extract grouper-based reimbursement attributes from service and reimbursement terms into JSON fields.\n" - "Use exact contract terminology and return 'N/A' when information is missing." - ) + """Static instruction for GROUPER_BREAKOUT prompt caching. + Contains objective, extraction rules, field definitions, and output format. + Field definitions are loaded from investment_prompts.json. + """ + # Load fields from investment_prompts.json with resolved valid_values + fields_text = _get_fields_text("grouper_breakout") - -def GROUPER_BREAKOUT(service, term, questions): return f"""[OBJECTIVE] -Analyze the given service and reimbursement term to identify grouper-based reimbursement information, and answer the following question(s): - -[FIELDS] -Populate a JSON dictionary with the following fields: -{questions} +Analyze service and reimbursement terms to identify grouper-based reimbursement information. +Extract grouper-based reimbursement attributes into JSON fields using exact contract terminology. +Return 'N/A' when information is missing. [KEY EXTRACTION RULES] - Extract exact terminology as it appears in the contract text @@ -720,14 +764,23 @@ Populate a JSON dictionary with the following fields: - For multiple values: separate with commas, not arrays - Return N/A when no relevant information is found -[CONTEXT] -Here are the service and reimbursement terms to analyze: -SERVICE: {service} -REIMBURSEMENT TERM: {term} +[FIELDS] +Populate a JSON dictionary with the following fields: +{fields_text} [OUTPUT FORMAT] -Briefly explain your answer, then return a valid JSON dictionary using the exact field names from the questions as keys. For any information not found in the Term, return 'N/A' -""" +Briefly explain your answer, then return a valid JSON dictionary using the exact field names from the questions as keys. For any information not found in the Term, return 'N/A'.""" + + +def GROUPER_BREAKOUT(service, term): + """Returns ONLY dynamic content for grouper breakout. + Call GROUPER_BREAKOUT_INSTRUCTION() separately for the cached instruction. + Note: Field definitions are now included in GROUPER_BREAKOUT_INSTRUCTION() for caching. + """ + return f"""[CONTEXT] +Here are the service and reimbursement terms to analyze: +SERVICE: {service} +REIMBURSEMENT TERM: {term}""" def SPECIAL_CASE_BREAKOUT(term, questions): @@ -775,39 +828,40 @@ def TRIGGER_CAP_BREAKOUT(term): def OUTLIER_BREAKOUT_INSTRUCTION() -> str: - return ( - "[OBJECTIVE]\n" - "Analyze a healthcare payer-provider contract to extract outlier payment information. " - "Outlier payments are additional supplemental reimbursements triggered when claims exceed " - "predetermined thresholds for cost, length of stay, or resource utilization.\n\n" - "**Extraction Guidelines:**\n" - "- Extract exact field values (numbers, percentages, dollar amounts) as specified\n" - "- Convert time units to days when requested (e.g., 'two weeks' = '14')\n" - "- Include code types when extracting exclusions (e.g., 'CPT 12345')\n" - "- For frequency fields, use exact contract language (e.g., 'per admission', 'per day')\n" - "- Base answers ONLY on information explicitly stated or directly inferable from the contract text" - ) + """Static instruction for OUTLIER_BREAKOUT prompt caching. + Contains objective, extraction guidelines, and field definitions. + Field definitions are loaded from investment_prompts.json. + """ + # Load fields from investment_prompts.json with resolved valid_values + fields_text = _get_fields_text("outlier_breakout") + + return f"""[OBJECTIVE] +Analyze a healthcare payer-provider contract to extract outlier payment information. Outlier payments are additional supplemental reimbursements triggered when claims exceed predetermined thresholds for cost, length of stay, or resource utilization. + +**Extraction Guidelines:** +- Extract exact field values (numbers, percentages, dollar amounts) as specified +- Convert time units to days when requested (e.g., 'two weeks' = '14') +- Include code types when extracting exclusions (e.g., 'CPT 12345') +- For frequency fields, use exact contract language (e.g., 'per admission', 'per day') +- Base answers ONLY on information explicitly stated or directly inferable from the contract text + +[FIELDS] +Populate a JSON dictionary with the following fields: +{fields_text} + +[OUTPUT FORMAT] +Briefly explain your answer, then provide the extracted outlier payment information in JSON format. For any information not found in the Term, return 'N/A'.""" def OUTLIER_BREAKOUT(term): """ - Returns prompt for outlier breakout extraction. + Returns ONLY dynamic content for outlier breakout extraction. Call OUTLIER_BREAKOUT_INSTRUCTION() separately for the cached instruction. + Note: Field definitions are now included in OUTLIER_BREAKOUT_INSTRUCTION() for caching. """ - questions = FieldSet( - config.FIELD_JSON_PATH, field_type="outlier_breakout" - ).print_prompt_dict() - - return f"""[FIELDS] -Populate a JSON dictionary with the following fields: -{questions} - -[CONTEXT] + return f"""[CONTEXT] Here is the text to analyze and respond to: -{term} - -[OUTPUT FORMAT] -Briefly explain your answer, then provide the extracted outlier payment information in JSON format. For any information not found in the Term, return 'N/A'""" +{term}""" def FACILITY_ADJUSTMENT_BREAKOUT(term): @@ -857,37 +911,50 @@ def STOP_LOSS_BREAKOUT(term): ##################################################################################### -def CODE_EXPLICIT(service, methodology, questions): - return f"""Analyze a given medical service: +def CODE_EXPLICIT_INSTRUCTION() -> str: + """Static instruction for CODE_EXPLICIT prompt caching. + Contains objective, instructions, and field definitions. + Field definitions are loaded from investment_prompts.json (field_type=code_primary_breakout). + """ + # Load fields from investment_prompts.json with resolved valid_values + fields_text = _get_fields_text("code_primary_breakout") -Use the text in the Service and Methodology to populate a JSON dictionary with the following fields: - -{questions}. + return f"""[OBJECTIVE] +Extract explicit procedure, revenue, diagnosis, and other healthcare codes from medical service descriptions. +[INSTRUCTIONS] - For each field, return a list of all codes EXPLICITLY written for that field. The code itself must be written, not language that describes the code. - Note that codes may be present, but not explicitly labeled as codes. For example, you may simply see "J1098", which is a PROCEDURE_CD. You may see "155" which is a REVENUE_CD, etc. - If the text gives a range of codes, without listing each code in the range individually, return the range in the format 'LowestCode-HighestCode'. - If the text describes an entire Code Category (not one specific code) based on a start letter, return the Category in the form "Category: X". e.g. "A-Codes" --> "Category: A", "K-Codes" --> "Category K", etc. - If the text explicitly says that there is no published or established rate, write "NOT_ESTABLISHED" for the PROCEDURE_CD value. - If any of the codes are not found, do not write a list for that field. Simply populate the field with an empty list []. -- If a standalone 3-digit code appears without explicit labels (e.g., “DRG,” “revenue,” “Rev code”), analyze the surrounding context for clues. Look for any direct or indirect references—no matter how subtle—that may suggest a connection to either a Revenue Code or a Grouper Code. If any contextual clues imply relevance to either classification, categorize the code accordingly. +- If a standalone 3-digit code appears without explicit labels (e.g., "DRG," "revenue," "Rev code"), analyze the surrounding context for clues. Look for any direct or indirect references—no matter how subtle—that may suggest a connection to either a Revenue Code or a Grouper Code. If any contextual clues imply relevance to either classification, categorize the code accordingly. +[FIELDS] +Populate a JSON dictionary with the following fields: +{fields_text} + +[OUTPUT FORMAT] +Briefly explain your answer before putting the final answer in JSON dictionary format.""" + + +def CODE_EXPLICIT(service, methodology): + """ + Returns ONLY dynamic content for code explicit extraction. + Call CODE_EXPLICIT_INSTRUCTION() separately for the cached instruction. + Note: Field definitions are now included in CODE_EXPLICIT_INSTRUCTION() for caching. + """ + return f"""[CONTEXT] Here is the Service and Methodology to analyze: Service: {service.replace('"', "'")} -Methodology: {methodology.replace('"', "'")} - -Briefly explain your answer before putting the final answer in JSON dictionary format. -""" +Methodology: {methodology.replace('"', "'")}""" -def CODE_CATEGORY(service, choices): - return f"""Analyze a given medical Service Term. - -What Procedure Code description does the Service Term describe? - -[VALID DESCRIPTIONS] -Here are the descriptions to choose from: -{choices} +def CODE_CATEGORY_INSTRUCTION() -> str: + """Static instruction for CODE_CATEGORY prompt caching.""" + return """[OBJECTIVE] +Match a medical Service Term to its corresponding Procedure Code description based on code categories. [INSTRUCTIONS] 1. Determine if the descriptions are needed. If the Service Term mentions the Code Category (a single-character e.g. A, B, C, ...), without any additional subcategories or descriptors, then there is no need to reference the descriptions. For example, if the Service Term is "A-Codes", the answer is "A0000-A9999". This represents the full range of A-Codes. @@ -895,18 +962,26 @@ Here are the descriptions to choose from: 2. If there are additional subcategories or descriptors accompanying the code category, match them to the closest item or items from the VALID DESCRIPTIONS. - There can be multiple correct answers. When this is the case, return them all in a list. This may be phrased like "Service1/Service2" or "Service1 and Service2", in which case both Services must be considered. - There may be no correct answers. When this is the case, return the full range of codes in that category (e.g. A0000-A9999, etc.) - -[CONTEXT] -Here is the Service Term to analyze: -{service.replace('"', "'")} [OUTPUT FORMAT] -Explain your answer in 1-2 sentences. Write your final answer in JSON list format. -""" +Explain your answer in 1-2 sentences. Write your final answer in JSON list format.""" -def CODE_IMPLICIT_SPECIAL(service): - return f"""Analyze a given medical Service Term. +def CODE_CATEGORY(service, choices): + """Call CODE_CATEGORY_INSTRUCTION() separately for the cached instruction.""" + return f"""[VALID DESCRIPTIONS] +Here are the descriptions to choose from: +{choices} + +[CONTEXT] +Here is the Service Term to analyze: +{service.replace('"', "'")}""" + + +def CODE_IMPLICIT_SPECIAL_INSTRUCTION() -> str: + """Static instruction for CODE_IMPLICIT_SPECIAL prompt caching.""" + return """[OBJECTIVE] +Classify a medical Service Term into special predefined categories for implicit code mapping. [INSTRUCTIONS] - If the Service refers to the broad categories of Drugs, Medications, Injectible Medications, Pharmaceuticals or similar, without referring to a more specific drug category, respond "Drugs". Do NOT use this category if vaccines are included. @@ -916,22 +991,21 @@ def CODE_IMPLICIT_SPECIAL(service): - If the Service refers to Multiple procedures, bilateral procedures, or similar, without referring to a single specific procedure or type of procedure, respond "Surgery" - If none of the above cases apply, respond "N/A". If any of the above are mentioned but only in the context of being excluded from the service, respond "N/A". +[OUTPUT FORMAT] +Briefly explain your answer, but put your final answer in |pipes|""" + + +def CODE_IMPLICIT_SPECIAL(service): + """Call CODE_IMPLICIT_SPECIAL_INSTRUCTION() separately for the cached instruction.""" + return f"""[CONTEXT] Here is the Service to analyze: -{service} - -Briefly explain your answer, but put your final answer in |pipes| -""" +{service}""" -def CODE_IMPLICIT(service, choices): - - return f"""Analyze a given medical Service Term. - -What Procedure Code description does the Service Term describe? - -[VALID DESCRIPTIONS] -Here are the descriptions to choose from: -{choices} +def CODE_IMPLICIT_INSTRUCTION() -> str: + """Static instruction for CODE_IMPLICIT prompt caching.""" + return """[OBJECTIVE] +Match a medical Service Term to its corresponding Procedure Code description using implicit matching logic. [INSTRUCTIONS] 1. Determine which part of the Service Term is the Service itself. The term may include additional confusing information that is irrelevant. Here are some things to look out for: @@ -944,31 +1018,68 @@ Here are the descriptions to choose from: - IGNORE distracting terms like "Services" or "Procedures" in both the Service and the Description. They are meaningless for the purpose of this exercise. - For each Description in VALID DESCRIPTIONS, ask yourself the following question: Is the Description SYNONYMOUS with the Service? - **Note**: Do NOT simply categorize the Service into the Description that it falls under. You are looking specifically for synonymous Service-Description matches. For example, "Nutritional Evaluations" is not synonymous with "Evaluations", because not ALL "Evaluations" are "Nutritional Evaluations". - - There can be multiple correct answers. When this is the case, return them all in a list. + - There can be multiple correct answers. When this is the case, return them all in a list. - There may be no correct answers, especially when the Service is much more specific than any of the descriptions. When this is the case, simply return an empty list "[]". +[OUTPUT FORMAT] +Explain your answer, ensuring each point in the instructions is addressed. Then return your final answer in JSON list format.""" + + +def CODE_IMPLICIT(service, choices): + """Call CODE_IMPLICIT_INSTRUCTION() separately for the cached instruction.""" + return f"""[VALID DESCRIPTIONS] +Here are the descriptions to choose from: +{choices} + [CONTEXT] Here is the Service Term to analyze: -{service.replace('"', "'")} +{service.replace('"', "'")}""" + + +def CODE_LAST_CHECK_INSTRUCTION() -> str: + """Static instruction for CODE_LAST_CHECK prompt caching.""" + return """[OBJECTIVE] +Analyze a given healthcare-related term to determine if it represents a specific medical service. + +[CLASSIFICATION RULES] +- Return "Specific" if the term is a specific medical service or category of services +- Return "Generic" if the term is: + - A generic medical service + - Not a service at all + - The name of a hospital or other provider + - A nonsensical or non-service phrase [OUTPUT FORMAT] -Explain your answer, ensuring each point in the instructions is addressed. Then return your final answer in JSON list format. -""" +Briefly explain your answer, then put your final answer in |pipes|.""" def CODE_LAST_CHECK(service): - return f"""Analyze the given healthcare-related term. + """Call CODE_LAST_CHECK_INSTRUCTION() separately for the cached instruction.""" + return f"""[SERVICE TO ANALYZE] +{service}""" -Determine if the term is a specific medical service or category of services. If so, return "Specific". -If the term is a generic medical service, not a service at all, the name of a hospital or other provider, or other nonsensical or non-service phrase, return "Generic". -Here is the service to analyze: {service} +def FILL_BILL_TYPE_INSTRUCTION() -> str: + """Static instruction for FILL_BILL_TYPE prompt caching.""" + return """[OBJECTIVE] +Analyze a given medical Service Term to determine which Bill Type Code Description it falls under. -Briefly explain your answer, then put your final answer in |pipes|. -""" +[INSTRUCTIONS] +- While determining the bill type code, first prioritize the place where the service is taking place, that will always take precedence over the service only terminology. For example in home dialysis, we can recognize home as place of service and only need to identify that for code determination. However if place of service is not available fall back on the service term as long as it is clearly and unambiguously referred to by the service and is present in [VALID DESCRIPTIONS]. +- Answer with the Description or Descriptions from [VALID DESCRIPTIONS] only if it is clearly and unambiguously referred to by the Service. Do not attempt much inference, the answer will be very clear if it is present. +- It is highly likely that none of the Descriptions will be a clear and unambiguous match. When this is the case, return an empty list: [] +- Here are some examples: + - "Ambulatory Surgery Procedures" --> ["Ambulatory Surgery Center"] + - "Hospital Services" --> ["Inpatient Hospital", "Outpatient Hospital"] + - "home dialysis" --> ["Home"] + - "dialysis" --> ["Dialysis Center"] + +[OUTPUT FORMAT] +Briefly explain your answer, then return your final answer in JSON list format.""" def FILL_BILL_TYPE(service, choices, reimb_term=None, exhibit_text=None): + """Call FILL_BILL_TYPE_INSTRUCTION() separately for the cached instruction.""" # Build reimbursement term section conditionally reimb_term_section = "" if reimb_term and not string_utils.is_empty(reimb_term): @@ -987,31 +1098,13 @@ If the Service Term and Reimbursement Term together do not provide sufficient in {exhibit_text.replace('"', "'")} """ - return f"""Analyze a given medical Service Term. - -What Bill Type Code Description does the Service Term fall under? - -[VALID DESCRIPTIONS] + return f"""[VALID DESCRIPTIONS] Here are the descriptions to choose from: {choices} -[INSTRUCTIONS] -- While determining the bill type code, first prioritize the place where the service is taking place, that will always take precedence over the service only terminology. For example in home dialysis, we can recognize home as place of service and only need to identify that for code determination. However if place of service is not available fall back on the service term as long as it is clearly and unambiguously referred to by the service and is present in [VALID DESCRIPTIONS]. -- Answer with the Description or Descriptions from [VALID DESCRIPTIONS] only if it is clearly and unambiguously referred to by the Service. Do not attempt much inference, the answer will be very clear if it is present. -- It is highly likely that none of the Descriptions will be a clear and unambiguous match. When this is the case, return an empty list: [] -- Here are some examples: - - "Ambulatory Surgery Procedures" --> ["Ambulatory Surgery Center"] - - "Hospital Services" --> ["Inpatient Hospital", "Outpatient Hospital"] - - "home dialysis" --> ["Home"] - - "dialysis" --> ["Dialysis Center"] - -[CONTEXT] -Here is the Service Term to analyze: +[SERVICE TERM TO ANALYZE] {service.replace('"', "'")} -{reimb_term_section}{exhibit_context_section} -[OUTPUT FORMAT] -Briefly explain your answer, then return your final answer in JSON list format. -""" +{reimb_term_section}{exhibit_context_section}""" ##################################################################################### @@ -1130,29 +1223,13 @@ def FULL_CONTEXT_PAYER_NAME_ADDITIONAL_INSTRUCTION(): def CHECK_PROVIDER_NAME_MATCH_INSTRUCTION() -> str: - return ( - "[OBJECTIVE]\n" - "Determine if two provider names refer to the same healthcare organization.\n" - "Consider legal suffixes, D/B/A variations, acronyms, and common word variations.\n" - "Return Y or N in pipes." - ) - - -def CHECK_PROVIDER_NAME_MATCH_PROMPT( - provider_name: str, hybrid_smart_chunking_provider_group_name: str -) -> str: + """Static instruction for CHECK_PROVIDER_NAME_MATCH prompt caching. + Contains all matching rules and critical exclusions. """ - Create prompt for LLM-based provider name matching. - Call CHECK_PROVIDER_NAME_MATCH_INSTRUCTION() separately for the cached instruction. - """ - return f"""Determine if the provider name matches any of the healthcare organizations listed. + return """[OBJECTIVE] +Determine if two provider names refer to the same healthcare organization. -PROVIDER NAME A: {provider_name} -PROVIDER NAME B: {hybrid_smart_chunking_provider_group_name} - -Note: PROVIDER NAME B is typically a single name but may contain multiple names separated by ' | '. Determine if PROVIDER NAME A matches PROVIDER NAME B (or ANY name if multiple are present). - -MATCHING RULES: +[MATCHING RULES] 1. PROVIDER NAME A must appear in PROVIDER NAME B (either as an exact match or close variation). 2. If PROVIDER NAME B contains multiple names (separated by ' | '), check each name individually. 3. Legal entity suffixes (LLC, Inc, PA, PC, etc.) can be ignored when matching. @@ -1160,12 +1237,26 @@ MATCHING RULES: 5. Acronyms that clearly represent the same entity count as matches (e.g., "MHS" matches "Memorial Hospital System"). 6. Common word variations count as matches (e.g., "St. Mark's" matches "Saint Mark's"). -CRITICAL - DO NOT MATCH: +[CRITICAL - DO NOT MATCH] - Parent company or holding company names that do NOT appear in PROVIDER NAME B, even if they own the listed entities. - Health system names that are NOT in PROVIDER NAME B, even if the listed hospitals belong to that system. - Corporate umbrella names unless they are explicitly listed in PROVIDER NAME B. -Briefly explain your reasoning. Then return ONLY 'Y' (match found) or 'N' (no match found). Enclose your final answer in |pipes|""" +[OUTPUT FORMAT] +Briefly explain your reasoning. Then return ONLY 'Y' (match found) or 'N' (no match found). Enclose your final answer in |pipes|.""" + + +def CHECK_PROVIDER_NAME_MATCH_PROMPT( + provider_name: str, hybrid_smart_chunking_provider_group_name: str +) -> str: + """Returns ONLY dynamic content for provider name matching. + Call CHECK_PROVIDER_NAME_MATCH_INSTRUCTION() separately for the cached instruction. + """ + return f"""[CONTEXT] +PROVIDER NAME A: {provider_name} +PROVIDER NAME B: {hybrid_smart_chunking_provider_group_name} + +Note: PROVIDER NAME B is typically a single name but may contain multiple names separated by ' | '. Determine if PROVIDER NAME A matches PROVIDER NAME B (or ANY name if multiple are present).""" def ONE_TO_ONE_MULTI_FIELD_TEMPLATE(context, questions: dict[str, str]): @@ -1223,24 +1314,25 @@ Briefly explain your reasoning, then put your final answer in JSON dictionary fo def EXHIBIT_HEADER_INSTRUCTION() -> str: - return ( - "[OBJECTIVE]\n" - "Extract exhibit/attachment header identifiers from contract page excerpts.\n" - "Return full header text in pipes, or N/A if no header found." - ) - - -def EXHIBIT_HEADER(context, EXHIBIT_HEADER_MARKERS): + """Static instruction for EXHIBIT_HEADER prompt caching. + Contains all inclusion/exclusion rules, header markers, and output format. + Header markers are loaded from Constants.EXHIBIT_HEADER_MARKERS. """ - Returns prompt for exhibit header extraction. - Call EXHIBIT_HEADER_INSTRUCTION() separately for the cached instruction. - """ - return f"""Analyze the following contract excerpt and extract the full header found. + # Load exhibit header markers from Constants + constants = _get_constants() + EXHIBIT_HEADER_MARKERS = constants.EXHIBIT_HEADER_MARKERS -Capture the complete hierarchy of document identifiers present in the text. The following keywords and phrases should be considered as exhibit/attachment headers: -* {'\n* '.join(EXHIBIT_HEADER_MARKERS)} + markers_text = "\n* ".join(EXHIBIT_HEADER_MARKERS) -Rules for Inclusion: + return f"""[OBJECTIVE] +Analyze contract excerpts and extract the full exhibit/attachment header found. +Capture the complete hierarchy of document identifiers present in the text. + +[HEADER MARKERS] +The following keywords and phrases should be considered as exhibit/attachment headers: +* {markers_text} + +[RULES FOR INCLUSION] * Include ALL text that appears to be part of the header * Headers MUST appear at the top of the page - do not mistakenly extract Footers, which will be found at the bottom of the page with other text before them. * Full header names and numbers/letters (e.g., "Attachment C: Commercial-Exchange") @@ -1250,55 +1342,58 @@ Rules for Inclusion: * Keep exact capitalization and formatting as shown in document * Include any information referring to the current exhibit's relationship with another exhibit (e.g. "Exhibit 1: Provider Roster (see Exhibit 2 for Compensation Schedule)") -- Rules for Exclusion: - * Do NOT abbreviate or summarize any part of the exhibit names - * Do NOT include page numbers - * Do NOT cherry-pick only certain parts - capture the complete hierarchy - * Do NOT include docusign IDs and other metadata - * DO NOT include unrelated text written in lowercase or title case. - -If there is no Exhibit Header present, return |N/A|. Do not fill in values from the examples above, these are only examples. - -Here is the text to analyze: -{context.replace('"', "'")} - -Before returning any output, ensure that all instructions for inclusion were followed. +[RULES FOR EXCLUSION] +* Do NOT abbreviate or summarize any part of the exhibit names +* Do NOT include page numbers +* Do NOT cherry-pick only certain parts - capture the complete hierarchy +* Do NOT include docusign IDs and other metadata +* DO NOT include unrelated text written in lowercase or title case. +[OUTPUT FORMAT] +If there is no Exhibit Header present, return |N/A|. Enclose your final answer in |pipes|, followed by a brief explanation. -""" +Before returning any output, ensure that all instructions for inclusion were followed.""" + + +def EXHIBIT_HEADER(context): + """Returns ONLY dynamic content for exhibit header extraction. + Call EXHIBIT_HEADER_INSTRUCTION() separately for the cached instruction. + Note: Header markers are now included in EXHIBIT_HEADER_INSTRUCTION() for caching. + """ + return f"""[CONTEXT] +Here is the text to analyze: +{context.replace('"', "'")}""" def EXHIBIT_LINKAGE_INSTRUCTION() -> str: - return ( - "[OBJECTIVE]\n" - "Determine if two exhibit headers are meaningfully different from each other.\n" - "Return Y if different, N if same or continuation. Answer in pipes." - ) - - -def EXHIBIT_LINKAGE(header1, header2): + """Static instruction for EXHIBIT_LINKAGE prompt caching. + Contains all decision rules for determining meaningful differences. """ - Returns prompt for exhibit linkage comparison. - Call EXHIBIT_LINKAGE_INSTRUCTION() separately for the cached instruction. - """ - return f""" -[INSTRUCTIONS] -* Determine whether the two Headers are meaningfully different from each other. -* The headers may refer to Attachment, Exhibit, Schedule, Addendum, or similar. If the highest-level number is identical, then the Headers ARE meaningfully different. For example, "Exhibit B-1" and "Exhibit B-2" are meaningfully different because they are both Exhibit B but have different sub-exhibits. + return """[OBJECTIVE] +Determine if two exhibit headers are meaningfully different from each other. + +[DECISION RULES] +* The headers may refer to Attachment, Exhibit, Schedule, Addendum, or similar. +* If the highest-level number is identical, then the Headers ARE meaningfully different. For example, "Exhibit B-1" and "Exhibit B-2" are meaningfully different because they are both Exhibit B but have different sub-exhibits. * If the headers have the same numbers, but different descriptors, they are NOT meaningfully different. For example, "Attachment A" and "Attachment A: Professional" are NOT meaningfully different because they are both attachment A. * If one header is a continuation of the other (e.g. "Continued", "CONT'D"), they are NOT meaningfully different. * If Header 1 directly references Header 2 in a way that links the two, then they are NOT meaningfully different. This linkage also applies if Header 2 references Header 1. -* Return |Y| if the two Headers are meaningfully different. Return |N| if they are not. -[CONTEXT] -Header 1: +[OUTPUT FORMAT] +Return |Y| if the two Headers are meaningfully different. Return |N| if they are not. +Briefly explain your answer, then enclose your final answer in |pipes|.""" + + +def EXHIBIT_LINKAGE(header1, header2): + """Returns ONLY dynamic content for exhibit linkage comparison. + Call EXHIBIT_LINKAGE_INSTRUCTION() separately for the cached instruction. + """ + return f"""[CONTEXT] +Header 1: "{header1}" Header 2: -"{header2}" - -Briefly explain your answer, then enclose your final answer in |pipes|. -""" +"{header2}" """ ##################################################################################### @@ -1306,19 +1401,14 @@ Briefly explain your answer, then enclose your final answer in |pipes|. ##################################################################################### -def VALIDATE_REIMBURSEMENTS_PROMPT(service_term: str, reimb_term: str) -> str: - """Validates whether a Reimbursement Term represents a complete reimbursement methodology. - Args: - reimb_term (str): The reimbursement term to validate. - service_term (str): The associated service term. - Returns: - str: A prompt for the AI to determine if the pair is a complete reimbursement methodology. +def VALIDATE_REIMBURSEMENTS_INSTRUCTION() -> str: + """Static instruction for VALIDATE_REIMBURSEMENTS prompt caching. + Contains all validation rules, criteria, and examples. """ - return f"""Analyze a REIMBURSEMENT TERM and SERVICE TERM pair from a healthcare contract: + return """[OBJECTIVE] +Analyze a REIMBURSEMENT TERM and SERVICE TERM pair from a healthcare contract. Determine if the pair represents a COMPLETE reimbursement methodology that specifies how payment will be calculated. -Determine if the pair of SERVICE TERM and REIMBURSEMENT TERM represent a COMPLETE reimbursement methodology that specifies how payment will be calculated. - -VALID entries - return YES: +[VALID ENTRIES - return YES] - Specific payment rates (percentages, dollar amounts) - Fee schedule references (including references to named/specific fee schedules without explicit rates) - Calculation formulas @@ -1327,9 +1417,10 @@ VALID entries - return YES: - Capitation rates (PMPM, PMPY, per member costs) - Flat rates per service or episode - These Special cases: OUTLIER, STOP_LOSS, SEQUESTRATION, DISCOUNT, PREMIUM, RATE_ESCALATOR, FACILITY_ADJUSTMENT + IMPORTANT: If a REIMBURSEMENT TERM contains a specific dollar amount or percentage rate, return YES regardless of the program type referenced in the SERVICE TERM. Specific payment amounts always constitute a complete reimbursement methodology. -INVALID entries - return NO unless there is VALID language as well: +[INVALID ENTRIES - return NO unless there is VALID language as well] - 'Empty' Reimbursements - have the same structure as a valid reimbursement but doesn't actually reference any specific rate or fee schedule (e.g. "Covered Services will be paid at the rates stated in the next section") - Service definitions without payment terms - Authorization/coverage requirements only @@ -1337,7 +1428,7 @@ INVALID entries - return NO unless there is VALID language as well: - Member cost responsibilities (including Medicare Member Cost Share) - Irrelevant information about parties, contract terms, or other non-reimbursement details e.g. 'transfer adjustments'. -HARD INVALID entries - return NO even if there is VALID reimbursement language present as well: +[HARD INVALID ENTRIES - return NO even if there is VALID reimbursement language present] - Any reference to Coordination of Benefits or general benefit descriptions - Any reference to Clean Claims in SERVICE TERM only - Audit and Time-based language describing how many claims must be paid in a certain timeframe (e.g. "... pay 85% of claims within a 30-day window") @@ -1346,7 +1437,7 @@ HARD INVALID entries - return NO even if there is VALID reimbursement language p - Non-reimbursement statements ("not covered", "excluded", "not payable") - Charge Description Master (CDM) or "chargemaster" language. -Examples: +[EXAMPLES] - "reimbursed at 100% of DMAP fee schedule" → YES (clear payment method) - "flat rate of $20.00 per frame" → YES (specific rate) - "$5.58 PMPM" → YES (capitation rate) @@ -1356,39 +1447,38 @@ Examples: - "Not payable per contract" → NO (no reimbursement) - "payment to IPA equal to fifty percent (50%) of the Surplus" → NO (risk-sharing distribution) -Here are the SERVICE TERM and REIMBURSEMENT TERM to analyze: -SERVICE TERM: {service_term} -REIMBURSEMENT TERM: {reimb_term} - +[OUTPUT FORMAT] Briefly explain your reasoning, then return YES or NO in |pipes|.""" -def VALIDATE_REIMBURSEMENTS_INSTRUCTION() -> str: - return ( - "[OBJECTIVE]\n" - "Validate whether a Reimbursement Term constitutes a complete reimbursement methodology." - ) +def VALIDATE_REIMBURSEMENTS_PROMPT(service_term: str, reimb_term: str) -> str: + """Returns ONLY dynamic content for validation. + Call VALIDATE_REIMBURSEMENTS_INSTRUCTION() separately for the cached instruction. + """ + return f"""[CONTEXT] +SERVICE TERM: {service_term} +REIMBURSEMENT TERM: {reimb_term}""" -def DATE_FIX_PROMPT(context: str) -> str: +def DATE_FIX_INSTRUCTION() -> str: + """Static instruction for DATE_FIX prompt caching. + Contains all conversion rules and examples. """ - Returns prompt for date formatting. - Call DATE_FIX_INSTRUCTION() separately for the cached instruction. - """ - return f"""Given a date string, convert it to YYYY/MM/DD format following these rules: + return f"""[OBJECTIVE] +Given a date string, convert it to YYYY/MM/DD format. Input: Text potentially containing a date Output: Date in YYYY/MM/DD format, or input if it's a duration, or "N/A" if ambiguous/incomplete -Rules: +[RULES] 1. Convert to YYYY/MM/DD (pad with leading zeros, use / separators) 2. Add 2000 to 2-digit years 0-49, 1900 to 50-99 3. Return unchanged if it's a duration (e.g., "3 years", "one year from effective date") -4. Return N/A if: missing/ambiguous month or year components, or invalid dates. If just the day is missing, assume it's the first of the month. +4. Return N/A if: missing/ambiguous month or year components, or invalid dates. If just the day is missing, assume it's the first of the month. 5. For ambiguous input cases, assume American format (MM/DD/YYYY) and apply YYYY/MM/DD formatting 6. Any day values with spaces or leading zeros (e.g., '0 1' or '01') are interpreted as single-digit days 7. Return final answer in |pipes| -Examples: +[EXAMPLES] Input: "7th day of February, 2002" Output: |2002/02/07| @@ -1402,7 +1492,7 @@ Input: "FEB 0 1 2020" Output: |2020/02/01| Input: "8.1.10" # (assumed MM.DD.YYYY) -Output: |2010/08/01| +Output: |2010/08/01| Input: "March 2014" # (missing day, assume 1st) Output: |2014/03/01| @@ -1410,58 +1500,75 @@ Output: |2014/03/01| Input: "Feb 26, 2_0_" # (incomplete year) Output: |N/A| +[OUTPUT FORMAT] +{PIPE_FORMAT_INSTRUCTIONS}""" + + +def DATE_FIX_PROMPT(context: str) -> str: + """Returns ONLY dynamic content for date fixing. + Call DATE_FIX_INSTRUCTION() separately for the cached instruction. + """ + return f"""[CONTEXT] Please analyze this date: - -## START CONTEXT ## -{context} -## END CONTEXT ## - -{PIPE_FORMAT_INSTRUCTIONS} -""" +{context}""" -def DATE_FIX_INSTRUCTION() -> str: - return ( - "[OBJECTIVE]\n" - "Convert date strings to YYYY/MM/DD format. Handle durations, ambiguous dates, and American format assumptions.\n" - "Return result in pipes." +def CARVEOUT_CHECK_INSTRUCTION() -> str: + """Static instruction for CARVEOUT_CHECK prompt caching. + Contains objective, case definitions (carveout + special cases), and instructions. + Carveout definitions are loaded from Constants.VALID_CARVEOUTS. + Special case definitions are loaded from investment_prompts.json. + """ + # Load carveout definitions from Constants + constants = _get_constants() + carveout_definitions = constants.VALID_CARVEOUTS + + # Load special case definitions from investment_prompts.json + special_case_fields = FieldSet( + file_path="src/prompts/investment_prompts.json", + relationship="one_to_n", + field_type="special_case_check", + ) + special_case_definitions = { + field.field_name: field.prompt for field in special_case_fields.fields + } + + carveout_text = "\n\n".join( + [f"{name} : {desc}" for name, desc in carveout_definitions.items()] + ) + special_case_text = "\n\n".join( + [f"{name} : {desc}" for name, desc in special_case_definitions.items()] ) - -def CARVEOUT_CHECK( - service_term: str, reimb_term: str, carveout_definitions, special_case_definitions -): - return f""" -[OBJECTIVE] + return f"""[OBJECTIVE] Examine the Reimbursement Term and identify which case it falls under. [CASE DEFINITIONS] Here are the cases, with their definitions: -Carveout cases: {"\n\n".join([name + " : " + description for name, description in carveout_definitions.items()])} -Special cases: {"\n\n".join([name + " : " + description for name, description in special_case_definitions.items()])} +Carveout cases: {carveout_text} -[REIMBURSEMENT TERM] -Here is the Reimbursement Term to analyze: -Service: {service_term} -Reimbursement Term: {reimb_term.replace('"', "'")} +Special cases: {special_case_text} [INSTRUCTIONS] - Determine which case description from the combined list of carveout cases and special cases corresponds to the specific reimbursement method. - The Service and Reimbursement will always match one of the listed cases. - If either the Service or Reimbursement is an explicit match with any of the cases, then that is the correct case regardless of other information (for example, if the Service says "Outlier", then choose "OUTLIER_TERM" because it is explicit). -- If multiple cases apply, choose the definition that BEST fits the Reimbursement Term. +- If multiple cases apply, choose the definition that BEST fits the Reimbursement Term. - Always return one value from the combined list of cases above; do not return 'N/A' or 'UNKNOWN'. [OUTPUT FORMAT] -Briefly explain your answer (including why it was one of carveout cases or special cases), then enclose your final answer in |pipes|. -""" +Briefly explain your answer (including why it was one of carveout cases or special cases), then enclose your final answer in |pipes|.""" -def CARVEOUT_CHECK_INSTRUCTION() -> str: - return ( - "[OBJECTIVE]\n" - "Determine if a carve-out applies for a service given a base reimbursement. Return result in pipes as instructed." - ) +def CARVEOUT_CHECK(service_term: str, reimb_term: str): + """Returns ONLY dynamic content for carveout check. + Call CARVEOUT_CHECK_INSTRUCTION() separately for the cached instruction. + Note: Case definitions are now included in CARVEOUT_CHECK_INSTRUCTION() for caching. + """ + return f"""[REIMBURSEMENT TERM] +Here is the Reimbursement Term to analyze: +Service: {service_term} +Reimbursement Term: {reimb_term.replace('"', "'")}""" def DUAL_LOB_CHECK(service_term): @@ -1491,20 +1598,12 @@ def EFFECTIVE_DATE_FIX_PROMPT() -> str: Return N/A only for date if NO valid effective date is found. give the answer in the format: {{\"AARETE_DERIVED_EFFECTIVE_DT\": \"YYYY/MM/DD\"}}. nothing else.""" -def SPLIT_REIMB_DATES(date_range: str) -> str: - """ - Splits a date range into start and end dates, returning them in YYYY/MM/DD format. - Handles both explicit dates and text-based duration language. +def SPLIT_REIMB_DATES_INSTRUCTION() -> str: + """Static instruction for SPLIT_REIMB_DATES prompt caching.""" + return """[OBJECTIVE] +Extract start and end dates from a date range string and return them in YYYY/MM/DD format. - Args: - date_range (str): The date range to split, e.g., "2023/01/01 - 2023/12/31" or "Initial Term - Renewal Term" - - Returns: - str: A JSON string with "start_date" and "end_date" keys - """ - return f"""Extract the start and end dates from the following date range context: {date_range} - -Based on the date range context: +[RULES] 1. If explicit dates are present (e.g., "2023/01/01 - 2023/12/31"): - Identify the effective date (start_date) and termination date (end_date) - Convert to YYYY/MM/DD format @@ -1528,95 +1627,129 @@ Based on the date range context: 6. If no extractable dates are found in the text: - Return "N/A" for both start_date and end_date +[OUTPUT FORMAT] Return the dates in the following JSON format: -{{ +{ "start_date": "YYYY/MM/DD", "end_date": "YYYY/MM/DD" -}} +} - Note: Use "N/A" for dates that cannot be determined or extracted from the input text.""" +Note: Use "N/A" for dates that cannot be determined or extracted from the input text.""" -def LOB_RELATIONSHIP(exhibit_text, dynamic_primary_values): - return f"""Analyze the exhibit from a Payer-Provider contract. +def SPLIT_REIMB_DATES(date_range: str) -> str: + """Call SPLIT_REIMB_DATES_INSTRUCTION() separately for the cached instruction.""" + return f"""[DATE RANGE TO EXTRACT] +{date_range}""" + + +def LOB_RELATIONSHIP_INSTRUCTION() -> str: + """Static instruction for LOB_RELATIONSHIP prompt caching. + Contains background, decision rules, and output format. + """ + return """[OBJECTIVE] +Analyze an exhibit from a Payer-Provider contract to determine if LOB and Program relationship is Inclusive or Exclusive. [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. +[DECISION RULES] - Inclusive Relationship: When the contract refers to an LOB *AND* the program(s) in the list. - Some examples of language indicating an inclusive relationship are: "Medicaid & CHIP", "Medicare and 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" + - Some examples of language indicating an exclusive 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. - For example, if an exhibit has a header that says "Medicaid & CHIP", but then there are different subsections within the Exhibit for "Medicaid" and "CHIP", this is an Exclusive relationship even though the "&" language is Inclusive. +[OUTPUT FORMAT - CRITICAL] +You MUST format your response as follows: +1. Briefly explain your reasoning +2. Then put your final answer in pipe delimiters: |Inclusive| or |Exclusive| +Example: "The contract shows... |Inclusive|" +Your response MUST include the pipe delimiters (|) around your final answer.""" + + +def LOB_RELATIONSHIP(exhibit_text, dynamic_primary_values): + """Returns ONLY dynamic content for LOB relationship analysis. + Call LOB_RELATIONSHIP_INSTRUCTION() separately for the cached instruction. + """ + return f"""[LOB AND PROGRAMS IDENTIFIED] +It was previously identified that the Exhibit contains the following LOB and Programs: +{dynamic_primary_values} + [CONTEXT] Here is the Exhibit to be analyzed: -{exhibit_text.replace('"', "'")} - -[OUTPUT FORMAT - REQUIRED] -You MUST format your response with pipe delimiters. Example format: -"Based on the analysis... |Inclusive|" or "The relationship is... |Exclusive|" -Your final answer MUST be enclosed in pipe delimiters: |Inclusive| or |Exclusive| AND you must make sure that the pipe delimiters are only used around the terms Inclusive or Exclusive. -""" +{exhibit_text.replace('"', "'")}""" -def LOB_RELATIONSHIP_INSTRUCTION() -> str: - return ( - "[OBJECTIVE]\n" - "Determine if LOB and Program relationship in the exhibit is Inclusive or Exclusive.\n\n" - "[OUTPUT FORMAT - CRITICAL]\n" - "You MUST format your response as follows:\n" - "1. Briefly explain your reasoning\n" - "2. Then put your final answer in pipe delimiters: |Inclusive| or |Exclusive|\n" - "Example: 'The contract shows... |Inclusive|'\n" - "Your response MUST include the pipe delimiters (|) around your final answer." - ) +def DERIVED_TERM_DATE_INSTRUCTION() -> str: + """Static instruction for DERIVED_TERM_DATE prompt caching. + Contains calculation rules and output format. + """ + return f"""[OBJECTIVE] +Determine the initial termination date of the contract from effective date and term duration, excluding any renewals or extensions. + +[CALCULATION RULES] +- If termination information comes in as a duration, derive the termination date exclusive of the final day. + - For example, if effective date is 2023/02/01 and termination information is 'two years', termination date would be 2025/01/31 and not 2025/02/01. + - If effective date is 2023/02/01 and termination information is 'year-to-year', termination date would be 2024/01/31. +- If the termination information comes in as a date, return that date. + - For example, if effective date is 2023/02/01 and termination information is 2024/07/01, return 2024/07/01. +- Only provide the first termination date, ignoring any automatic or optional renewals. + +[OUTPUT FORMAT] +Provide the date in YYYY/MM/DD format only. +{PIPE_FORMAT_INSTRUCTIONS}""" def DERIVED_TERM_DATE_PROMPT( effective_date: str = None, termination_information: str = None ) -> str: - """ - Generates a prompt to extract the termination date from a legal contract. + """Returns ONLY dynamic content for derived term date calculation. Call DERIVED_TERM_DATE_INSTRUCTION() separately for the cached instruction. - - Args: - effective_date (str, optional): The effective date of the contract. Defaults to None. - termination_information (str, optional): Information regarding the termination of the contract. Defaults to None. - - Returns: - str: A formatted string prompt requesting the termination date of the contract. """ - return f"The following are excerpts from a legal contract related to effective and termination dates. Determine the initial termination date of the contract, excluding any renewals or extensions.\nIf termination information comes in as a duration, derive the termination date exclusive of the final day. For example, if effective date is 2023/02/01 and termination information is 'two years', termination date would be 2025/01/31 and not 2025/02/01. If effective date is 2023/02/01 and termination information is 'year-to-year', termination date would be 2024/01/31.\nIf the termination information comes in as a date, return that date. For example, if effective date is 2023/02/01 and termination information is 2024/07/01, return 2024/07/01.\nEffective Date: {effective_date}\nTermination Information: {termination_information}\nOnly provide the first termination date, ignoring any automatic or optional renewals. Provide the date in YYYY/MM/DD format only. \n{PIPE_FORMAT_INSTRUCTIONS}" + return f"""[CONTEXT] +Effective Date: {effective_date} +Termination Information: {termination_information}""" -def DERIVED_TERM_DATE_INSTRUCTION() -> str: - return ( - "[OBJECTIVE]\n" - "Calculate contract termination date from effective date and term duration.\n" - "Return date in YYYY/MM/DD format in pipes." - ) +def SPECIAL_CASE_ASSIGNMENT_INSTRUCTION() -> str: + """Static instruction for SPECIAL_CASE_ASSIGNMENT prompt caching. + Contains objective, rules, and output format. + """ + return f"""[OBJECTIVE] +Analyze a section of a contract to determine which special case term value is most associated with a given reimbursement term. + +[TASK] +You will be given: +1. A section of contract text for context +2. A reimbursement term (service and reimbursement description) +3. A numbered list of special case term values + +Your job is to determine which special case term (by index number) is most closely associated with the reimbursement term based on context clues in the contract text. + +[RULES] +- Use context clues from the contract text to match the reimbursement term to the most appropriate special case term +- If multiple special case terms could apply, choose the one with the strongest contextual connection +- Return only the integer index of the matching term + +[OUTPUT FORMAT] +{PIPE_FORMAT_INSTRUCTIONS}""" def SPECIAL_CASE_ASSIGNMENT( exhibit_text, reimbursement_dict, special_case_dicts, special_case_term ): - """ - Returns prompt for special case assignment. + """Returns ONLY dynamic content for special case assignment. Call SPECIAL_CASE_ASSIGNMENT_INSTRUCTION() separately for the cached instruction. """ - return f"""[OBJECTIVE] -Analyze a section of a contract. Determine which of {special_case_term} values are associated with the REIMBURSEMENT TERM shown. - -[CONTEXT] + special_case_list = [ + f"{i} : {special_case_dicts[i][special_case_term]}" + for i in range(len(special_case_dicts)) + ] + return f"""[CONTEXT] Here is the section of the contract: {exhibit_text} @@ -1625,17 +1758,6 @@ Service: {reimbursement_dict["SERVICE_TERM"]} Reimbursement: {reimbursement_dict["REIMB_TERM"]} [{special_case_term} LIST] -{[f"{i} : {special_case_dicts[i][special_case_term]}" for i in range(len(special_case_dicts))]} +{special_case_list} -Return the integer of the {special_case_term} that is most associated with the REIMBURSEMENT TERM. Base your answer on context clues found in the text. - -{PIPE_FORMAT_INSTRUCTIONS} -""" - - -def SPECIAL_CASE_ASSIGNMENT_INSTRUCTION() -> str: - return ( - "[OBJECTIVE]\n" - "Determine which special case term value is associated with a reimbursement term based on contract context.\n" - "Return the integer index of the matching term in pipes." - ) +Return the integer of the {special_case_term} that is most associated with the REIMBURSEMENT TERM.""" diff --git a/src/scripts/qa_qc.py b/src/scripts/qa_qc.py index 8271734..cd64a3f 100644 --- a/src/scripts/qa_qc.py +++ b/src/scripts/qa_qc.py @@ -15,7 +15,6 @@ import config from qa_qc_helpers import perform_qc_qa, save_qcqa_wb from consolidate_output import consolidate_output - file_name_column = config.FILE_NAME_COLUMN """python scripts/qa_qc.py input_dir=______ output_dir=_______ ac_df=________ b_df=______ read_mode=____ df_read_mode=________ diff --git a/src/tests/test_prompt_caching.py b/src/tests/test_prompt_caching.py index 89b62cc..f28e060 100644 --- a/src/tests/test_prompt_caching.py +++ b/src/tests/test_prompt_caching.py @@ -20,10 +20,10 @@ class TestPromptTemplatesReturnStrings(unittest.TestCase): self.assertIn("test context", result) def test_methodology_breakout_returns_string(self): - """Test METHODOLOGY_BREAKOUT returns a prompt string.""" - result = prompt_templates.METHODOLOGY_BREAKOUT( - "service term", "reimb term", "questions" - ) + """Test METHODOLOGY_BREAKOUT returns a prompt string. + Note: Field definitions are now in METHODOLOGY_BREAKOUT_INSTRUCTION() for caching. + """ + result = prompt_templates.METHODOLOGY_BREAKOUT("service term", "reimb term") self.assertIsInstance(result, str) self.assertIn("service term", result) @@ -61,18 +61,20 @@ class TestPromptTemplatesReturnStrings(unittest.TestCase): self.assertIn("reimb term", result) def test_fee_schedule_breakout_returns_string(self): - """Test FEE_SCHEDULE_BREAKOUT returns a prompt string.""" - result = prompt_templates.FEE_SCHEDULE_BREAKOUT( - "methodology", "fee schedule", "questions" - ) + """Test FEE_SCHEDULE_BREAKOUT returns a prompt string. + Note: Field definitions are now in FEE_SCHEDULE_BREAKOUT_INSTRUCTION() for caching. + """ + result = prompt_templates.FEE_SCHEDULE_BREAKOUT("methodology", "fee schedule") self.assertIsInstance(result, str) self.assertIn("methodology", result) self.assertIn("fee schedule", result) def test_grouper_breakout_returns_string(self): - """Test GROUPER_BREAKOUT returns a prompt string.""" - result = prompt_templates.GROUPER_BREAKOUT("service", "term", "questions") + """Test GROUPER_BREAKOUT returns a prompt string. + Note: Field definitions are now in GROUPER_BREAKOUT_INSTRUCTION() for caching. + """ + result = prompt_templates.GROUPER_BREAKOUT("service", "term") self.assertIsInstance(result, str) self.assertIn("service", result) @@ -105,6 +107,35 @@ class TestPromptTemplatesReturnStrings(unittest.TestCase): self.assertIn("Payer Name", result) self.assertIn("contract context", result) + def test_carveout_check_returns_string(self): + """Test CARVEOUT_CHECK returns a prompt string. + Note: Case definitions are now in CARVEOUT_CHECK_INSTRUCTION() for caching. + """ + result = prompt_templates.CARVEOUT_CHECK("service term", "reimb term") + + self.assertIsInstance(result, str) + self.assertIn("service term", result) + self.assertIn("reimb term", result) + + def test_exhibit_header_returns_string(self): + """Test EXHIBIT_HEADER returns a prompt string. + Note: Header markers are now in EXHIBIT_HEADER_INSTRUCTION() for caching. + """ + result = prompt_templates.EXHIBIT_HEADER("page context text") + + self.assertIsInstance(result, str) + self.assertIn("page context text", result) + + def test_code_explicit_returns_string(self): + """Test CODE_EXPLICIT returns a prompt string. + Note: Field definitions are now in CODE_EXPLICIT_INSTRUCTION() for caching. + """ + result = prompt_templates.CODE_EXPLICIT("Lab Services", "Medicare Fee Schedule") + + self.assertIsInstance(result, str) + self.assertIn("Lab Services", result) + self.assertIn("Medicare Fee Schedule", result) + class TestInstructionFunctions(unittest.TestCase): """Test that _INSTRUCTION() functions return static instruction text.""" @@ -231,6 +262,116 @@ class TestInstructionFunctions(unittest.TestCase): self.assertIsInstance(result, str) self.assertIn("OBJECTIVE", result) + def test_carveout_check_instruction_returns_string(self): + """Test CARVEOUT_CHECK_INSTRUCTION returns a string with case definitions.""" + result = prompt_templates.CARVEOUT_CHECK_INSTRUCTION() + self.assertIsInstance(result, str) + self.assertIn("OBJECTIVE", result) + # Should contain static carveout definitions + self.assertIn("BASE_COVERED_SERVICES", result) + self.assertIn("PAYMENT_CARVEOUT", result) + # Should contain static special case definitions + self.assertIn("OUTLIER_TERM", result) + self.assertIn("STOP_LOSS_TERM", result) + + def test_code_explicit_instruction_returns_string(self): + """Test CODE_EXPLICIT_INSTRUCTION returns a string with field definitions. + Field definitions are loaded from investment_prompts.json (field_type=code_primary_breakout). + """ + result = prompt_templates.CODE_EXPLICIT_INSTRUCTION() + self.assertIsInstance(result, str) + self.assertIn("OBJECTIVE", result) + # Should contain field definitions from investment_prompts.json + self.assertIn("PROCEDURE_CD", result) + self.assertIn("REVENUE_CD", result) + self.assertIn("DIAG_CD", result) + + +class TestInstructionFieldContent(unittest.TestCase): + """Test that _INSTRUCTION() functions contain expected static field definitions.""" + + def test_methodology_breakout_instruction_contains_fields(self): + """Test METHODOLOGY_BREAKOUT_INSTRUCTION contains field definitions.""" + result = prompt_templates.METHODOLOGY_BREAKOUT_INSTRUCTION() + self.assertIsInstance(result, str) + # Should contain field definitions + self.assertIn("LESSER_OF_IND", result) + self.assertIn("AARETE_DERIVED_REIMB_METHOD", result) + self.assertIn("REIMB_PCT_RATE", result) + self.assertIn("REIMB_FEE_RATE", result) + # Should contain valid values + self.assertIn("Fee Schedule", result) + self.assertIn("Grouper", result) + self.assertIn("Per Diem", result) + + def test_fee_schedule_breakout_instruction_contains_fields(self): + """Test FEE_SCHEDULE_BREAKOUT_INSTRUCTION contains field definitions from investment_prompts.json.""" + result = prompt_templates.FEE_SCHEDULE_BREAKOUT_INSTRUCTION() + self.assertIsInstance(result, str) + # Should contain field definitions (from investment_prompts.json) + self.assertIn("AARETE_DERIVED_FEE_SCHEDULE", result) + self.assertIn("FEE_SCHEDULE_VERSION", result) + self.assertIn("AARETE_DERIVED_FEE_SCHEDULE_VERSION", result) + # Should contain valid values (from Constants) + self.assertIn("CMS", result) + self.assertIn("State", result) + + def test_grouper_breakout_instruction_contains_fields(self): + """Test GROUPER_BREAKOUT_INSTRUCTION contains field definitions from investment_prompts.json.""" + result = prompt_templates.GROUPER_BREAKOUT_INSTRUCTION() + self.assertIsInstance(result, str) + # Should contain field definitions (from investment_prompts.json) + self.assertIn("GROUPER_TYPE", result) + self.assertIn("GROUPER_VERSION", result) + self.assertIn("AARETE_DERIVED_GROUPER_VERSION", result) + # Should contain grouper-related terms from field prompts + self.assertIn("MS-DRG", result) + self.assertIn("APR-DRG", result) + + def test_outlier_breakout_instruction_contains_fields(self): + """Test OUTLIER_BREAKOUT_INSTRUCTION contains field definitions from investment_prompts.json.""" + result = prompt_templates.OUTLIER_BREAKOUT_INSTRUCTION() + self.assertIsInstance(result, str) + # Should contain field definitions (from investment_prompts.json) + self.assertIn("OUTLIER_FIRST_DOLLAR_IND", result) + self.assertIn("OUTLIER_FIXED_LOSS_THRESHOLD", result) + self.assertIn("OUTLIER_PCT_RATE", result) + + def test_exhibit_header_instruction_contains_markers(self): + """Test EXHIBIT_HEADER_INSTRUCTION contains static header markers.""" + result = prompt_templates.EXHIBIT_HEADER_INSTRUCTION() + self.assertIsInstance(result, str) + # Should contain header markers + self.assertIn("EXHIBIT", result) + self.assertIn("ATTACHMENT", result) + self.assertIn("SCHEDULE", result) + self.assertIn("ADDENDUM", result) + + def test_carveout_check_instruction_contains_all_definitions(self): + """Test CARVEOUT_CHECK_INSTRUCTION contains all carveout and special case definitions.""" + result = prompt_templates.CARVEOUT_CHECK_INSTRUCTION() + # Carveout definitions + carveout_types = [ + "BASE_COVERED_SERVICES", + "PAYMENT_CARVEOUT", + "TRIGGER_CAP", + "ADDITION", + "DEFAULT_TERM", + "UNLISTED_CODE", + ] + for carveout in carveout_types: + self.assertIn(carveout, result) + + # Special case definitions + special_cases = [ + "OUTLIER_TERM", + "STOP_LOSS_TERM", + "DISCOUNT_TERM", + "PREMIUM_TERM", + ] + for case in special_cases: + self.assertIn(case, result) + class TestGetCacheableInstructions(unittest.TestCase): """Test get_cacheable_instructions function returns expected instructions."""