Merged in feature/DAIP2-2023-eliminate-full-context-processing (pull request #905)

Feature/DAIP2-2023 eliminate full context processing

* testing full context fields

* remove full context processing

* merge Dev with DAIP2-2-23

* full context removal in client codes

* AARETE_DERIVED_PROVIDER_NAME field changes

* Merged DEV into feature/DAIP2-2023-eliminate-full-context-processing

* optimized provider name

* black format fix

* contract title fixes

* PAYER NAME AUTO RENEWAL IND fixes

* Merged DEV into feature/DAIP2-2023-eliminate-full-context-processing

* Merge branch 'DEV' into feature/DAIP2-2023-eliminate-full-context-processing

* Remove prints


Approved-by: Katon Minhas
This commit is contained in:
Venkatakrishna Reddy Avula
2026-03-11 17:12:27 +00:00
committed by Katon Minhas
parent 8ea4f2e3e5
commit ea1f4a2c8c
12 changed files with 130 additions and 548 deletions
@@ -213,24 +213,7 @@ def run_one_to_one_prompts(
hsc_fields, constants, contract_text, filename, text_dict
)
)
################## RUN FULL CONTEXT PROMPTS ##################
# Use hsc_fields (not one_to_one_fields) to include bcbs_promise fields
# This ensures fallback to full_context works if HSC fails for any field
with timing_utils.timed_block("one_to_one.full_context", context=filename):
full_context_answers_dict = one_to_one_funcs.run_full_context_fields(
hsc_fields,
contract_text,
text_dict,
first_reimbursement_page,
constants,
filename,
)
################## COMBINE ANSWERS ################
# Prefer HSC answers over full_context when HSC value is not N/A
for key, value in full_context_answers_dict.items():
one_to_one_results[key] = value
################## COMBINE ANSWERS ##################
for key, value in hybrid_smart_chunked_answers_dict.items():
if not string_utils.is_empty(value):
one_to_one_results[key] = value
@@ -408,43 +408,6 @@ def prompt_special_case_assignment(
return special_case_dicts[0]
def prompt_full_context(
contract_text: str,
full_context_fields: FieldSet,
constants: Constants,
filename: str,
):
prompt_questions = full_context_fields.print_prompt_dict(constants)
full_context_prompt, _parser = prompt_templates.ONE_TO_ONE_MULTI_FIELD_TEMPLATE(
context=contract_text[
0 : min(
config.MAX_CONTEXT_LENGTH - len(prompt_questions),
len(contract_text) - 1,
)
],
questions=prompt_questions,
)
try:
claude_answer_raw = llm_utils.invoke_claude(
full_context_prompt,
"sonnet_latest",
filename,
8192,
cache=True,
instruction=prompt_templates.ONE_TO_ONE_MULTI_FIELD_INSTRUCTION(),
)
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
def validate_reimbursements_for_llm(answer_dict: dict[str, str], filename: str) -> bool:
"""Use LLM to determine if a service has actual reimbursement methodology.
@@ -211,17 +211,6 @@ def run_one_to_one_prompts(
)
)
################## RUN FULL CONTEXT PROMPTS ##################
with timing_utils.timed_block("one_to_one.full_context", context=filename):
full_context_answers_dict = one_to_one_funcs.run_full_context_fields(
one_to_one_fields,
contract_text,
text_dict,
first_reimbursement_page,
constants,
filename,
)
################## RUN CLOVER PROMPTS ##################
with timing_utils.timed_block("one_to_one.clover_fields", context=filename):
clover_answers_dict = one_to_one_funcs.run_full_context_fields(
@@ -232,11 +221,7 @@ def run_one_to_one_prompts(
constants,
filename,
)
################## COMBINE ANSWERS ################
# Prefer HSC answers over full_context when HSC value is not N/A
for key, value in full_context_answers_dict.items():
one_to_one_results[key] = value
################## COMBINE ANSWERS ##################
for key, value in hybrid_smart_chunked_answers_dict.items():
if not string_utils.is_empty(value):
one_to_one_results[key] = value
@@ -430,46 +430,6 @@ def extract_and_parse(text):
raise ValueError(f"Invalid JSON: {e}")
def prompt_full_context(
contract_text: str,
full_context_fields: FieldSet,
constants: Constants,
filename: str,
):
prompt_questions = full_context_fields.print_prompt_dict(constants)
full_context_prompt, _parser = prompt_templates.ONE_TO_ONE_MULTI_FIELD_TEMPLATE(
context=contract_text[
0 : min(
config.MAX_CONTEXT_LENGTH - len(prompt_questions),
len(contract_text) - 1,
)
],
questions=prompt_questions,
)
try:
claude_answer_raw = llm_utils.invoke_claude(
full_context_prompt,
"sonnet_latest",
filename,
8192,
cache=True,
instruction=prompt_templates.ONE_TO_ONE_MULTI_FIELD_INSTRUCTION(),
)
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)
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
def validate_reimbursements_for_llm(answer_dict: dict[str, str], filename: str) -> bool:
"""Use LLM to determine if a service has actual reimbursement methodology.
+1 -18
View File
@@ -225,24 +225,7 @@ def run_one_to_one_prompts(
f"Hybrid Smart Chunked Answers for {filename}: {hybrid_smart_chunked_answers_dict}"
)
################## RUN FULL CONTEXT PROMPTS ##################
# Only run if there are fields to extract
full_context_answers_dict = {}
if one_to_one_fields.contains_fields():
with timing_utils.timed_block("one_to_one.full_context", context=filename):
full_context_answers_dict = one_to_one_funcs.run_full_context_fields(
one_to_one_fields,
contract_text,
text_dict,
first_reimbursement_page,
constants,
filename,
)
################## COMBINE ANSWERS ################
# Prefer HSC answers over full_context when HSC value is not N/A
for key, value in full_context_answers_dict.items():
one_to_one_results[key] = value
################## COMBINE ANSWERS ##################
for key, value in hybrid_smart_chunked_answers_dict.items():
if not string_utils.is_empty(value):
one_to_one_results[key] = value
@@ -403,50 +403,6 @@ def prompt_special_case_assignment(
return special_case_dicts[0]
def prompt_full_context(
contract_text: str,
full_context_fields: FieldSet,
constants: Constants,
filename: str,
):
prompt_questions = full_context_fields.print_prompt_dict(constants)
# Extract field names for field-aware normalization
field_names = full_context_fields.list_fields()
context_text = contract_text[
0 : min(
config.MAX_CONTEXT_LENGTH - len(prompt_questions),
len(contract_text) - 1,
)
]
full_context_prompt, _parser = prompt_templates.ONE_TO_ONE_MULTI_FIELD_TEMPLATE(
context=context_text,
questions=prompt_questions,
field_names=field_names,
)
try:
claude_answer_raw = llm_utils.invoke_claude(
full_context_prompt,
"sonnet_latest",
filename,
8192,
cache=True,
instruction=prompt_templates.ONE_TO_ONE_MULTI_FIELD_INSTRUCTION(),
)
full_context_answers_dict = _parser(claude_answer_raw)
# Field-aware normalization is already done in the JSON parser
# No additional normalization needed for fields in FIELD_FORMAT_MAPPING
except Exception as e:
logging.error(f"Error processing high accuracy fields: {str(e)}")
full_context_answers_dict = {}
return full_context_answers_dict
def validate_reimbursements_for_llm(answer_dict: dict[str, str], filename: str) -> bool:
"""Use LLM to determine if a service has actual reimbursement methodology.
@@ -263,21 +263,20 @@ def add_one_to_one_field(
if field_to_add.field_name == "CLAIM_TYPE_CD":
field_to_add.prompt = (
field_to_add.prompt
+ prompt_templates.FULL_CONTEXT_CLAIM_TYPES_ADDITIONAL_INSTRUCTION()
+ prompt_templates.HSC_CLAIM_TYPES_ADDITIONAL_INSTRUCTION()
)
# Special instructions for dynamic primary fields when passed to 1:1
# Add specific template-language guidance, then append general full context instructions
if field_to_add.field_name in ["LOB", "PRODUCT", "PROGRAM", "NETWORK"]:
field_to_add.prompt = (
field_to_add.prompt
+ prompt_templates.FULL_CONTEXT_DYNAMIC_PRIMARY_INSTRUCTION()
field_to_add.prompt + prompt_templates.HSC_DYNAMIC_PRIMARY_INSTRUCTION()
)
# Apply general full context instructions to all fields
field_to_add.prompt = (
field_to_add.prompt
+ prompt_templates.FULL_CONTEXT_ADDITIONAL_INSTRUCTIONS(field_to_add.allow_na)
+ prompt_templates.HSC_ADDITIONAL_INSTRUCTIONS(field_to_add.allow_na)
)
if field_to_add.valid_values:
@@ -18,30 +18,6 @@ from src.pipelines.shared.extraction.vision_funcs import get_image_array_based_a
from src.prompts.fieldset import FieldSet
def pass_smart_chunked_to_full_context(answers_dict, one_to_one_fields):
# even after running the vision api, if the effective date is still N/A, we need to run the prompt again with full context
if "EFFECTIVE_DT" in answers_dict and string_utils.is_empty(
answers_dict.get("EFFECTIVE_DT")
):
# if the effective date is N/A, we need to run the prompt again with full context
one_to_one_fields.update_field_attr(
field_name="EFFECTIVE_DT",
attr="field_type",
value="full_context",
)
# if the termination date is N/A, we need to run the prompt again with full context
if "TERMINATION_DT" in answers_dict and string_utils.is_empty(
answers_dict.get("TERMINATION_DT")
):
# if the termination date is N/A, we need to run the prompt again with full context
one_to_one_fields.update_field_attr(
field_name="TERMINATION_DT",
attr="field_type",
value="full_context",
)
return one_to_one_fields
def check_and_update_effective_date(answers_dict, text_dict, filename):
"""Apply two-stage postprocessing to effective date with optional vision-based fallback.
@@ -135,184 +111,6 @@ def check_and_update_effective_date(answers_dict, text_dict, filename):
return answers_dict
def run_full_context_fields(
one_to_one_fields: FieldSet,
contract_text: str,
text_dict: dict[str, str],
first_reimbursement_page: str,
constants: Constants,
filename: str,
) -> dict[str, str]:
"""Process fields requiring full contract context and return extracted answers with post-processing.
This function orchestrates the extraction and processing of contract-level fields that require
access to the entire contract text (as opposed to smart-chunked or reimbursement-level
fields that use targeted context). The workflow includes:
1. **Field Filtering**: Extracts fields marked as "full_context" or "full_context_separate"
2. **State Optimization**: Skips PAYER_STATE extraction if only one state is configured in the batch
3. **LLM Extraction**: Invokes Claude with full contract context to extract field values
4. **State Normalization**: Converts state names to standardized two-letter abbreviations
(e.g., "California""CA", "New York""NY")
5. **Special Case Breakout**: Processes any "special_case_primary" fields that require additional
parsing (e.g., complex reimbursement terms)
Args:
one_to_one_fields (FieldSet): Complete set of one-to-one fields to process. The function
filters this to only "full_context" and "full_context_separate" types.
contract_text (str): Full untruncated text of the contract. This is passed to the LLM
with max context length constraints applied in prompt_calls.prompt_full_context().
constants (Constants): Configuration object containing valid codes and mappings
used during extraction and post-processing.
filename (str): Source filename (e.g., "contract_001.txt").
Returns:
dict[str, str]: Dictionary mapping field names to extracted values. Common fields include:
- PAYER_STATE: Two-letter state abbreviation or value from config.STATE[0] if single-state batch
- PROVIDER_STATE: Two-letter state abbreviation
Side Effects:
- Makes LLM API call via prompt_calls.prompt_full_context()
- May make additional LLM calls for special case breakouts via one_to_one_breakout()
- Logs extraction progress and results via logging_utils context
Notes:
- State optimization assumes config.STATE contains the list of states in the current batch
- State normalization uses string_utils.normalize_state_to_abbreviation() which handles
full names, abbreviations, and common misspellings
- Special case breakout is only triggered for fields with field_type="special_case_primary"
- Full context fields are processed together in a single LLM call for efficiency
Example:
>>> fields = FieldSet(relationship="one_to_one", file_path="fields.json")
>>> text = "Agreement between Aetna and Dr. Smith, effective in California..."
>>> constants = Constants(...)
>>> result = run_full_context_fields(fields, text, constants, "contract.txt")
>>> result["PROVIDER_STATE"]
'CA'
>>> result["PAYER_STATE"]
'CA'
"""
full_context_fields = one_to_one_fields.filter(field_type="full_context")
full_context_answers_dict = prompt_calls.prompt_full_context(
contract_text, full_context_fields, constants, filename
)
# Normalize PAYER_STATE to two-letter abbreviation
# normalize_state_field correctly handles list[str] format from field-aware parser
full_context_answers_dict = string_utils.normalize_state_field(
full_context_answers_dict, field_name="PAYER_STATE"
)
# Normalize PROVIDER_STATE to two-letter abbreviation
# normalize_state_field correctly handles list[str] format from field-aware parser
full_context_answers_dict = string_utils.normalize_state_field(
full_context_answers_dict, field_name="PROVIDER_STATE"
)
# Run Special Case Breakout on any breakout terms
full_context_answers_dict = one_to_one_breakout(full_context_answers_dict, filename)
# Field-aware normalization is already done in prompt_calls.prompt_full_context()
# and prompt_special_case_breakout() uses field-aware parsers
# No additional normalization needed for fields in FIELD_FORMAT_MAPPING
# Handle separate one-to-one fields that require individual processing
separate_fields = one_to_one_fields.filter(field_type="full_context_separate")
return full_context_answers_dict
def one_to_one_breakout(
full_context_answers_dict: dict[str, str], filename: str
) -> dict[str, str]:
"""Process special case primary fields through LLM-based breakout to extract structured sub-fields.
Special case breakout is a two-stage extraction process used for complex contract-level payment
terms that require additional parsing after initial extraction. For example, a STOP_LOSS_TERM
containing "compensate Provider 120% of Medicaid once claim exceeds $300,000" needs breakout to
extract STOP_LOSS_PCT_RATE_ON_EXCESS_CHARGES=120, STOP_LOSS_FIXED_LOSS_THRESHOLD=$300,000, etc.
This function:
1. Loads all fields marked as field_type="special_case_primary" from the field JSON.
These fields represent already-extracted complex cases that need to be broken out.
2. For each special case field present in the input dictionary:
- Retrieves the field's breakout template (e.g., STOP_LOSS_BREAKOUT, OUTLIER_BREAKOUT)
- Invokes Claude with the breakout template and the field's extracted value
- Parses the LLM response into structured sub-fields
- Updates the input dictionary with the breakout results
3. Returns the mutated dictionary with all breakout results appended
Args:
full_context_answers_dict (dict[str, str]): Dictionary containing extracted field values
from full context processing. This dictionary is **mutated in place** with breakout results.
Keys are field names (e.g., "STOP_LOSS_TERM", "OUTLIER_TERM"), values are extracted text.
filename (str): Source filename (e.g., "contract_001.txt"). Used for:
- Logging context
- LLM request attribution
- Error tracking
Returns:
dict[str, str]: The mutated input dictionary with breakout results added. For example:
Input: {"STOP_LOSS_TERM": "compensate Provider 120% of Medicaid once claim exceeds $300,000"}
Output: {
"STOP_LOSS_TERM": "compensate Provider 120% of Medicaid once claim exceeds $300,000",
"STOP_LOSS_PCT_RATE_ON_EXCESS_CHARGES": "120",
"STOP_LOSS_FIXED_LOSS_THRESHOLD": "$300,000",
"STOP_LOSS_FIRST_DOLLAR_IND": "N"
}
Side Effects:
- Mutates full_context_answers_dict in place
- Makes LLM API call via prompt_calls.prompt_special_case_breakout() for each
special_case_primary field present in the input dictionary
- Logs breakout progress and results via logging_utils context
Notes:
- Only processes fields that are (a) marked as special_case_primary in the field JSON
AND (b) present in the input dictionary with non-empty values
- Common special case fields include: STOP_LOSS_TERM, OUTLIER_TERM, SEQUESTRATION_TERM,
DISCOUNT_TERM, PREMIUM_TERM, RATE_ESCALATOR_TERM, FACILITY_ADJUSTMENT_TERM
- Breakout templates are defined in the Field object's breakout_template attribute
(e.g., "STOP_LOSS_BREAKOUT", "OUTLIER_BREAKOUT")
- If a special case field is missing from the input dict, it is silently skipped
- Breakout results are appended directly to the input dictionary (no namespacing)
- If breakout parsing fails, the error is logged but the function continues processing
remaining fields
Example:
>>> answers = {
... "PAYER_NAME": "Aetna",
... "OUTLIER_TERM": "outlier payment of 80% for charges exceeding $50,000"
... }
>>> result = one_to_one_breakout(answers, "contract.txt")
>>> result.keys()
dict_keys(['PAYER_NAME', 'OUTLIER_TERM', 'OUTLIER_PCT_RATE_ON_EXCESS_CHARGES',
'OUTLIER_FIXED_LOSS_THRESHOLD', 'OUTLIER_FIRST_DOLLAR_IND'])
>>> result["OUTLIER_PCT_RATE_ON_EXCESS_CHARGES"]
'80'
>>> result["OUTLIER_FIXED_LOSS_THRESHOLD"]
'$50,000'
"""
special_case_primary_fields = FieldSet(
config.FIELD_JSON_PATH, field_type="special_case_primary"
)
for term_field_name in special_case_primary_fields.list_fields():
if term_field_name in full_context_answers_dict:
term_field = special_case_primary_fields.get_field(term_field_name)
breakout_template = term_field.get_breakout_template()
term_answer = full_context_answers_dict[term_field_name]
breakout_answer_dict = prompt_calls.prompt_special_case_breakout(
breakout_template, term_answer, filename
)
# Field-aware normalization is already done in prompt_special_case_breakout()
# which uses field-aware parsers based on the breakout template's field_names
if isinstance(breakout_answer_dict, dict):
full_context_answers_dict.update(breakout_answer_dict)
return full_context_answers_dict
def get_aarete_derived_termination_dt(one_to_one_results: dict):
"""Derive standardized termination date from effective date and term duration.
@@ -153,6 +153,100 @@ def preprocessing_pipeline(contract_text):
return sections_with_page_numbers, is_preserved
def update_provider_name(
answers_dict: dict,
filename: str,
one_to_one_fields: FieldSet,
retriever,
reranker,
rag_config: RAGConfig,
text_dict: dict,
constants,
) -> dict:
"""Update provider name fields using enhanced RAG if initial extraction failed.
Args:
answers_dict: Dictionary containing extracted field values
filename: Name of the file being processed
one_to_one_fields: FieldSet containing field definitions
retriever: Hybrid retriever instance
reranker: Reranker client
rag_config: RAG configuration
text_dict: Dictionary mapping page numbers to text content
constants: Constants object
Returns:
Updated answers_dict with provider name fields populated
"""
provider_name = answers_dict.get("PROVIDER_NAME")
# if (provider name is a str and non empty) or (provider name is a list and has only one non empty item),
# then populate AARETE_DERIVED_PROVIDER_NAME with that provider name
if provider_name and (
(isinstance(provider_name, str) and not string_utils.is_empty(provider_name))
or (
isinstance(provider_name, list)
and len(provider_name) == 1
and not string_utils.is_empty(provider_name)
)
):
answers_dict["AARETE_DERIVED_PROVIDER_NAME"] = (
provider_name if isinstance(provider_name, str) else provider_name[0]
)
logging.info(
f"AARETE_DERIVED_PROVIDER_NAME set to: {answers_dict['AARETE_DERIVED_PROVIDER_NAME']} | Filename: {filename}"
)
else:
logging.info(
f"Attempting enhanced RAG extraction for AARETE_DERIVED_PROVIDER_NAME | Filename: {filename}"
)
# Create custom RAG config with more aggressive retrieval for AARETE_DERIVED_PROVIDER_NAME
enhanced_config = RAGConfig(
CHUNK_SIZE=rag_config.CHUNK_SIZE,
CHUNK_OVERLAP=rag_config.CHUNK_OVERLAP,
SEMANTIC_TOP_K=35,
KEYWORD_TOP_K=25,
ENSEMBLE_TOP_K=40,
RERANKER_TOP_K=30, # increase reranker top k to have more candidates for LLM to extract from
BM25_WEIGHT=0.5, # Balanced for better keyword matching
VECTOR_WEIGHT=0.5, # Balanced for better semantic matching
RERANKER=rag_config.RERANKER,
EMBEDDING_MODEL_ID=rag_config.EMBEDDING_MODEL_ID,
BEDROCK_REGION=rag_config.BEDROCK_REGION,
)
hsc_provider_name = prompt_hsc_single_field(
one_to_one_fields.get_field("AARETE_DERIVED_PROVIDER_NAME"),
retriever,
reranker,
enhanced_config, # Use enhanced config instead of default
text_dict,
constants,
filename,
)[
1
] # Get the field value from the returned tuple
if not string_utils.is_empty(hsc_provider_name):
answers_dict["AARETE_DERIVED_PROVIDER_NAME"] = (
hsc_provider_name[0]
if isinstance(hsc_provider_name, list)
else hsc_provider_name
)
logging.info(
f"AARETE_DERIVED_PROVIDER_NAME extracted via enhanced RAG: {answers_dict['AARETE_DERIVED_PROVIDER_NAME']} | Filename: {filename}"
)
if string_utils.is_empty(answers_dict["PROVIDER_NAME"]):
answers_dict["PROVIDER_NAME"] = answers_dict.get(
"AARETE_DERIVED_PROVIDER_NAME", "N/A"
)
logging.info(
f"Empty PROVIDER_NAME updated to: {answers_dict['PROVIDER_NAME']} | Filename: {filename}"
)
return answers_dict
def run_hybrid_smart_chunked_fields(
one_to_one_fields: FieldSet,
constants,
@@ -242,6 +336,18 @@ def run_hybrid_smart_chunked_fields(
except Exception as e:
logging.error(f"Error processing field: {str(e)}")
# Update provider name fields with enhanced RAG if needed
answers_dict = update_provider_name(
answers_dict=answers_dict,
filename=filename,
one_to_one_fields=one_to_one_fields,
retriever=retriever,
reranker=reranker,
rag_config=rag_config,
text_dict=text_dict,
constants=constants,
)
del retriever, vectorstore, chunks
gc.collect()
logging.debug(f"RAG extraction done: {len(answers_dict)} fields")
@@ -249,31 +355,6 @@ def run_hybrid_smart_chunked_fields(
# FILENAME_AMENDMENT_NUM
answers_dict = extract_amendment_num_from_filename(answers_dict, filename)
answers_dict = check_and_update_effective_date(
answers_dict, text_dict, filename
)
# even after running the vision api, if the effective date is still N/A, we need to run the prompt again with full context
if "AARETE_DERIVED_EFFECTIVE_DT" in answers_dict and string_utils.is_empty(
answers_dict.get("AARETE_DERIVED_EFFECTIVE_DT")
):
# if the effective date is N/A, we need to run the prompt again with full context
one_to_one_fields.update_field_attr(
field_name="EFFECTIVE_DT",
attr="field_type",
value="full_context",
)
# if the termination date is N/A, we need to run the prompt again with full context
if "TERMINATION_DT" in answers_dict and string_utils.is_empty(
answers_dict.get("TERMINATION_DT")
):
# if the termination date is N/A, we need to run the prompt again with full context
one_to_one_fields.update_field_attr(
field_name="TERMINATION_DT",
attr="field_type",
value="full_context",
)
# Normalize PROVIDER_STATE
answers_dict = string_utils.normalize_state_field(
answers_dict, field_name="PROVIDER_STATE"
@@ -357,18 +438,17 @@ def prompt_hsc_single_field(
context = format_chunks_for_llm(text_dict, reranked)
if len(context) == 0:
# Fallback: mark field for full_context processing
field.field_type = "full_context"
logging.warning(
f"No context retrieved for {field.field_name}, marking as full_context"
f"No context retrieved for {field.field_name}, marking as Null value"
)
return (field.field_name, "N/A", field)
llm_prompt = field.get_prompt_dict(
constants
) # Returns dict of {field_name : prompt}
if field.field_name == "CONTRACT_TITLE":
context = text_dict["1"]
if field.field_name in ["CONTRACT_TITLE", "PAYER_NAME"]:
# concatenate values from first 3 items in text_dict to provide more context for contract title extraction, as it is often found in the beginning of the contract
context = "\n\n".join(list(text_dict.values())[:3])
prompt, _parser = prompt_templates.ONE_TO_ONE_SINGLE_FIELD_TEMPLATE(
context, llm_prompt, field_name=field.field_name
)
@@ -376,6 +456,9 @@ def prompt_hsc_single_field(
logging.debug(f"Field: {field.field_name}")
logging.debug(f"Retrieval question: {retrieval_query}")
logging.debug(f"LLM prompt: {llm_prompt}")
logging.debug(
f"context with len {len(context)}: {context[:500]}........{context[-500:]}"
) # log only the beginning and end of the context to avoid clutter
# LLM call
with timing_utils.timed_block(
@@ -388,28 +471,6 @@ def prompt_hsc_single_field(
try:
llm_answer_final = _parser(llm_answer_raw) # returns dict
field_value = llm_answer_final.get(field.field_name)
# Check for N/A - handle both string and list formats
field_value_str = (
field_value
if isinstance(field_value, str)
else (
field_value[0]
if isinstance(field_value, list) and field_value
else "N/A"
)
)
if string_utils.is_empty(field_value_str):
field.field_type = "full_context"
if field.field_name == "PAYER_NAME":
field.prompt = (
field.prompt
+ prompt_templates.FULL_CONTEXT_PAYER_NAME_ADDITIONAL_INSTRUCTION()
)
logging.warning(
f"Obtained N/A for {field.field_name}, marking as full_context"
)
return (field.field_name, field_value, field)
except Exception as e:
@@ -420,9 +481,5 @@ def prompt_hsc_single_field(
return (field.field_name, "N/A", field)
except Exception as e:
# Mark field for full_context retry and preserve error info
field.field_type = "full_context"
logging.error(
f"Error on {field.field_name}: {type(e).__name__}: {str(e)}, marking as full_context"
)
logging.error(f"Error on {field.field_name}: {type(e).__name__}: {str(e)}")
return (field.field_name, "N/A", field)
+6 -5
View File
@@ -234,13 +234,13 @@
"relationship": "one_to_one",
"field_type": "smart_chunked",
"prompt": "What US state is the payer based in or incorporated in? Look for:\n\n1. Payer address information in the contract header, preamble, or signature sections\n2. State names or abbreviations in payer contact information\n3. State of incorporation or principal place of business\n4. Payer headquarters location\n5. Payer licensing information that mentions a specific state\n\nReturn the full state name (e.g., 'California', 'Texas', 'New York') or standard 2-letter abbreviation (e.g., 'CA', 'TX', 'NY') as it appears in the document.\n\nIf multiple states are mentioned, prioritize:\n1. The state in the payer's primary/headquarters address\n2. The state of incorporation\n3. The state most frequently referenced for the payer organization\n\nIf no state can be determined, return 'N/A'.",
"retrieval_question": "What US state is the payer based in, incorporated in, or headquartered in? What state is mentioned in the payer's address or principal place of business?"
"retrieval_question": "What US state is the payer based in, incorporated in, or headquartered in? What state is mentioned in the payer's address or principal place of business? Mailing address, headquarters location, state of incorporation for the payer organization? Legal jurisdiction or governing law state for the payer organization?"
},
{
"field_name": "PAYER_NAME",
"relationship": "one_to_one",
"field_type": "smart_chunked",
"prompt": "What is the name of the payer that is a party to the contract as stated in the Preamble? Extract the name exactly as it appears in the contract.",
"prompt": "What is the name of the payer that is a party to the contract as stated in the Preamble? Extract the name exactly as it appears in the contract. If the payer name is not explicitly stated in the preamble, look for it in the signature section or the parties section of the contract. The payer is typically a health plan, insurance company, or payor organization (e.g., Highmark, Aetna, UnitedHealthcare). If the payer name cannot be determined from the given text, return 'N/A'.",
"retrieval_question": "What is the name of the payer, health plan, insurance company, or payor that is a party to this contract? Payer name in the preamble, parties section, or beginning of agreement."
},
{
@@ -267,11 +267,12 @@
"field_type": "smart_chunked",
"prompt": "Extract the PROVIDER organization name(s) from the retrieved contract text.\n\nThe provider is the healthcare organization entering into the agreement (NOT the payer/insurance company like Highmark, Aetna, UnitedHealthcare, Blue Cross Blue Shield, etc.).\n\nGUIDELINES:\n1. Look for the provider name in phrases like:\n - 'This Agreement is between [PAYER] and [PROVIDER]'\n - 'Agreement between [PAYER] and [PROVIDER]'\n - 'by and between [PAYER] and [PROVIDER]'\n - '[PROVIDER] (hereinafter referred to as \"Provider\")'\n - 'Provider: [PROVIDER]'\n2. The provider name often appears in the opening paragraph or preamble of the contract.\n3. Return the EXACT provider name as it appears in the document, including legal suffixes (LLC, Inc., P.C., etc.).\n4. If the provider name includes 'd/b/a' (doing business as), return the FULL name including the d/b/a portion.\n5. If MULTIPLE provider organization names appear in the contract (e.g., multiple entities entering the agreement), return ALL of them as a JSON list.\n - Example: [\"Provider Group A, LLC\", \"Provider Group B, P.C.\", \"Provider Group C d/b/a City Medical\"]\n6. Do NOT return:\n - The payer/insurance company name\n - Individual physician names (unless that is the practice name)\n - Department names only\n - Addresses or other metadata\n7. If the provider name cannot be determined from the retrieved text, return 'N/A'.\n\nBriefly explain your reasoning. Return your answer as a valid JSON dictionary with the key \"PROVIDER_NAME\". If multiple provider names are found, the value should be a JSON list. If a single provider name is found, the value should be a string. Example: {\"PROVIDER_NAME\": [\"Provider Group A, LLC\", \"Provider Group B, P.C.\"]} or {\"PROVIDER_NAME\": \"Provider Group A, LLC\"} or {\"PROVIDER_NAME\": \"N/A\"}",
"retrieval_question": "What is the provider organization name? Agreement between payer and provider. This agreement is made between. By and between. Hereinafter referred to as Provider. Provider name in contract preamble. Provider party to the agreement. Healthcare organization entering agreement."
},
{
"field_name": "AARETE_DERIVED_PROVIDER_NAME",
"relationship": "one_to_one",
"field_type": "full_context",
"field_type": "TBD",
"prompt": "Extract the PROVIDER organization name from the retrieved contract text.\n\nThe provider is the healthcare organization entering into the agreement (NOT the payer/insurance company like Highmark, Aetna, UnitedHealthcare, Blue Cross Blue Shield, etc.).\n\nGUIDELINES:\n1. Look for the provider name in phrases like:\n - 'This Agreement is between [PAYER] and [PROVIDER]'\n - 'Agreement between [PAYER] and [PROVIDER]'\n - 'by and between [PAYER] and [PROVIDER]'\n - '[PROVIDER] (hereinafter referred to as \"Provider\")'\n - 'Provider: [PROVIDER]'\n2. The provider name often appears in the opening paragraph or preamble of the contract.\n3. Return the EXACT provider name as it appears in the document, including legal suffixes (LLC, Inc., P.C., etc.).\n4. If the provider name includes 'd/b/a' (doing business as), return the FULL name including the d/b/a portion.\n5. If multiple provider organization names appear in the contract (for example, if multiple entities are entering into the agreement), identify and return the single most appropriate provider name based on the overall contract context. Carefully analyze all provider names mentioned in the contract. If the extracted provider name contains the word 'and', split the name into separate entities and determine which one is the most relevant primary provider for the agreement. Always return only one provider name in the output. The final result must not contain the word 'and'.\n6. Do NOT return:\n - The payer/insurance company name\n - Individual physician names (unless that is the practice name)\n - Department names only\n - Addresses or other metadata\n7. If the provider name cannot be determined from the retrieved text, return 'N/A'.\n\nBriefly explain your reasoning.",
"retrieval_question": "What is the provider organization name? Agreement between payer and provider. This agreement is made between. By and between. Hereinafter referred to as Provider. Provider name in contract preamble. Provider party to the agreement. Healthcare organization entering agreement."
},
@@ -349,7 +350,7 @@
"field_name": "AUTO_RENEWAL_IND",
"relationship": "one_to_one",
"field_type": "smart_chunked",
"prompt": "This pertains to the term and termination section of the contract. If this contract automatically renews (keyword \"automatically\" should be mentioned) , answer Y. Otherwise, answer N. If you cannot determine answer N.",
"prompt": "This pertains to the term and termination section of the contract. Determine whether the contract includes an automatic renewal. Answer Y only if the contract renews automatically without any action required by either party (for example, it renews unless a party gives notice to terminate). Answer N if renewal requires any action, such as written agreement, consent, negotiation, or signing a renewal. If you cannot determine, answer N. Output only Y or N.",
"keywords": [
"Renew",
"renew",
@@ -361,7 +362,7 @@
],
"methodology": "or",
"case_sensitive": "True",
"retrieval_question": "Does this contract automatically renew, auto-renew, continue automatically, or remain in effect unless terminated by either party? Is this an evergreen contract or does it renew for successive terms or renewal periods?"
"retrieval_question": "Does this contract automatically renew, auto-renew, continue automatically, or remain in effect unless terminated by either party? Does this contract require no action for renewal? Is this an evergreen contract or does it renew for successive terms or renewal periods? Does term and termination of the agreement indicate that the contract automatically renews for renewal periods?"
},
{
"field_name": "AUTO_RENEWAL_TERM",
+4 -67
View File
@@ -194,9 +194,6 @@ def get_cacheable_instructions():
cacheable_instructions["ONE_TO_ONE_SINGLE_FIELD"] = (
ONE_TO_ONE_SINGLE_FIELD_INSTRUCTION()
)
cacheable_instructions["ONE_TO_ONE_MULTI_FIELD"] = (
ONE_TO_ONE_MULTI_FIELD_INSTRUCTION()
)
except Exception:
pass
@@ -1675,22 +1672,18 @@ Contract text:
return (prompt, _json_list_parser)
def FULL_CONTEXT_CLAIM_TYPES_ADDITIONAL_INSTRUCTION():
def HSC_CLAIM_TYPES_ADDITIONAL_INSTRUCTION():
return " IMPORTANT: Prioritize the title and header as the primary source for determining claim type. Look for terms like 'Professional', 'Physician', 'Hospital', 'Facility', 'Institutional', 'Ancillary', 'DME', 'Home Health', 'Laboratory' in the title or header. If the title/header clearly indicates the claim type, use that. Otherwise, check the preamble, provider name, or signature section to help determine the claim type."
def FULL_CONTEXT_ADDITIONAL_INSTRUCTIONS(allow_na):
def HSC_ADDITIONAL_INSTRUCTIONS(allow_na):
if allow_na:
return " Only provide an answer if that answer clearly applies to the ENTIRE contract. Otherwise, answer N/A. If there are multiple correct answers, return them as a JSON list (array) in the dictionary value."
else:
return " Do NOT leave this field N/A. Choose the most appropriate answer based on the context. If there are multiple answers, return them as a JSON list (array) in the dictionary value."
def FULL_CONTEXT_PAYER_NAME_ADDITIONAL_INSTRUCTION():
return " Additionally check the contract text for the payer name or payer organization name or payer entity or name of organization issuing the health plan/policy and Choose the most appropriate answer based on the context."
def FULL_CONTEXT_DYNAMIC_PRIMARY_INSTRUCTION():
def HSC_DYNAMIC_PRIMARY_INSTRUCTION():
"""Additional instructions for dynamic primary fields (LOB, PRODUCT, PROGRAM, NETWORK) when passed to 1:1 processing.
These instructions emphasize that values should only be extracted if they explicitly
@@ -1753,62 +1746,6 @@ Note: PROVIDER NAME B is typically a single name but may contain multiple names.
return (prompt, _json_list_parser)
def ONE_TO_ONE_MULTI_FIELD_TEMPLATE(
context, questions: dict[str, str], field_names: list[str] | None = None
) -> Tuple[str, Callable[[str], dict]]:
"""
Returns prompt and parser for one-to-one multi-field extraction.
Args:
context: Contract text context
questions: Dictionary mapping field names to prompt questions
field_names: Optional list of field names for field-aware normalization
Returns:
Tuple of (prompt_string, parser_function) where parser expects JSON dict output.
"""
prompt = f"""The following is a contract (or excerpt thereof). Answer the following questions: {questions}
## START CONTRACT TEXT ##
{context.replace('"', "'")}
## END CONTRACT TEXT ##
For each question, explain your reasoning. Then provide your final answers in a JSON dictionary format. Explanation MUST NOT contain any curly brackets '{{' or '}}'. Replace them with '()' instead.
The JSON should:
- Include every key
- Use 'N/A' for any question where the requested information doesn't apply to this context
- Use 'UNKNOWN' for any question where the requested information should exist but cannot be clearly identified
- Contain only the final answers, not explanations
- NOT contain any curly brackets '{{' or '}}' inside.
- If curly bracket still exists inside JSON, replace them with '()' instead.
Example format:
I found the effective date in section 2.1 which states...
The term length appears in two places but I'm selecting...
{{
"effective_date": "January 1, 2024",
"term_length": "36 months"
}}
"""
# Use field-aware parser if field_names provided
if field_names:
parser = _create_json_dict_parser(field_names=field_names)
else:
parser = _json_dict_parser
return (prompt, parser)
def ONE_TO_ONE_MULTI_FIELD_INSTRUCTION() -> str:
return (
"[OBJECTIVE]\n"
"Answer multiple deterministic questions about contract text. Follow JSON-only output rules in the prompt."
)
def EXTRACT_AMENDMENT_NUM_FROM_FILENAME(filename) -> Tuple[str, Callable[[str], dict]]:
"""
Returns prompt and parser for extracting amendment number from filename.
@@ -2169,7 +2106,7 @@ Output: Date in YYYY/MM/DD format, or input if it's a duration, or "N/A" if ambi
1. Convert to YYYY/MM/DD (pad with leading zeros, use / separators)
2. Add 2000 to 2-digit years 0-49, 1900 to 50-99
3. Return unchanged if it's a duration (e.g., "3 years", "one year from effective date")
4. Return N/A if: missing/ambiguous month or year components, or invalid dates. If just the day is missing, assume it's the first of the month.
4. Return N/A if: missing/ambiguous month or year components, or invalid dates. If just the day is missing or day is just 0, assume it's the first of the month.
5. For ambiguous input cases, assume American format (MM/DD/YYYY) and apply YYYY/MM/DD formatting
6. Any day values with spaces or leading zeros (e.g., '0 1' or '01') are interpreted as single-digit days
7. Return final answer as a single item in a JSON list
-40
View File
@@ -638,46 +638,6 @@ class TestPromptCalls(unittest.TestCase):
f"LOB_PROGRAM_RELATIONSHIP should be list[str] (format: {expected_format}), got {type(result)}",
)
# ==================== ONE-TO-ONE PROMPTS ====================
@patch("src.utils.llm_utils.invoke_claude")
def test_prompt_full_context_normalizes_fields(self, mock_invoke):
"""Test that prompt_full_context normalizes one-to-one fields."""
# Simulate LLM returning full context answers
mock_invoke.return_value = """{
"PAYER_NAME": "Test Payer",
"CONTRACT_TITLE": "Test Contract",
"EFFECTIVE_DT": "2024/01/01"
}"""
full_context_fields = FieldSet(config.FIELD_JSON_PATH, field_type="one_to_one")
result = prompt_calls.prompt_full_context(
"Contract text here", full_context_fields, self.constants, self.filename
)
# Should return dict with normalized fields
self.assertIsInstance(result, dict)
# Check fields against format mapping
for field_name in ["PAYER_NAME", "CONTRACT_TITLE", "EFFECTIVE_DT"]:
if field_name in result:
expected_format = FIELD_FORMAT_MAPPING.get(field_name, "str")
actual_value = result.get(field_name)
if expected_format == "str":
self.assertIsInstance(
actual_value,
str,
f"{field_name} should be str (format: {expected_format}), got {type(actual_value)}",
)
elif expected_format == "list[str]":
self.assertIsInstance(
actual_value,
list,
f"{field_name} should be list[str] (format: {expected_format}), got {type(actual_value)}",
)
# ==================== VALIDATION PROMPTS ====================
@patch("src.utils.llm_utils.invoke_claude")