From dba611469d8dd1d707bf7b7694ffb1dfd66728dd Mon Sep 17 00:00:00 2001 From: Katon Minhas Date: Mon, 2 Feb 2026 20:53:23 -0500 Subject: [PATCH] Update prompt_calls --- .../bcbs_promise/prompts/prompt_calls.py | 150 +++++++++--------- .../clients/clover/prompts/prompt_calls.py | 137 ++++++++-------- src/pipelines/saas/prompts/prompt_calls.py | 143 +++++++---------- .../shared/extraction/dynamic_funcs.py | 2 +- .../hybrid_smart_chunking_funcs.py | 4 +- src/prompts/prompt_templates.py | 26 +-- 6 files changed, 205 insertions(+), 257 deletions(-) diff --git a/src/pipelines/clients/bcbs_promise/prompts/prompt_calls.py b/src/pipelines/clients/bcbs_promise/prompts/prompt_calls.py index 7aca165..43d8d44 100644 --- a/src/pipelines/clients/bcbs_promise/prompts/prompt_calls.py +++ b/src/pipelines/clients/bcbs_promise/prompts/prompt_calls.py @@ -3,7 +3,6 @@ import logging import src.config as config import src.prompts.prompt_templates as prompt_templates from src.constants.constants import Constants -from src.constants.delimiters import Delimiter from src.prompts.fieldset import Field, FieldSet from src.utils import llm_utils, string_utils import json @@ -19,7 +18,7 @@ def prompt_exhibit_level( logging.debug(exhibit_level_fields.print_prompt_dict(constants)) if not exhibit_level_fields.contains_fields(): return {} - prompt = prompt_templates.EXHIBIT_LEVEL( + prompt, _parser = prompt_templates.EXHIBIT_LEVEL( exhibit_text, exhibit_level_fields.print_prompt_dict(constants) ) llm_answer_raw = llm_utils.invoke_claude( @@ -27,7 +26,7 @@ def prompt_exhibit_level( ) logging.debug(f"LLM raw output for {filename}: {llm_answer_raw}") - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) return llm_answer_final @@ -51,7 +50,7 @@ def prompt_exhibit_level_breakout( fields extracted from the FACILITY_ADJUSTMENT_TERM breakout, or the original dictionary if no FACILITY_ADJUSTMENT_TERM was present. Raises: - Any exceptions raised by llm_utils.invoke_claude() or string_utils.universal_json_load() + Any exceptions raised by llm_utils.invoke_claude() or _parser() will propagate to the caller. """ @@ -59,14 +58,14 @@ def prompt_exhibit_level_breakout( if not string_utils.is_empty( exhibit_level_answers.get("FACILITY_ADJUSTMENT_TERM", "") ): - prompt = prompt_templates.FACILITY_ADJUSTMENT_BREAKOUT( + prompt, _parser = prompt_templates.FACILITY_ADJUSTMENT_BREAKOUT( exhibit_level_answers["FACILITY_ADJUSTMENT_TERM"] ) llm_answer_raw = llm_utils.invoke_claude( prompt=prompt, model_id="sonnet_latest", filename=filename ) print("Facility Adjustment: ", llm_answer_raw) - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) exhibit_level_answers.update(llm_answer_final) return exhibit_level_answers @@ -75,7 +74,7 @@ def prompt_exhibit_level_breakout( def prompt_dynamic_primary( exhibit_text: str, field: Field, constants: Constants, filename: str, TEMPLATE ): - prompt = TEMPLATE(exhibit_text, field.field_name, field.get_prompt(constants)) + prompt, _parser = TEMPLATE(exhibit_text, field.field_name, field.get_prompt(constants)) logging.debug(f"Dynamic primary prompt for {filename}; {field}: {prompt}") llm_answer_raw = llm_utils.invoke_claude( prompt, @@ -85,13 +84,7 @@ def prompt_dynamic_primary( instruction=prompt_templates.DYNAMIC_PRIMARY_INSTRUCTION(), ) logging.debug(f"Claude answer for {filename}; {field}: {llm_answer_raw}") - llm_answer_final = string_utils.extract_text_from_delimiters( - llm_answer_raw, string_utils.Delimiter.PIPE - ) - if "," in llm_answer_final: - llm_answer_final = "|".join( - [item.strip() for item in llm_answer_final.split(",")] - ) + llm_answer_final = _parser(llm_answer_raw) return llm_answer_final @@ -100,7 +93,7 @@ def prompt_reimbursement_primary( filename: str, ) -> list[dict[str, str]]: - prompt = prompt_templates.REIMBURSEMENT_PRIMARY(page_text) + prompt, _parser = prompt_templates.REIMBURSEMENT_PRIMARY(page_text) logging.debug( f"""Running reimbursement primary prompt for {filename} with prompt: {prompt}""" @@ -121,7 +114,7 @@ def prompt_reimbursement_primary( return [] # Return empty list if no reimbursement terms found try: - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) except ValueError as e: logging.error(f"Error parsing LLM response: {e}") logging.error(f"Raw LLM output: {llm_answer_raw}") @@ -138,7 +131,7 @@ def prompt_methodology_breakout( Call METHODOLOGY_BREAKOUT prompt with cached instruction. Note: Field definitions are now included in METHODOLOGY_BREAKOUT_INSTRUCTION() for caching. """ - prompt = prompt_templates.METHODOLOGY_BREAKOUT( + prompt, _parser = prompt_templates.METHODOLOGY_BREAKOUT( service_term, reimb_term, ) @@ -153,7 +146,7 @@ def prompt_methodology_breakout( ) logging.debug(f"LLM Response for {filename}: {llm_response}") try: - methodology_breakout_answers = string_utils.universal_json_load(llm_response) + methodology_breakout_answers = _parser(llm_response) except ValueError as e: logging.error(f"Error parsing LLM response: {e}") methodology_breakout_answers = [] @@ -169,7 +162,7 @@ def prompt_fee_schedule_breakout( 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( + prompt, _parser = prompt_templates.FEE_SCHEDULE_BREAKOUT( reimbursement_method, methodology_breakout_dict.get("FEE_SCHEDULE"), ) @@ -184,7 +177,7 @@ def prompt_fee_schedule_breakout( ) logging.debug(f"LLM Response for {filename}: {llm_answer_raw}") try: - fs_breakout_dict = string_utils.universal_json_load(llm_answer_raw) + fs_breakout_dict = _parser(llm_answer_raw) except ValueError as e: fs_breakout_dict = {} return fs_breakout_dict @@ -199,7 +192,7 @@ def prompt_grouper_breakout( Call GROUPER_BREAKOUT prompt with cached instruction. Note: Field definitions are now included in GROUPER_BREAKOUT_INSTRUCTION() for caching. """ - prompt = prompt_templates.GROUPER_BREAKOUT( + prompt, _parser = prompt_templates.GROUPER_BREAKOUT( service, reimbursement_method, ) @@ -214,7 +207,7 @@ def prompt_grouper_breakout( ) logging.debug(f"LLM Response for {filename}: {llm_answer_raw}") try: - grouper_breakout_dict = string_utils.universal_json_load(llm_answer_raw) + grouper_breakout_dict = _parser(llm_answer_raw) except: grouper_breakout_dict = {} @@ -230,7 +223,7 @@ def prompt_carveout_check( 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) + carveout_prompt, _parser = prompt_templates.CARVEOUT_CHECK(service_term, reimb_term) logging.debug( f"Running carveout check for {filename} with prompt:\n{carveout_prompt}" ) @@ -243,22 +236,20 @@ def prompt_carveout_check( usage_label="CARVEOUT_CHECK", ) - carveout_answer = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE - ) + carveout_answer = _parser(llm_answer_raw) return carveout_answer def prompt_special_case_breakout(breakout_template, term_answer, filename): # Get the prompt from the template - prompt = breakout_template(term_answer) + prompt, _parser = breakout_template(term_answer) # Ensure prompt is a string for Bedrock messages API if not isinstance(prompt, str): logging.warning( f"Expected prompt string, got {type(prompt)} in prompt_special_case_breakout; coercing to str." ) - prompt = str(prompt) + prompt, _parser = str(prompt) # Look for a corresponding _INSTRUCTION() function for caching template_name = getattr(breakout_template, "__name__", "SPECIAL_CASE_BREAKOUT") @@ -278,7 +269,7 @@ def prompt_special_case_breakout(breakout_template, term_answer, filename): llm_answer_raw = llm_utils.invoke_claude( prompt, "sonnet_latest", filename, usage_label="SPECIAL_CASE_BREAKOUT" ) - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) return llm_answer_final @@ -293,7 +284,7 @@ def prompt_lob_relationship( f"LOB: {answer_dict.get("AARETE_DERIVED_LOB")}\nPROGRAM: {programs}" ) - prompt = prompt_templates.LOB_RELATIONSHIP(exhibit_text, dynamic_primary_values) + prompt, _parser = prompt_templates.LOB_RELATIONSHIP(exhibit_text, dynamic_primary_values) llm_answer_raw = llm_utils.invoke_claude( prompt, model_id="sonnet_latest", @@ -301,8 +292,8 @@ def prompt_lob_relationship( cache=True, instruction=prompt_templates.LOB_RELATIONSHIP_INSTRUCTION(), ) - llm_answer_final = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE + llm_answer_final = _parser( + llm_answer_raw ) return llm_answer_final @@ -329,20 +320,22 @@ def prompt_special_case_assignment( ] ) return answer_dict + + prompt, _parser = prompt_templates.SPECIAL_CASE_ASSIGNMENT( + exhibit_text, reimbursement_dict, special_case_dicts, special_case_term + ) # Otherwise use LLM llm_answer_raw = llm_utils.invoke_claude( - prompt_templates.SPECIAL_CASE_ASSIGNMENT( - exhibit_text, reimbursement_dict, special_case_dicts, special_case_term - ), + prompt, "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 + index_answer = _parser( + llm_answer_raw ) try: if not string_utils.is_empty(index_answer): @@ -362,7 +355,7 @@ def prompt_full_context( prompt_questions = full_context_fields.print_prompt_dict(constants) - full_context_prompt = prompt_templates.ONE_TO_ONE_MULTI_FIELD_TEMPLATE( + full_context_prompt, _parser = prompt_templates.ONE_TO_ONE_MULTI_FIELD_TEMPLATE( context=contract_text[ 0 : min( config.MAX_CONTEXT_LENGTH - len(prompt_questions), @@ -380,7 +373,7 @@ def prompt_full_context( cache=True, instruction=prompt_templates.ONE_TO_ONE_MULTI_FIELD_INSTRUCTION(), ) - full_context_answers_dict = string_utils.universal_json_load(claude_answer_raw) + full_context_answers_dict = _parser(claude_answer_raw) except Exception as e: logging.error(f"Error processing high accuracy fields: {str(e)}") full_context_answers_dict = {} @@ -412,7 +405,7 @@ def validate_reimbursements_for_llm(answer_dict: dict[str, str], filename: str) return False service_term, reimb_term = answer_dict["SERVICE_TERM"], answer_dict["REIMB_TERM"] - prompt = prompt_templates.VALIDATE_REIMBURSEMENTS_PROMPT(service_term, reimb_term) + prompt, _parser = prompt_templates.VALIDATE_REIMBURSEMENTS_PROMPT(service_term, reimb_term) logging.debug(f"Prompt for reimbursement validation in {filename}:\n{prompt}") llm_response = llm_utils.invoke_claude( @@ -425,11 +418,7 @@ def validate_reimbursements_for_llm(answer_dict: dict[str, str], filename: str) logging.debug( f"LLM response for reimbursement validation in {filename}:\n{llm_response}" ) - final_answer = ( - string_utils.extract_text_from_delimiters(llm_response, Delimiter.PIPE) - .strip() - .upper() - ) + final_answer = _parser(llm_response) return final_answer == "YES" @@ -445,7 +434,7 @@ def prompt_dynamic(text: str, field_prompts, filename): Returns: dict: A dictionary containing field names as keys and extracted answers as values. """ - prompt = prompt_templates.EXHIBIT_LEVEL(text, field_prompts) + prompt, _parser = prompt_templates.EXHIBIT_LEVEL(text, field_prompts) logging.debug(f"Dynamic prompt for {filename}: {prompt}") llm_answer_raw = llm_utils.invoke_claude( prompt, @@ -455,7 +444,7 @@ def prompt_dynamic(text: str, field_prompts, filename): instruction=prompt_templates.EXHIBIT_LEVEL_INSTRUCTION(), ) # Returns dictionary of lists logging.debug(f"Dynamic answer for {filename}: {llm_answer_raw}") - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) return llm_answer_final @@ -473,10 +462,10 @@ def prompt_exhibit_linkage(page_header, previous_header, filename): Returns: str: LLM response indicating if the exhibit has changed. """ - prompt = prompt_templates.EXHIBIT_LINKAGE(page_header, previous_header) + prompt, _parser = prompt_templates.EXHIBIT_LINKAGE(page_header, previous_header) claude_answer_raw = llm_utils.invoke_claude(prompt, "legacy_sonnet", filename) - exhibit_is_different = string_utils.extract_text_from_delimiters( - claude_answer_raw, Delimiter.PIPE + exhibit_is_different = _parser( + claude_answer_raw ) return exhibit_is_different @@ -495,7 +484,7 @@ def prompt_exhibit_header(page_content, filename): Returns: str: Extracted exhibit header identifier or marker indicating no header found. """ - prompt = prompt_templates.EXHIBIT_HEADER(page_content[0:400]) + prompt, _parser = prompt_templates.EXHIBIT_HEADER(page_content[0:400]) llm_answer_raw = llm_utils.invoke_claude( prompt, "sonnet_latest", @@ -505,8 +494,8 @@ def prompt_exhibit_header(page_content, filename): instruction=prompt_templates.EXHIBIT_HEADER_INSTRUCTION(), usage_label="EXHIBIT_HEADER", ) - llm_answer_extracted = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE + llm_answer_extracted = _parser( + llm_answer_raw ) return llm_answer_extracted @@ -522,7 +511,7 @@ def prompt_date_fix(date: str) -> str: """ if string_utils.is_empty(date): return "N/A" - prompt = prompt_templates.DATE_FIX_PROMPT(date) + prompt, _parser = prompt_templates.DATE_FIX_PROMPT(date) response = llm_utils.invoke_claude( prompt, "haiku_latest", @@ -531,7 +520,7 @@ def prompt_date_fix(date: str) -> str: instruction=prompt_templates.DATE_FIX_INSTRUCTION(), usage_label="DATE_FIX", ) - return string_utils.extract_text_from_delimiters(response, Delimiter.PIPE) + return _parser(response) def prompt_derived_term_date( @@ -549,11 +538,11 @@ def prompt_derived_term_date( Returns: str: ISO-formatted termination date (YYYY-MM-DD) or "N/A" if calculation fails. """ - prompt = prompt_templates.DERIVED_TERM_DATE_PROMPT( + prompt, _parser = prompt_templates.DERIVED_TERM_DATE_PROMPT( effective_date, termination_information ) response = llm_utils.invoke_claude(prompt, "haiku_latest", "derive_term_date") - return string_utils.extract_text_from_delimiters(response, Delimiter.PIPE) + return _parser(response) def prompt_dynamic_assignment( @@ -567,18 +556,18 @@ def prompt_dynamic_assignment( ): field_name = dynamic_field.field_name - field_prompt = dynamic_field.get_prompt(constants) + field_prompt, _parser = dynamic_field.get_prompt(constants) # Use specialized prompt for REIMB_DATES assignment if field_name == "REIMB_DATES": - prompt = prompt_templates.REIMB_DATES_ASSIGNMENT( + prompt, _parser = 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( + prompt, _parser = prompt_templates.DYNAMIC_ASSIGNMENT( service_term, reimb_term, field_name, @@ -599,7 +588,7 @@ def prompt_dynamic_assignment( ) try: - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) return llm_answer_final except: logging.error( @@ -652,7 +641,7 @@ def prompt_lesser_of_distribution( ... ) >>> # Returns: "Office Visit paid at the lesser of $150 or charges" """ - prompt = prompt_templates.LESSER_OF_DISTRIBUTION( + prompt, _parser = prompt_templates.LESSER_OF_DISTRIBUTION( service_term, reimb_term, page_num, @@ -676,8 +665,8 @@ def prompt_lesser_of_distribution( ) return reimb_term # Return original term unchanged else: - llm_answer_final = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE + llm_answer_final = _parser( + llm_answer_raw ) logging.debug( @@ -713,7 +702,7 @@ def prompt_lesser_of_check( f"LESSER_OF_CHECK input: service='{service_term[:50]}...', reimb_term='{reimb_term[:100]}...'" ) - prompt = prompt_templates.LESSER_OF_CHECK(service_term, reimb_term, exhibit_title) + prompt, _parser = prompt_templates.LESSER_OF_CHECK(service_term, reimb_term, exhibit_title) llm_answer_raw = llm_utils.invoke_claude( prompt, "sonnet_latest", @@ -725,8 +714,8 @@ def prompt_lesser_of_check( try: # Extract JSON from pipes - llm_answer_final = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE + llm_answer_final = _parser( + llm_answer_raw ) logging.debug(f"LESSER_OF_CHECK extracted from pipes: {llm_answer_final}") @@ -795,14 +784,14 @@ def prompt_lesser_of_check( def prompt_exhibit_title_match( current_exhibit_title: str, target_exhibit_reference: str, filename: str = "" ) -> str: - prompt = prompt_templates.EXHIBIT_TITLE_MATCH( + prompt, _parser = prompt_templates.EXHIBIT_TITLE_MATCH( current_exhibit_title, target_exhibit_reference ) llm_answer_raw = llm_utils.invoke_claude(prompt, "sonnet_latest", filename) try: - llm_answer_final = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE + llm_answer_final = _parser( + llm_answer_raw ) return llm_answer_final.strip().upper() # Return "YES" or "NO" as string except: @@ -822,33 +811,39 @@ def provider_name_match_check( return False # Create prompt for LLM - CHECK_PROVIDER_NAME_MATCH_PROMPT = prompt_templates.CHECK_PROVIDER_NAME_MATCH_PROMPT( + CHECK_PROVIDER_NAME_MATCH_PROMPT, _parser = prompt_templates.CHECK_PROVIDER_NAME_MATCH_PROMPT( provider_name=provider_name, hybrid_smart_chunking_provider_group_name=hybrid_smart_chunking_provider_group_name, ) try: # Adjust this based on your LLM client - response = llm_utils.invoke_claude( - CHECK_PROVIDER_NAME_MATCH_PROMPT, "sonnet_latest", filename + llm_answer_raw = llm_utils.invoke_claude( + CHECK_PROVIDER_NAME_MATCH_PROMPT, + "sonnet_latest", + filename, + cache=True, + instruction=prompt_templates.CHECK_PROVIDER_NAME_MATCH_INSTRUCTION(), + usage_label="CHECK_PROVIDER_NAME_MATCH", ) # Extract last character (should be Y or N) - response = string_utils.extract_text_from_delimiters(response, Delimiter.PIPE) + llm_answer_final = _parser(llm_answer_raw) + response_char = llm_answer_final[0] - if response == "Y": + if response_char == "Y": logging.debug( f"[is_provider_name_match_llm] ✓ LLM matched: '{provider_name}' ≈ '{hybrid_smart_chunking_provider_group_name}'" ) return True - elif response == "N": + elif response_char == "N": logging.debug( f"[is_provider_name_match_llm] ✗ LLM no match: '{provider_name}' ≠ '{hybrid_smart_chunking_provider_group_name}'" ) return False else: logging.warning( - f"[is_provider_name_match_llm] Unexpected response: {response}, defaulting to False" + f"[is_provider_name_match_llm] Unexpected response: {llm_answer_raw}, defaulting to False" ) return False @@ -857,3 +852,4 @@ def provider_name_match_check( f"[is_provider_name_match_llm] Error calling LLM: {e}, defaulting to False" ) return False + diff --git a/src/pipelines/clients/clover/prompts/prompt_calls.py b/src/pipelines/clients/clover/prompts/prompt_calls.py index 947b59f..8832169 100644 --- a/src/pipelines/clients/clover/prompts/prompt_calls.py +++ b/src/pipelines/clients/clover/prompts/prompt_calls.py @@ -19,7 +19,7 @@ def prompt_exhibit_level( logging.debug(exhibit_level_fields.print_prompt_dict(constants)) if not exhibit_level_fields.contains_fields(): return {} - prompt = prompt_templates.EXHIBIT_LEVEL( + prompt, _parser = prompt_templates.EXHIBIT_LEVEL( exhibit_text, exhibit_level_fields.print_prompt_dict(constants) ) llm_answer_raw = llm_utils.invoke_claude( @@ -33,7 +33,7 @@ def prompt_exhibit_level( ) logging.debug(f"LLM raw output for {filename}: {llm_answer_raw}") - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) return llm_answer_final @@ -57,7 +57,7 @@ def prompt_exhibit_level_breakout( fields extracted from the FACILITY_ADJUSTMENT_TERM breakout, or the original dictionary if no FACILITY_ADJUSTMENT_TERM was present. Raises: - Any exceptions raised by llm_utils.invoke_claude() or string_utils.universal_json_load() + Any exceptions raised by llm_utils.invoke_claude() or _parser() will propagate to the caller. """ @@ -65,14 +65,14 @@ def prompt_exhibit_level_breakout( if not string_utils.is_empty( exhibit_level_answers.get("FACILITY_ADJUSTMENT_TERM", "") ): - prompt = prompt_templates.FACILITY_ADJUSTMENT_BREAKOUT( + prompt, _parser = prompt_templates.FACILITY_ADJUSTMENT_BREAKOUT( exhibit_level_answers["FACILITY_ADJUSTMENT_TERM"] ) llm_answer_raw = llm_utils.invoke_claude( prompt=prompt, model_id="sonnet_latest", filename=filename ) print("Facility Adjustment: ", llm_answer_raw) - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) exhibit_level_answers.update(llm_answer_final) return exhibit_level_answers @@ -81,7 +81,7 @@ def prompt_exhibit_level_breakout( def prompt_dynamic_primary( exhibit_text: str, field: Field, constants: Constants, filename: str, TEMPLATE ): - prompt = TEMPLATE(exhibit_text, field.field_name, field.get_prompt(constants)) + prompt, _parser = TEMPLATE(exhibit_text, field.field_name, field.get_prompt(constants)) logging.debug(f"Dynamic primary prompt for {filename}; {field}: {prompt}") llm_answer_raw = llm_utils.invoke_claude( prompt, @@ -91,13 +91,9 @@ def prompt_dynamic_primary( instruction=prompt_templates.DYNAMIC_PRIMARY_INSTRUCTION(), ) logging.debug(f"Claude answer for {filename}; {field}: {llm_answer_raw}") - llm_answer_final = string_utils.extract_text_from_delimiters( - llm_answer_raw, string_utils.Delimiter.PIPE + llm_answer_final = _parser( + llm_answer_raw ) - if "," in llm_answer_final: - llm_answer_final = "|".join( - [item.strip() for item in llm_answer_final.split(",")] - ) return llm_answer_final @@ -106,7 +102,7 @@ def prompt_reimbursement_primary( filename: str, ) -> list[dict[str, str]]: - prompt = prompt_templates.REIMBURSEMENT_PRIMARY(page_text) + prompt, _parser = prompt_templates.REIMBURSEMENT_PRIMARY(page_text) logging.debug( f"""Running reimbursement primary prompt for {filename} with prompt: {prompt}""" @@ -127,7 +123,7 @@ def prompt_reimbursement_primary( return [] # Return empty list if no reimbursement terms found try: - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) except ValueError as e: logging.error(f"Error parsing LLM response: {e}") logging.error(f"Raw LLM output: {llm_answer_raw}") @@ -144,7 +140,7 @@ def prompt_methodology_breakout( Call METHODOLOGY_BREAKOUT prompt with cached instruction. Note: Field definitions are now included in METHODOLOGY_BREAKOUT_INSTRUCTION() for caching. """ - prompt = prompt_templates.METHODOLOGY_BREAKOUT( + prompt, _parser = prompt_templates.METHODOLOGY_BREAKOUT( service_term, reimb_term, ) @@ -159,7 +155,7 @@ def prompt_methodology_breakout( ) logging.debug(f"LLM Response for {filename}: {llm_response}") try: - methodology_breakout_answers = string_utils.universal_json_load(llm_response) + methodology_breakout_answers = _parser(llm_response) except ValueError as e: logging.error(f"Error parsing LLM response: {e}") methodology_breakout_answers = [] @@ -175,7 +171,7 @@ def prompt_fee_schedule_breakout( 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( + prompt, _parser = prompt_templates.FEE_SCHEDULE_BREAKOUT( reimbursement_method, methodology_breakout_dict.get("FEE_SCHEDULE"), ) @@ -190,7 +186,7 @@ def prompt_fee_schedule_breakout( ) logging.debug(f"LLM Response for {filename}: {llm_answer_raw}") try: - fs_breakout_dict = string_utils.universal_json_load(llm_answer_raw) + fs_breakout_dict = _parser(llm_answer_raw) except ValueError as e: fs_breakout_dict = {} return fs_breakout_dict @@ -205,7 +201,7 @@ def prompt_grouper_breakout( Call GROUPER_BREAKOUT prompt with cached instruction. Note: Field definitions are now included in GROUPER_BREAKOUT_INSTRUCTION() for caching. """ - prompt = prompt_templates.GROUPER_BREAKOUT( + prompt, _parser = prompt_templates.GROUPER_BREAKOUT( service, reimbursement_method, ) @@ -220,7 +216,7 @@ def prompt_grouper_breakout( ) logging.debug(f"LLM Response for {filename}: {llm_answer_raw}") try: - grouper_breakout_dict = string_utils.universal_json_load(llm_answer_raw) + grouper_breakout_dict = _parser(llm_answer_raw) except: grouper_breakout_dict = {} @@ -236,7 +232,7 @@ def prompt_carveout_check( 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) + carveout_prompt, _parser = prompt_templates.CARVEOUT_CHECK(service_term, reimb_term) logging.debug( f"Running carveout check for {filename} with prompt:\n{carveout_prompt}" ) @@ -249,22 +245,22 @@ def prompt_carveout_check( usage_label="CARVEOUT_CHECK", ) - carveout_answer = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE + carveout_answer = _parser( + llm_answer_raw ) return carveout_answer def prompt_special_case_breakout(breakout_template, term_answer, filename): # Get the prompt from the template - prompt = breakout_template(term_answer) + prompt, _parser = breakout_template(term_answer) # Ensure prompt is a string for Bedrock messages API if not isinstance(prompt, str): logging.warning( f"Expected prompt string, got {type(prompt)} in prompt_special_case_breakout; coercing to str." ) - prompt = str(prompt) + prompt, _parser = str(prompt) # Look for a corresponding _INSTRUCTION() function for caching template_name = getattr(breakout_template, "__name__", "SPECIAL_CASE_BREAKOUT") @@ -284,7 +280,7 @@ def prompt_special_case_breakout(breakout_template, term_answer, filename): llm_answer_raw = llm_utils.invoke_claude( prompt, "sonnet_latest", filename, usage_label="SPECIAL_CASE_BREAKOUT" ) - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) return llm_answer_final @@ -299,7 +295,7 @@ def prompt_lob_relationship( f"LOB: {answer_dict.get("AARETE_DERIVED_LOB")}\nPROGRAM: {programs}" ) - prompt = prompt_templates.LOB_RELATIONSHIP(exhibit_text, dynamic_primary_values) + prompt, _parser = prompt_templates.LOB_RELATIONSHIP(exhibit_text, dynamic_primary_values) llm_answer_raw = llm_utils.invoke_claude( prompt, model_id="sonnet_latest", @@ -307,8 +303,8 @@ def prompt_lob_relationship( cache=True, instruction=prompt_templates.LOB_RELATIONSHIP_INSTRUCTION(), ) - llm_answer_final = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE + llm_answer_final = _parser( + llm_answer_raw ) return llm_answer_final @@ -335,20 +331,22 @@ def prompt_special_case_assignment( ] ) return answer_dict + + prompt, _parser = prompt_templates.SPECIAL_CASE_ASSIGNMENT( + exhibit_text, reimbursement_dict, special_case_dicts, special_case_term + ) # Otherwise use LLM llm_answer_raw = llm_utils.invoke_claude( - prompt_templates.SPECIAL_CASE_ASSIGNMENT( - exhibit_text, reimbursement_dict, special_case_dicts, special_case_term - ), + prompt, "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 + index_answer = _parser( + llm_answer_raw ) try: if not string_utils.is_empty(index_answer): @@ -384,7 +382,7 @@ def prompt_full_context( prompt_questions = full_context_fields.print_prompt_dict(constants) - full_context_prompt = prompt_templates.ONE_TO_ONE_MULTI_FIELD_TEMPLATE( + full_context_prompt, _parser = prompt_templates.ONE_TO_ONE_MULTI_FIELD_TEMPLATE( context=contract_text[ 0 : min( config.MAX_CONTEXT_LENGTH - len(prompt_questions), @@ -402,7 +400,7 @@ def prompt_full_context( cache=True, instruction=prompt_templates.ONE_TO_ONE_MULTI_FIELD_INSTRUCTION(), ) - full_context_answers_dict = string_utils.universal_json_load(claude_answer_raw) + full_context_answers_dict = _parser(claude_answer_raw) if not isinstance(full_context_answers_dict, dict): full_context_answers_dict = extract_and_parse(claude_answer_raw) @@ -437,7 +435,7 @@ def validate_reimbursements_for_llm(answer_dict: dict[str, str], filename: str) return False service_term, reimb_term = answer_dict["SERVICE_TERM"], answer_dict["REIMB_TERM"] - prompt = prompt_templates.VALIDATE_REIMBURSEMENTS_PROMPT(service_term, reimb_term) + prompt, _parser = prompt_templates.VALIDATE_REIMBURSEMENTS_PROMPT(service_term, reimb_term) logging.debug(f"Prompt for reimbursement validation in {filename}:\n{prompt}") llm_response = llm_utils.invoke_claude( @@ -451,7 +449,7 @@ def validate_reimbursements_for_llm(answer_dict: dict[str, str], filename: str) f"LLM response for reimbursement validation in {filename}:\n{llm_response}" ) final_answer = ( - string_utils.extract_text_from_delimiters(llm_response, Delimiter.PIPE) + _parser(llm_response) .strip() .upper() ) @@ -470,7 +468,7 @@ def prompt_dynamic(text: str, field_prompts, filename): Returns: dict: A dictionary containing field names as keys and extracted answers as values. """ - prompt = prompt_templates.EXHIBIT_LEVEL(text, field_prompts) + prompt, _parser = prompt_templates.EXHIBIT_LEVEL(text, field_prompts) logging.debug(f"Dynamic prompt for {filename}: {prompt}") llm_answer_raw = llm_utils.invoke_claude( prompt, @@ -480,7 +478,7 @@ def prompt_dynamic(text: str, field_prompts, filename): instruction=prompt_templates.EXHIBIT_LEVEL_INSTRUCTION(), ) # Returns dictionary of lists logging.debug(f"Dynamic answer for {filename}: {llm_answer_raw}") - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) return llm_answer_final @@ -498,7 +496,7 @@ def prompt_exhibit_linkage(page_header, previous_header, filename): Returns: str: LLM response indicating if the exhibit has changed. """ - prompt = prompt_templates.EXHIBIT_LINKAGE(page_header, previous_header) + prompt, _parser = prompt_templates.EXHIBIT_LINKAGE(page_header, previous_header) claude_answer_raw = llm_utils.invoke_claude( prompt, "legacy_sonnet", @@ -507,8 +505,8 @@ def prompt_exhibit_linkage(page_header, previous_header, filename): instruction=prompt_templates.EXHIBIT_LINKAGE_INSTRUCTION(), usage_label="EXHIBIT_LINKAGE", ) - exhibit_is_different = string_utils.extract_text_from_delimiters( - claude_answer_raw, Delimiter.PIPE + exhibit_is_different = _parser( + claude_answer_raw ) return exhibit_is_different @@ -527,7 +525,7 @@ def prompt_exhibit_header(page_content, filename): Returns: str: Extracted exhibit header identifier or marker indicating no header found. """ - prompt = prompt_templates.EXHIBIT_HEADER(page_content[0:400]) + prompt, _parser = prompt_templates.EXHIBIT_HEADER(page_content[0:400]) llm_answer_raw = llm_utils.invoke_claude( prompt, "sonnet_latest", @@ -537,8 +535,8 @@ def prompt_exhibit_header(page_content, filename): instruction=prompt_templates.EXHIBIT_HEADER_INSTRUCTION(), usage_label="EXHIBIT_HEADER", ) - llm_answer_extracted = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE + llm_answer_extracted = _parser( + llm_answer_raw ) return llm_answer_extracted @@ -554,7 +552,7 @@ def prompt_date_fix(date: str) -> str: """ if string_utils.is_empty(date): return "N/A" - prompt = prompt_templates.DATE_FIX_PROMPT(date) + prompt, _parser = prompt_templates.DATE_FIX_PROMPT(date) response = llm_utils.invoke_claude( prompt, "haiku_latest", @@ -563,7 +561,7 @@ def prompt_date_fix(date: str) -> str: instruction=prompt_templates.DATE_FIX_INSTRUCTION(), usage_label="DATE_FIX", ) - return string_utils.extract_text_from_delimiters(response, Delimiter.PIPE) + return _parser(response) def prompt_derived_term_date( @@ -581,7 +579,7 @@ def prompt_derived_term_date( Returns: str: ISO-formatted termination date (YYYY-MM-DD) or "N/A" if calculation fails. """ - prompt = prompt_templates.DERIVED_TERM_DATE_PROMPT( + prompt, _parser = prompt_templates.DERIVED_TERM_DATE_PROMPT( effective_date, termination_information ) response = llm_utils.invoke_claude( @@ -592,7 +590,7 @@ def prompt_derived_term_date( instruction=prompt_templates.DERIVED_TERM_DATE_INSTRUCTION(), usage_label="DERIVED_TERM_DATE", ) - return string_utils.extract_text_from_delimiters(response, Delimiter.PIPE) + return _parser(response) def prompt_dynamic_assignment( @@ -606,18 +604,18 @@ def prompt_dynamic_assignment( ): field_name = dynamic_field.field_name - field_prompt = dynamic_field.get_prompt(constants) + field_prompt, _parser = dynamic_field.get_prompt(constants) # Use specialized prompt for REIMB_DATES assignment if field_name == "REIMB_DATES": - prompt = prompt_templates.REIMB_DATES_ASSIGNMENT( + prompt, _parser = 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( + prompt, _parser = prompt_templates.DYNAMIC_ASSIGNMENT( service_term, reimb_term, field_name, @@ -638,7 +636,7 @@ def prompt_dynamic_assignment( ) try: - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) return llm_answer_final except: logging.error( @@ -691,7 +689,7 @@ def prompt_lesser_of_distribution( ... ) >>> # Returns: "Office Visit paid at the lesser of $150 or charges" """ - prompt = prompt_templates.LESSER_OF_DISTRIBUTION( + prompt, _parser = prompt_templates.LESSER_OF_DISTRIBUTION( service_term, reimb_term, page_num, @@ -715,8 +713,8 @@ def prompt_lesser_of_distribution( ) return reimb_term # Return original term unchanged else: - llm_answer_final = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE + llm_answer_final = _parser( + llm_answer_raw ) logging.debug( @@ -752,7 +750,7 @@ def prompt_lesser_of_check( f"LESSER_OF_CHECK input: service='{service_term[:50]}...', reimb_term='{reimb_term[:100]}...'" ) - prompt = prompt_templates.LESSER_OF_CHECK(service_term, reimb_term, exhibit_title) + prompt, _parser = prompt_templates.LESSER_OF_CHECK(service_term, reimb_term, exhibit_title) llm_answer_raw = llm_utils.invoke_claude( prompt, "sonnet_latest", @@ -764,8 +762,8 @@ def prompt_lesser_of_check( try: # Extract JSON from pipes - llm_answer_final = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE + llm_answer_final = _parser( + llm_answer_raw ) logging.debug(f"LESSER_OF_CHECK extracted from pipes: {llm_answer_final}") @@ -834,7 +832,7 @@ def prompt_lesser_of_check( def prompt_exhibit_title_match( current_exhibit_title: str, target_exhibit_reference: str, filename: str = "" ) -> str: - prompt = prompt_templates.EXHIBIT_TITLE_MATCH( + prompt, _parser = prompt_templates.EXHIBIT_TITLE_MATCH( current_exhibit_title, target_exhibit_reference ) llm_answer_raw = llm_utils.invoke_claude( @@ -847,8 +845,8 @@ def prompt_exhibit_title_match( ) try: - llm_answer_final = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE + llm_answer_final = _parser( + llm_answer_raw ) return llm_answer_final.strip().upper() # Return "YES" or "NO" as string except: @@ -868,14 +866,14 @@ def provider_name_match_check( return False # Create prompt for LLM - CHECK_PROVIDER_NAME_MATCH_PROMPT = prompt_templates.CHECK_PROVIDER_NAME_MATCH_PROMPT( + CHECK_PROVIDER_NAME_MATCH_PROMPT, _parser = prompt_templates.CHECK_PROVIDER_NAME_MATCH_PROMPT( provider_name=provider_name, hybrid_smart_chunking_provider_group_name=hybrid_smart_chunking_provider_group_name, ) try: # Adjust this based on your LLM client - response = llm_utils.invoke_claude( + llm_answer_raw = llm_utils.invoke_claude( CHECK_PROVIDER_NAME_MATCH_PROMPT, "sonnet_latest", filename, @@ -885,21 +883,22 @@ def provider_name_match_check( ) # Extract last character (should be Y or N) - response = string_utils.extract_text_from_delimiters(response, Delimiter.PIPE) + llm_answer_final = _parser(llm_answer_raw) + response_char = llm_answer_final[0] - if response == "Y": + if response_char == "Y": logging.debug( f"[is_provider_name_match_llm] ✓ LLM matched: '{provider_name}' ≈ '{hybrid_smart_chunking_provider_group_name}'" ) return True - elif response == "N": + elif response_char == "N": logging.debug( f"[is_provider_name_match_llm] ✗ LLM no match: '{provider_name}' ≠ '{hybrid_smart_chunking_provider_group_name}'" ) return False else: logging.warning( - f"[is_provider_name_match_llm] Unexpected response: {response}, defaulting to False" + f"[is_provider_name_match_llm] Unexpected response: {llm_answer_raw}, defaulting to False" ) return False diff --git a/src/pipelines/saas/prompts/prompt_calls.py b/src/pipelines/saas/prompts/prompt_calls.py index 2684b92..6724a97 100644 --- a/src/pipelines/saas/prompts/prompt_calls.py +++ b/src/pipelines/saas/prompts/prompt_calls.py @@ -3,7 +3,6 @@ import logging import src.config as config import src.prompts.prompt_templates as prompt_templates from src.constants.constants import Constants -from src.constants.delimiters import Delimiter from src.prompts.fieldset import Field, FieldSet from src.utils import llm_utils, string_utils import json @@ -19,7 +18,7 @@ def prompt_exhibit_level( logging.debug(exhibit_level_fields.print_prompt_dict(constants)) if not exhibit_level_fields.contains_fields(): return {} - prompt = prompt_templates.EXHIBIT_LEVEL( + prompt, _parser = prompt_templates.EXHIBIT_LEVEL( exhibit_text, exhibit_level_fields.print_prompt_dict(constants) ) llm_answer_raw = llm_utils.invoke_claude( @@ -33,7 +32,7 @@ def prompt_exhibit_level( ) logging.debug(f"LLM raw output for {filename}: {llm_answer_raw}") - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) return llm_answer_final @@ -65,13 +64,13 @@ def prompt_exhibit_level_breakout( if not string_utils.is_empty( exhibit_level_answers.get("FACILITY_ADJUSTMENT_TERM", "") ): - prompt = prompt_templates.FACILITY_ADJUSTMENT_BREAKOUT( + prompt, _parser = prompt_templates.FACILITY_ADJUSTMENT_BREAKOUT( exhibit_level_answers["FACILITY_ADJUSTMENT_TERM"] ) llm_answer_raw = llm_utils.invoke_claude( prompt=prompt, model_id="sonnet_latest", filename=filename ) - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) exhibit_level_answers.update(llm_answer_final) return exhibit_level_answers @@ -80,7 +79,7 @@ def prompt_exhibit_level_breakout( def prompt_dynamic_primary( exhibit_text: str, field: Field, constants: Constants, filename: str, TEMPLATE ): - prompt = TEMPLATE(exhibit_text, field.field_name, field.get_prompt(constants)) + prompt, _parser = TEMPLATE(exhibit_text, field.field_name, field.get_prompt(constants)) logging.debug(f"Dynamic primary prompt for {filename}; {field}: {prompt}") llm_answer_raw = llm_utils.invoke_claude( prompt, @@ -90,13 +89,8 @@ def prompt_dynamic_primary( instruction=prompt_templates.DYNAMIC_PRIMARY_INSTRUCTION(), ) logging.debug(f"Claude answer for {filename}; {field}: {llm_answer_raw}") - llm_answer_final = string_utils.extract_text_from_delimiters( - llm_answer_raw, string_utils.Delimiter.PIPE - ) - if "," in llm_answer_final: - llm_answer_final = "|".join( - [item.strip() for item in llm_answer_final.split(",")] - ) + llm_answer_final = _parser(llm_answer_raw) + return llm_answer_final @@ -105,7 +99,7 @@ def prompt_reimbursement_primary( filename: str, ) -> list[dict[str, str]]: - prompt = prompt_templates.REIMBURSEMENT_PRIMARY(page_text) + prompt, _parser = prompt_templates.REIMBURSEMENT_PRIMARY(page_text) logging.debug( f"""Running reimbursement primary prompt for {filename} with prompt: {prompt}""" @@ -126,7 +120,7 @@ def prompt_reimbursement_primary( return [] # Return empty list if no reimbursement terms found try: - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) except ValueError as e: logging.error(f"Error parsing LLM response: {e}") logging.error(f"Raw LLM output: {llm_answer_raw}") @@ -143,7 +137,7 @@ def prompt_methodology_breakout( Call METHODOLOGY_BREAKOUT prompt with cached instruction. Note: Field definitions are now included in METHODOLOGY_BREAKOUT_INSTRUCTION() for caching. """ - prompt = prompt_templates.METHODOLOGY_BREAKOUT( + prompt, _parser = prompt_templates.METHODOLOGY_BREAKOUT( service_term, reimb_term, ) @@ -158,7 +152,7 @@ def prompt_methodology_breakout( ) logging.debug(f"LLM Response for {filename}: {llm_response}") try: - methodology_breakout_answers = string_utils.universal_json_load(llm_response) + methodology_breakout_answers = _parser(llm_response) except ValueError as e: logging.error(f"Error parsing LLM response: {e}") methodology_breakout_answers = [] @@ -174,7 +168,7 @@ def prompt_fee_schedule_breakout( 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( + prompt, _parser = prompt_templates.FEE_SCHEDULE_BREAKOUT( reimbursement_method, methodology_breakout_dict.get("FEE_SCHEDULE"), ) @@ -189,7 +183,7 @@ def prompt_fee_schedule_breakout( ) logging.debug(f"LLM Response for {filename}: {llm_answer_raw}") try: - fs_breakout_dict = string_utils.universal_json_load(llm_answer_raw) + fs_breakout_dict = _parser(llm_answer_raw) except ValueError as e: fs_breakout_dict = {} return fs_breakout_dict @@ -204,7 +198,7 @@ def prompt_grouper_breakout( Call GROUPER_BREAKOUT prompt with cached instruction. Note: Field definitions are now included in GROUPER_BREAKOUT_INSTRUCTION() for caching. """ - prompt = prompt_templates.GROUPER_BREAKOUT( + prompt, _parser = prompt_templates.GROUPER_BREAKOUT( service, reimbursement_method, ) @@ -219,7 +213,7 @@ def prompt_grouper_breakout( ) logging.debug(f"LLM Response for {filename}: {llm_answer_raw}") try: - grouper_breakout_dict = string_utils.universal_json_load(llm_answer_raw) + grouper_breakout_dict = _parser(llm_answer_raw) except: grouper_breakout_dict = {} @@ -235,7 +229,7 @@ def prompt_carveout_check( 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) + carveout_prompt, _parser = prompt_templates.CARVEOUT_CHECK(service_term, reimb_term) logging.debug( f"Running carveout check for {filename} with prompt:\n{carveout_prompt}" ) @@ -248,15 +242,13 @@ def prompt_carveout_check( usage_label="CARVEOUT_CHECK", ) - carveout_answer = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE - ) + carveout_answer = _parser(llm_answer_raw) return carveout_answer def prompt_special_case_breakout(breakout_template, term_answer, filename): # Get the prompt from the template - prompt = breakout_template(term_answer) + prompt, _parser = breakout_template(term_answer) # Ensure prompt is a string for Bedrock messages API if not isinstance(prompt, str): @@ -283,7 +275,7 @@ def prompt_special_case_breakout(breakout_template, term_answer, filename): llm_answer_raw = llm_utils.invoke_claude( prompt, "sonnet_latest", filename, usage_label="SPECIAL_CASE_BREAKOUT" ) - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) return llm_answer_final @@ -298,7 +290,7 @@ def prompt_lob_relationship( f"LOB: {answer_dict.get("AARETE_DERIVED_LOB")}\nPROGRAM: {programs}" ) - prompt = prompt_templates.LOB_RELATIONSHIP(exhibit_text, dynamic_primary_values) + prompt, _parser = prompt_templates.LOB_RELATIONSHIP(exhibit_text, dynamic_primary_values) llm_answer_raw = llm_utils.invoke_claude( prompt, model_id="sonnet_latest", @@ -306,9 +298,7 @@ def prompt_lob_relationship( cache=True, instruction=prompt_templates.LOB_RELATIONSHIP_INSTRUCTION(), ) - llm_answer_final = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE - ) + llm_answer_final = _parser(llm_answer_raw) return llm_answer_final @@ -334,21 +324,21 @@ def prompt_special_case_assignment( ] ) return answer_dict + + prompt, _parser = prompt_templates.SPECIAL_CASE_ASSIGNMENT( + exhibit_text, reimbursement_dict, special_case_dicts, special_case_term + ) # Otherwise use LLM llm_answer_raw = llm_utils.invoke_claude( - prompt_templates.SPECIAL_CASE_ASSIGNMENT( - exhibit_text, reimbursement_dict, special_case_dicts, special_case_term - ), + prompt, "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 - ) + index_answer = _parser(llm_answer_raw) try: if not string_utils.is_empty(index_answer): return special_case_dicts[int(index_answer)] @@ -367,7 +357,7 @@ def prompt_full_context( prompt_questions = full_context_fields.print_prompt_dict(constants) - full_context_prompt = prompt_templates.ONE_TO_ONE_MULTI_FIELD_TEMPLATE( + full_context_prompt, _parser = prompt_templates.ONE_TO_ONE_MULTI_FIELD_TEMPLATE( context=contract_text[ 0 : min( config.MAX_CONTEXT_LENGTH - len(prompt_questions), @@ -387,13 +377,10 @@ def prompt_full_context( instruction=prompt_templates.ONE_TO_ONE_MULTI_FIELD_INSTRUCTION(), ) - full_context_answers_dict = string_utils.universal_json_load(claude_answer_raw) + full_context_answers_dict = _parser(claude_answer_raw) except Exception as e: logging.error(f"Error processing high accuracy fields: {str(e)}") full_context_answers_dict = {} - for field in full_context_fields.list_fields(): - full_context_answers_dict[field] = f"Error: {str(e)}" - return full_context_answers_dict @@ -419,7 +406,7 @@ def validate_reimbursements_for_llm(answer_dict: dict[str, str], filename: str) return False service_term, reimb_term = answer_dict["SERVICE_TERM"], answer_dict["REIMB_TERM"] - prompt = prompt_templates.VALIDATE_REIMBURSEMENTS_PROMPT(service_term, reimb_term) + prompt, _parser = prompt_templates.VALIDATE_REIMBURSEMENTS_PROMPT(service_term, reimb_term) logging.debug(f"Prompt for reimbursement validation in {filename}:\n{prompt}") llm_response = llm_utils.invoke_claude( @@ -432,11 +419,7 @@ def validate_reimbursements_for_llm(answer_dict: dict[str, str], filename: str) logging.debug( f"LLM response for reimbursement validation in {filename}:\n{llm_response}" ) - final_answer = ( - string_utils.extract_text_from_delimiters(llm_response, Delimiter.PIPE) - .strip() - .upper() - ) + final_answer = _parser(llm_response) return final_answer == "YES" @@ -452,7 +435,7 @@ def prompt_dynamic(text: str, field_prompts, filename): Returns: dict: A dictionary containing field names as keys and extracted answers as values. """ - prompt = prompt_templates.EXHIBIT_LEVEL(text, field_prompts) + prompt, _parser = prompt_templates.EXHIBIT_LEVEL(text, field_prompts) logging.debug(f"Dynamic prompt for {filename}: {prompt}") llm_answer_raw = llm_utils.invoke_claude( prompt, @@ -462,7 +445,7 @@ def prompt_dynamic(text: str, field_prompts, filename): instruction=prompt_templates.EXHIBIT_LEVEL_INSTRUCTION(), ) # Returns dictionary of lists logging.debug(f"Dynamic answer for {filename}: {llm_answer_raw}") - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) return llm_answer_final @@ -480,7 +463,7 @@ def prompt_exhibit_linkage(page_header, previous_header, filename): Returns: str: LLM response indicating if the exhibit has changed. """ - prompt = prompt_templates.EXHIBIT_LINKAGE(page_header, previous_header) + prompt, _parser = prompt_templates.EXHIBIT_LINKAGE(page_header, previous_header) claude_answer_raw = llm_utils.invoke_claude( prompt, "legacy_sonnet", @@ -489,9 +472,8 @@ def prompt_exhibit_linkage(page_header, previous_header, filename): instruction=prompt_templates.EXHIBIT_LINKAGE_INSTRUCTION(), usage_label="EXHIBIT_LINKAGE", ) - exhibit_is_different = string_utils.extract_text_from_delimiters( - claude_answer_raw, Delimiter.PIPE - ) + exhibit_is_different = _parser(claude_answer_raw) + return exhibit_is_different @@ -509,7 +491,7 @@ def prompt_exhibit_header(page_content, filename): Returns: str: Extracted exhibit header identifier or marker indicating no header found. """ - prompt = prompt_templates.EXHIBIT_HEADER(page_content[0:400]) + prompt, _parser = prompt_templates.EXHIBIT_HEADER(page_content[0:400]) llm_answer_raw = llm_utils.invoke_claude( prompt, "sonnet_latest", @@ -519,9 +501,7 @@ def prompt_exhibit_header(page_content, filename): instruction=prompt_templates.EXHIBIT_HEADER_INSTRUCTION(), usage_label="EXHIBIT_HEADER", ) - llm_answer_extracted = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE - ) + llm_answer_extracted = _parser(llm_answer_raw) return llm_answer_extracted @@ -536,7 +516,7 @@ def prompt_date_fix(date: str) -> str: """ if string_utils.is_empty(date): return "N/A" - prompt = prompt_templates.DATE_FIX_PROMPT(date) + prompt, _parser = prompt_templates.DATE_FIX_PROMPT(date) response = llm_utils.invoke_claude( prompt, "haiku_latest", @@ -545,7 +525,7 @@ def prompt_date_fix(date: str) -> str: instruction=prompt_templates.DATE_FIX_INSTRUCTION(), usage_label="DATE_FIX", ) - return string_utils.extract_text_from_delimiters(response, Delimiter.PIPE) + return _parser(response) def prompt_derived_term_date( @@ -563,7 +543,7 @@ def prompt_derived_term_date( Returns: str: ISO-formatted termination date (YYYY-MM-DD) or "N/A" if calculation fails. """ - prompt = prompt_templates.DERIVED_TERM_DATE_PROMPT( + prompt, _parser = prompt_templates.DERIVED_TERM_DATE_PROMPT( effective_date, termination_information ) response = llm_utils.invoke_claude( @@ -574,7 +554,7 @@ def prompt_derived_term_date( instruction=prompt_templates.DERIVED_TERM_DATE_INSTRUCTION(), usage_label="DERIVED_TERM_DATE", ) - return string_utils.extract_text_from_delimiters(response, Delimiter.PIPE) + return _parser(response) def prompt_dynamic_assignment( @@ -592,14 +572,14 @@ def prompt_dynamic_assignment( # Use specialized prompt for REIMB_DATES assignment if field_name == "REIMB_DATES": - prompt = prompt_templates.REIMB_DATES_ASSIGNMENT( + prompt, _parser = 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( + prompt, _parser = prompt_templates.DYNAMIC_ASSIGNMENT( service_term, reimb_term, field_name, @@ -620,7 +600,7 @@ def prompt_dynamic_assignment( ) try: - llm_answer_final = string_utils.universal_json_load(llm_answer_raw) + llm_answer_final = _parser(llm_answer_raw) return llm_answer_final except: logging.error( @@ -673,7 +653,7 @@ def prompt_lesser_of_distribution( ... ) >>> # Returns: "Office Visit paid at the lesser of $150 or charges" """ - prompt = prompt_templates.LESSER_OF_DISTRIBUTION( + prompt, _parser = prompt_templates.LESSER_OF_DISTRIBUTION( service_term, reimb_term, page_num, @@ -697,9 +677,7 @@ def prompt_lesser_of_distribution( ) return reimb_term # Return original term unchanged else: - llm_answer_final = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE - ) + llm_answer_final = _parser(llm_answer_raw) logging.debug( f"Applied lesser-of to '{service_term}' on page {page_num}: " @@ -734,7 +712,7 @@ def prompt_lesser_of_check( f"LESSER_OF_CHECK input: service='{service_term}...', reimb_term='{reimb_term}...'" ) - prompt = prompt_templates.LESSER_OF_CHECK(service_term, reimb_term, exhibit_title) + prompt, _parser = prompt_templates.LESSER_OF_CHECK(service_term, reimb_term, exhibit_title) llm_answer_raw = llm_utils.invoke_claude( prompt, "sonnet_latest", @@ -746,9 +724,7 @@ def prompt_lesser_of_check( try: # Extract JSON from pipes - llm_answer_final = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE - ) + llm_answer_final = _parser(llm_answer_raw) logging.debug(f"LESSER_OF_CHECK extracted from pipes: {llm_answer_final}") @@ -816,7 +792,7 @@ def prompt_lesser_of_check( def prompt_exhibit_title_match( current_exhibit_title: str, target_exhibit_reference: str, filename: str = "" ) -> str: - prompt = prompt_templates.EXHIBIT_TITLE_MATCH( + prompt, _parser = prompt_templates.EXHIBIT_TITLE_MATCH( current_exhibit_title, target_exhibit_reference ) llm_answer_raw = llm_utils.invoke_claude( @@ -829,10 +805,8 @@ def prompt_exhibit_title_match( ) try: - llm_answer_final = string_utils.extract_text_from_delimiters( - llm_answer_raw, Delimiter.PIPE - ) - return llm_answer_final.strip().upper() # Return "YES" or "NO" as string + llm_answer_final = _parser(llm_answer_raw) + return llm_answer_final[0].strip().upper() # Return "YES" or "NO" as string except: return "NO" # Default to "NO" string @@ -850,14 +824,14 @@ def provider_name_match_check( return False # Create prompt for LLM - CHECK_PROVIDER_NAME_MATCH_PROMPT = prompt_templates.CHECK_PROVIDER_NAME_MATCH_PROMPT( + CHECK_PROVIDER_NAME_MATCH_PROMPT, _parser = prompt_templates.CHECK_PROVIDER_NAME_MATCH_PROMPT( provider_name=provider_name, hybrid_smart_chunking_provider_group_name=hybrid_smart_chunking_provider_group_name, ) try: # Adjust this based on your LLM client - response = llm_utils.invoke_claude( + llm_answer_raw = llm_utils.invoke_claude( CHECK_PROVIDER_NAME_MATCH_PROMPT, "sonnet_latest", filename, @@ -867,21 +841,22 @@ def provider_name_match_check( ) # Extract last character (should be Y or N) - response = string_utils.extract_text_from_delimiters(response, Delimiter.PIPE) + llm_answer_final = _parser(llm_answer_raw) + response_char = llm_answer_final[0] - if response == "Y": + if response_char == "Y": logging.debug( f"[is_provider_name_match_llm] ✓ LLM matched: '{provider_name}' ≈ '{hybrid_smart_chunking_provider_group_name}'" ) return True - elif response == "N": + elif response_char == "N": logging.debug( f"[is_provider_name_match_llm] ✗ LLM no match: '{provider_name}' ≠ '{hybrid_smart_chunking_provider_group_name}'" ) return False else: logging.warning( - f"[is_provider_name_match_llm] Unexpected response: {response}, defaulting to False" + f"[is_provider_name_match_llm] Unexpected response: {llm_answer_raw}, defaulting to False" ) return False diff --git a/src/pipelines/shared/extraction/dynamic_funcs.py b/src/pipelines/shared/extraction/dynamic_funcs.py index 5d8af6b..df412a3 100644 --- a/src/pipelines/shared/extraction/dynamic_funcs.py +++ b/src/pipelines/shared/extraction/dynamic_funcs.py @@ -48,7 +48,7 @@ def dynamic_primary( field, constants, filename, - prompt_templates.DYNAMIC_PRIMARY_TEXT, + prompt_templates.DYNAMIC_PRIMARY, ) # If there is at least ONE Non-N/A answers in the Exhibit if not string_utils.is_empty(exhibit_text_answer): diff --git a/src/pipelines/shared/preprocessing/hybrid_smart_chunking_funcs.py b/src/pipelines/shared/preprocessing/hybrid_smart_chunking_funcs.py index df70d25..cc634cb 100644 --- a/src/pipelines/shared/preprocessing/hybrid_smart_chunking_funcs.py +++ b/src/pipelines/shared/preprocessing/hybrid_smart_chunking_funcs.py @@ -19,11 +19,9 @@ import src.utils.string_utils as string_utils import src.utils.timing_utils as timing_utils import src.pipelines.shared.preprocessing.preprocessing_funcs as preprocessing_funcs import src.pipelines.shared.preprocessing.hybrid_smart_chunking_preprocessing as hybrid_smart_chunking_preprocessing -from src.constants.constants import Constants -from src.constants.delimiters import Delimiter from src import config from src.prompts import prompt_templates -from src.prompts.fieldset import FieldSet, Field +from src.prompts.fieldset import FieldSet from src.pipelines.shared.extraction.one_to_one_funcs import ( check_and_update_effective_date, ) diff --git a/src/prompts/prompt_templates.py b/src/prompts/prompt_templates.py index c876a9d..e745500 100644 --- a/src/prompts/prompt_templates.py +++ b/src/prompts/prompt_templates.py @@ -4,10 +4,6 @@ import src.config as config from src.prompts.fieldset import FieldSet from src.constants.constants import Constants from src.utils import string_utils, json_utils -from src.constants.delimiters import Delimiter - -# Legacy pipe format instructions (deprecated - use JSON formats instead) -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|." # JSON format instructions for standardized LLM outputs JSON_DICT_FORMAT_INSTRUCTIONS = """Return your final answer as a valid JSON dictionary with the specified field names as keys. @@ -44,11 +40,6 @@ def _json_list_parser(raw_output: str) -> list: return json_utils.parse_json_list(raw_output) -def _pipe_delimiter_parser(raw_output: str) -> str: - """Parse pipe-delimited output from LLM (legacy format).""" - return string_utils.extract_text_from_delimiters(raw_output, Delimiter.PIPE) - - # Module-level cached Constants instance for instruction functions # This is lazily initialized on first use and reused across all instruction calls _cached_constants = None @@ -264,8 +255,8 @@ Here is the text to analyze: return (prompt, _json_dict_parser) -def DYNAMIC_PRIMARY_TEXT_INSTRUCTION() -> str: - """Static instruction for DYNAMIC_PRIMARY_TEXT prompt caching. +def DYNAMIC_PRIMARY_INSTRUCTION() -> str: + """Static instruction for DYNAMIC_PRIMARY prompt caching. Contains extraction rules for text-based fields (allows obvious assumptions). """ return f"""[OBJECTIVE] @@ -282,7 +273,7 @@ Briefly explain your answer before putting the final answer in a properly-format {JSON_LIST_FORMAT_INSTRUCTIONS}""" -def DYNAMIC_PRIMARY_TEXT(context, field_name, field_prompt) -> Tuple[str, Callable[[str], list]]: +def DYNAMIC_PRIMARY(context, field_name, field_prompt) -> Tuple[str, Callable[[str], list]]: """Returns ONLY dynamic content for text-based dynamic primary extraction. Call DYNAMIC_PRIMARY_TEXT_INSTRUCTION() separately for the cached instruction. @@ -299,17 +290,6 @@ Here is the text to analyze: return (prompt, _json_list_parser) -def DYNAMIC_PRIMARY_INSTRUCTION() -> str: - """Legacy instruction for backward compatibility. - Use DYNAMIC_PRIMARY_TEXT_INSTRUCTION instead. - DEPRECATED: This function is maintained for backward compatibility only. - """ - return ( - "[OBJECTIVE]\n" - "Extract attribute values for dynamic fields from contract text. Use exact values and follow output format instructions." - ) - - def REIMB_DATES_ASSIGNMENT_INSTRUCTION() -> str: """Static instruction for REIMB_DATES_ASSIGNMENT prompt caching. Contains all date assignment rules and examples.