Update prompt_calls

This commit is contained in:
Katon Minhas
2026-02-02 20:53:23 -05:00
parent cbff139ce9
commit dba611469d
6 changed files with 205 additions and 257 deletions
@@ -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
@@ -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
+59 -84
View File
@@ -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
@@ -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):
@@ -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,
)
+3 -23
View File
@@ -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.