Merged in bugfix/row_count (pull request #858)

Bugfix/row count

* Updated lesser of statements filter; Updated lesser of prompt related to GLOBAL and STANDALONE

* Updated validate reimbursement prompt

* Update lesser of check prompt related to GLOBAL assignment

* Restructured VALIDATE_REIMBURSEMENTS_PROMPT

* Updated reimbursement primary; validate reimbursement prompt

* Updated reimb primary prompt and validate reimb prompt

* Code cleaned up. Updated unittest cases

* Updated validate reimbursement prompt

* Added logging

* Update on validate reimbursement prompt; Code clean up

* Fix Black version conflict in dependencies

* Add Black back to dependencies

* Apply black formatting and update uv.lock

* Resolve conflict

* Merge branch 'DEV' into bugfix/row_count


Approved-by: Katon Minhas
This commit is contained in:
Sha Brown
2026-01-30 23:28:48 +00:00
committed by Katon Minhas
parent 90bed0d5f6
commit 89515ca5cc
6 changed files with 268 additions and 221 deletions
-1
View File
@@ -33,7 +33,6 @@ dependencies = [
[dependency-groups]
dev = [
"black>=24.10.0",
"mypy>=1.12.0",
"jupyter>=1.1.1",
"isort>=5.13.2",
+1 -1
View File
@@ -729,7 +729,7 @@ def prompt_lesser_of_check(
"""
# DEBUG: Log what we're sending
logging.debug(
f"LESSER_OF_CHECK input: service='{service_term[:50]}...', reimb_term='{reimb_term[:100]}...'"
f"LESSER_OF_CHECK input: service='{service_term}...', reimb_term='{reimb_term}...'"
)
prompt = prompt_templates.LESSER_OF_CHECK(service_term, reimb_term, exhibit_title)
@@ -807,7 +807,7 @@ def lesser_of_distribution(
exhibit: Exhibit,
) -> list[dict[str, str]]:
"""
Process lesser-of statements withcross-exhibit support.
Process lesser-of statements with cross-exhibit support.
Hybrid approach:
- Intra-exhibit (GLOBAL, CURRENT): Search exhibit_text_simplified to avoid race conditions
@@ -851,36 +851,42 @@ def lesser_of_distribution(
lesser_of_answer_dict = prompt_calls.prompt_lesser_of_check(
service_term, original_reimb_term, exhibit.exhibit_header, filename
)
# Add page number to the classification
lesser_of_answer_dict["page_num"] = page_num
scope = lesser_of_answer_dict["scope"]
target = lesser_of_answer_dict.get("target_exhibit")
exhibit_ref = lesser_of_answer_dict.get("exhibit_reference")
# Store GLOBAL and cross-exhibit templates for use by other exhibits
# GLOBAL: Store for cross-exhibit, don't add to results
if scope == "GLOBAL":
exhibit.add_lesser_of_statement(lesser_of_answer_dict)
logging.info(f"Stored GLOBAL lesser-of template from page {page_num}")
final_reimbursement_level_answers.append(answer_dict)
logging.info(
f"Stored GLOBAL template from page {page_num} - NOT added to results"
)
# EXHIBIT_SPECIFIC (OTHER): Store but exclude from results
# EXHIBIT_SPECIFIC (OTHER): Store for cross-exhibit, don't add to results
elif scope == "EXHIBIT_SPECIFIC" and target == "OTHER":
exhibit.add_lesser_of_statement(lesser_of_answer_dict)
logging.info(
f"Stored cross-exhibit template from page {page_num}"
) # Still excluded (not a rate)
# STANDALONE: Complete rate with both options present
# EXHIBIT_SPECIFIC (CURRENT): Applies to current exhibit (found via text search)
# Both should be kept in final results
else:
logging.debug(
f"Keeping {scope} lesser-of statement on page {page_num}: "
f"{original_reimb_term[:60]}..."
f"Stored cross-exhibit template from page {page_num} - NOT added to results"
)
# EXHIBIT_SPECIFIC (CURRENT): Don't store (race condition), don't add to results
elif scope == "EXHIBIT_SPECIFIC" and target == "CURRENT":
logging.info(
f"CURRENT template from page {page_num} - handled via text search - NOT added to results"
)
# STANDALONE: Complete rate - ADD to results
elif scope == "STANDALONE":
logging.info(f"STANDALONE rate from page {page_num} - ADDED to results")
final_reimbursement_level_answers.append(
answer_dict
) # ← Only this gets added
else:
logging.warning(
f"Unknown scope/target combination: {scope}/{target} - NOT added"
)
final_reimbursement_level_answers.append(answer_dict)
# Branch B: Statement doesn't contain "less" - apply lesser-of if applicable
else:
+117 -61
View File
@@ -437,10 +437,15 @@ REIMB_TERM: Describes the method by which the price of the Service is calculated
- It's ok if the SERVICE_TERM shows up in the REIMB_TERM. Prioritize maintaining complete sentences.
- Do not leave out any relevant information from either the SERVICE_TERM or REIMB_TERM even if it is not present in the vicinity. e.g. answer "$100 per visit" rather than just "$100". If table headers OR table rows provide ANY relevant information (like code type, provider type, hospital name, provider name, Tax ID, legal name, etc), include that in the SERVICE_TERM or REIMB_TERM as appropriate.
- Include all SERVICE_TERM and REIMB_TERM pairs mentioned, even if they are redundant or overlapping. There should be at least one entry for each SERVICE_TERM and REIMB_TERM pair mentioned.
- ALWAYS include all relevant percentages and dollar values. If there's even a slight chance that the number is relevant, include it.
- For each REIMB_TERM, include any additional information mentioned that will be useful in determining reimbursement for the service, e.g., "payment amount will be determined by multiplying it by the DRG weight". It should be appended to relevant REIMB_TERM and not added as a separate entry.
- If different reimbursement percentages or dollar values are mentioned for different effective dates, create separate entries for each effective date.
- For the tables: Extract EVERY ROW as a separate entry, including all line items, subtotals, totals, adjustments, charges, and calculated ratios. When rows involve different providers/entities, create separate entries for EACH provider/entity and include the provider/entity name in the SERVICE_TERM (e.g., "Provider Name Service Category").
- Extract ONE entry per RATE STATEMENT in the contract. A rate statement includes percentages, dollar amounts, weight-based calculations (e.g., DRG weight), or other reimbursement methodologies.
- GROUPING RULE: If multiple services are explicitly listed together (e.g., "Reference Lab and DME") AND share the same rate statement, extract as ONE entry preserving the exact combined SERVICE_TERM. Do not split them.
- SEPARATION RULE: If services appear in separate sentences or have different reimbursement methods, extract as separate entries.
- Examples:
"Lab and Imaging Services: 110% of Medicare" → ONE entry with SERVICE_TERM "Lab and Imaging Services"
"Emergency Services: $500 per visit" → ONE separate entry
"Surgical Services: 150% of Medicare" → ONE separate entry
[EXCLUSIONS]
- NEVER return reimbursement terms that are labeled as Examples or appear in an EXAMPLE section. These are not actual reimbursements and should not be included in the output.
@@ -484,9 +489,12 @@ DECISION LOGIC:
2. FILTER BY SERVICE SCOPE
Keep only statements whose scope matches SERVICE:
- "All services in this exhibit" → applies to any service
- "All services in this exhibit" / "All services" / "All covered services" → applies to any service
- "Inpatient services only" → does NOT apply to outpatient services
- Lesser-of statements containing "listed below", "rates below", "set forth below"
→ applies to ANY service rate that appears after that statement in the exhibit text,
regardless of section headers
If nothing applies → return N/A
3. COMBINE & FORMAT
@@ -568,19 +576,30 @@ def LESSER_OF_CHECK_INSTRUCTION() -> str:
1. **EXHIBIT_SPECIFIC (OTHER)** - References a different specific exhibit
- Check BOTH fields for: "contained in [Exhibit]", "in Attachment [X]", "for [Schedule] services", "described in [Appendix]"
- Extract exact exhibit name → exhibit_reference
- NOTE: External fee schedule references (e.g., "Company's fee schedule", "Payer's fee schedule", "Provider's fee schedule") are reimbursement methodologies, NOT exhibit references
2. **EXHIBIT_SPECIFIC (CURRENT)** - References only this exhibit
- Phrases: "this exhibit", "rates below", "in this schedule"
- Phrases: "this exhibit", "rates below", "listed below", "set forth below", "in this schedule"
- These are templates/preambles that apply to rates defined within the same exhibit
- NOT standalone rates—they reference other rates rather than being complete themselves
3. **STANDALONE** - Complete rate with both options AND a specific service/CPT code
- Both amounts/rates fully specified
- Service is specific (e.g., "MRI", "CPT 70450", "Office Visit Level 3")
- NOT generic terms like "All Services", "Covered Services", "Services"
- No external references
3. **STANDALONE** - A complete, self-contained reimbursement rate
- This IS a rate (a fee schedule entry, a line item)
- Has BOTH service identification AND complete reimbursement methodology
- Answers the question: "How much do we pay for X?"
- Would make sense as one row in a rate table alongside other rates
- Can be specific ("MRI", "CPT 70450") OR broad ("Covered Services", "All Services")
- Even if it says "all services" - if it's functioning as a single rate entry within an exhibit, it's STANDALONE
- No external exhibit/attachment references
- Must NOT contain "listed below", "rates below", "set forth below" (these indicate a template, not a complete rate)
4. **GLOBAL** - Applies to entire contract
- Phrases: "all services under this Agreement", "unless otherwise specified"
- NO specific exhibit/attachment/schedule mentions in EITHER field
4. **GLOBAL** - A contract-wide constraint that governs ALL other rates
- This is a RULE ABOUT rates, not a rate itself
- Applies across the ENTIRE contract (all exhibits, all services, all fee schedules)
- Answers the question: "What ceiling/constraint applies to every payment we make?"
- Would NOT make sense as a row in a rate table - it's a meta-rule governing the table
- Typically found in general terms, master agreement sections, or contract-wide provisions
- NO exhibit/attachment/schedule mentions in EITHER field
**Examples:**
@@ -588,13 +607,13 @@ Service: "services described in Attachment B - Facility Services"
Reimbursement: "payment shall be at lesser of contracted rates or 100% of billed charges"
→ EXHIBIT_SPECIFIC (OTHER), exhibit_reference: "Attachment B - Facility Services"
Service: "services contained in Attachment A - Professional Services"
Reimbursement: "payment shall not exceed 110% Medicare"
EXHIBIT_SPECIFIC (OTHER), exhibit_reference: "Attachment A - Professional Services"
Service: "All services rendered under this Agreement"
Reimbursement: "In no event shall payment exceed the lesser of billed charges or the contracted rate"
GLOBAL - this is a contract-wide ceiling/constraint, not a rate itself; it modifies how all other rates are applied
Service: "All covered services"
Reimbursement: "All services under this Agreement paid at lesser of rates or charges"
→ GLOBAL (no exhibit mentioned in either field)
Service: "All claims submitted to Payer"
Reimbursement: "Notwithstanding any rates set forth in the attached Exhibits, payment shall not exceed the lesser of billed charges or 100% of Medicare"
→ GLOBAL - → GLOBAL - "Notwithstanding...attached Exhibits" indicates this constrains rates defined elsewhere
Service: "Services"
Reimbursement: "Services in this exhibit: lesser of rates below or Medicare"
@@ -602,9 +621,21 @@ Reimbursement: "Services in this exhibit: lesser of rates below or Medicare"
Service: "MRI"
Reimbursement: "lesser of $1000 or Medicare"
→ STANDALONE
→ STANDALONE (specific service + complete rate)
**CRITICAL: If you see ANY exhibit/attachment/schedule name in EITHER the Service OR Reimbursement field, it CANNOT be GLOBAL - it must be EXHIBIT_SPECIFIC (OTHER).**"""
Service: "emergency room services Covered Services"
Reimbursement: "lesser of Allowable Charges or 100% of Medicare fee schedule"
→ STANDALONE (complete rate)
Service: "Medicare Product Covered Services"
Reimbursement: "lesser of Allowable Charges or Company's fee schedule in effect on date of service"
→ STANDALONE - "Company's fee schedule" is an external reimbursement benchmark, not a contract exhibit reference
**CRITICAL GUIDANCE:**
- GLOBAL is rare. Most contracts will have STANDALONE or EXHIBIT_SPECIFIC entries.
- The presence of "all services" or "covered services" does NOT automatically mean GLOBAL.
- GLOBAL must be a constraint that governs OTHER rates across the contract, not a rate itself.
- When in doubt between STANDALONE and GLOBAL, ask: "Is this a rate, or a rule about rates?" If it's a rate, choose STANDALONE."""
def LESSER_OF_CHECK(service_term: str, reimb_term: str, exhibit_title: str):
@@ -1402,62 +1433,87 @@ Header 2:
def VALIDATE_REIMBURSEMENTS_INSTRUCTION() -> str:
"""Static instruction for VALIDATE_REIMBURSEMENTS prompt caching.
Contains all validation rules, criteria, and examples.
"""
return """[OBJECTIVE]
Analyze a REIMBURSEMENT TERM and SERVICE TERM pair from a healthcare contract. Determine if the pair represents a COMPLETE reimbursement methodology that specifies how payment will be calculated.
"""Validates whether a Service Term and Reimbursement Term pair represents a complete reimbursement methodology."""
return f"""Validate this SERVICE TERM and REIMBURSEMENT TERM pair from a healthcare contract.
[VALID ENTRIES - return YES]
- Specific payment rates (percentages, dollar amounts)
- Fee schedule references (including references to named/specific fee schedules without explicit rates)
- Calculation formulas
- Payment limitations with amounts
- "Lesser of" or "greater of" statements with clear and specific rates
## DECISION RULE
Default is NO. Return YES only if ALL three conditions pass:
---
## CONDITION 1: Valid SERVICE TERM
Must identify a billable service, procedure, or care category.
Valid examples: service names, procedure types, CPT/HCPCS codes, care categories
Invalid - return NO if SERVICE TERM is:
- Clinical definitions or level-of-care descriptions (acuity, intervention frequency, co-morbidities)
- Authorization or coverage requirements
- Clean Claims definitions or processing requirements without a payment methodology (e.g., "Clean Claims are claims submitted within 90 days")
- Contract terms, party information, or administrative language
- Administrative tasks or non-clinical services (e.g., medical records copying, administrative fees, filing charges)
- Billing rules or payment logic describing scenarios (e.g., "the procedure for which the Allowed Amount is greatest", "when multiple procedures are performed")
---
## CONDITION 2: Valid REIMBURSEMENT TERM
Must contain a SPECIFIC, CONCRETE payment method you can point to.
Valid payment methods:
- Dollar amounts (e.g., "$150 per visit")
- Percentage rates (e.g., "80% of Medicare allowable")
- Named fee schedule references with specific percentage (e.g., "100% of DMAP fee schedule")
- Payment formulas or limitations with amounts
- "Lesser of" or "greater of" statements with specific rates
- Capitation rates (PMPM, PMPY, per member costs)
- Flat rates per service or episode
- These Special cases: OUTLIER, STOP_LOSS, SEQUESTRATION, DISCOUNT, PREMIUM, RATE_ESCALATOR, FACILITY_ADJUSTMENT
- Calculation formulas with defined components (e.g., "base rate multiplied by DRG weight plus adjustments")
- Special types: OUTLIER, STOP_LOSS, SEQUESTRATION, DISCOUNT, PREMIUM, RATE_ESCALATOR, FACILITY_ADJUSTMENT
IMPORTANT: If a REIMBURSEMENT TERM contains a specific dollar amount or percentage rate, return YES regardless of the program type referenced in the SERVICE TERM. Specific payment amounts always constitute a complete reimbursement methodology.
[INVALID ENTRIES - return NO unless there is VALID language as well]
- 'Empty' Reimbursements - have the same structure as a valid reimbursement but doesn't actually reference any specific rate or fee schedule (e.g. "Covered Services will be paid at the rates stated in the next section")
Invalid - return NO if REIMBURSEMENT TERM is:
- Empty reimbursements: structure of a payment method but actual rate/percentage is not stated or references another location (e.g., "paid at rates in the next section", "the percentage listed below", "per the attached schedule", "as set forth in Exhibit A"). Note: formulas that define calculation methodology (e.g., "base rate multiplied by DRG weight") ARE valid even if specific dollar amounts aren't stated.
- Vague or undefined amounts: uses non-specific language like "appropriate amount", "applicable rate", "as determined", "reasonable charges", "customary charges" without a concrete percentage or dollar figure. A fee schedule reference is only valid if paired with a specific percentage (e.g., "100% of fee schedule" is valid; "appropriate amount under fee schedule" is NOT valid).
- Service definitions without payment terms
- Authorization/coverage requirements only
- Processing instructions, funding arrangements, and administrative statements without specific rates
- Authorization or coverage requirements only
- Processing instructions, funding arrangements, or administrative statements without specific rates
- Member cost responsibilities (including Medicare Member Cost Share)
- Irrelevant information about parties, contract terms, or other non-reimbursement details e.g. 'transfer adjustments'.
- Irrelevant information: parties, contract terms, transfer adjustments, non-reimbursement details
- Billing prohibitions or refund procedures
[HARD INVALID ENTRIES - return NO even if there is VALID reimbursement language present]
- Any reference to Coordination of Benefits or general benefit descriptions
- Any reference to Clean Claims in SERVICE TERM only
- Audit and Time-based language describing how many claims must be paid in a certain timeframe (e.g. "... pay 85% of claims within a 30-day window")
- Risk-sharing arrangements, surplus/deficit distributions, settlement payments
- Performance bonuses and incentive payments, or administrative compensation.
- Non-reimbursement statements ("not covered", "excluded", "not payable")
- Charge Description Master (CDM) or "chargemaster" language.
---
[EXAMPLES]
- "reimbursed at 100% of DMAP fee schedule" → YES (clear payment method)
- "flat rate of $20.00 per frame" → YES (specific rate)
- "$5.58 PMPM" → YES (capitation rate)
- "80% of Medicare allowable" → YES (percentage with reference)
- "Standard eyeglass lenses as defined by DMAP are CR39 lenses..." → NO (definition only)
- "Contact lenses must be prior authorized" → NO (authorization only)
- "Not payable per contract" → NO (no reimbursement)
- "payment to IPA equal to fifty percent (50%) of the Surplus" → NO (risk-sharing distribution)
## CONDITION 3: No Disqualifiers
[OUTPUT FORMAT]
Briefly explain your reasoning, then return YES or NO in |pipes|."""
Return NO if EITHER term contains (even if valid payment method exists):
- Coordination of Benefits (COB)
- Risk-sharing, surplus/deficit distributions, settlement payments
- Performance bonuses, incentives, administrative compensation
- Audit/timeliness metrics (e.g., "pay 85% of claims within 30 days")
- Non-reimbursement statements: "not covered", "not payable", "excluded", "shall not bill", "shall not collect"
- Chargemaster/CDM references
"""
def VALIDATE_REIMBURSEMENTS_PROMPT(service_term: str, reimb_term: str) -> str:
"""Returns ONLY dynamic content for validation.
Call VALIDATE_REIMBURSEMENTS_INSTRUCTION() separately for the cached instruction.
"""
return f"""[CONTEXT]
return f"""## INPUT
SERVICE TERM: {service_term}
REIMBURSEMENT TERM: {reimb_term}"""
REIMBURSEMENT TERM: {reimb_term}
## RESPOND
1. Is SERVICE TERM a valid service identifier?
2. Is REIMBURSEMENT TERM a specific payment method?
3. Any disqualifiers in either term?
If 1=yes, 2=yes, 3=no → return |YES|
Otherwise → return |NO|
Briefly explain your reasoning, then return YES or NO in |pipes|"""
def DATE_FIX_INSTRUCTION() -> str:
+125 -133
View File
@@ -695,50 +695,6 @@ class TestOneToNFuncs(unittest.TestCase):
# ==================== lesser_of_distribution Tests ====================
@patch(
"src.pipelines.shared.extraction.one_to_n_funcs.prompt_calls.prompt_lesser_of_check"
)
def test_lesser_of_distribution_global_stored_AND_in_results(
self, mock_lesser_check
):
"""Test that GLOBAL statements are stored AND kept in final results (UPDATED)."""
mock_exhibit = Mock()
mock_exhibit.exhibit_header = "Exhibit A"
mock_exhibit.get_cross_exhibit_lesser_of.return_value = []
mock_lesser_check.return_value = {
"service_term": "All Services",
"reimb_term": "All services: lesser of rates or charges",
"scope": "GLOBAL",
"target_exhibit": "ALL",
"exhibit_reference": None,
"page_num": "5",
}
input_answers = [
{
"SERVICE_TERM": "All Services",
"REIMB_TERM": "All services: lesser of rates or charges",
}
]
result = one_to_n_funcs.lesser_of_distribution(
input_answers,
"exhibit text",
"5",
self.constants,
self.filename,
mock_exhibit,
)
# Template should be stored
mock_exhibit.add_lesser_of_statement.assert_called_once()
# AND should be in final results (CHANGED from old test)
self.assertEqual(len(result), 1)
self.assertEqual(
result[0]["REIMB_TERM"], "All services: lesser of rates or charges"
)
@patch(
"src.pipelines.shared.extraction.one_to_n_funcs.prompt_calls.prompt_lesser_of_distribution"
)
@@ -789,47 +745,6 @@ class TestOneToNFuncs(unittest.TestCase):
# But NOT in final results
self.assertEqual(len(result), 0)
@patch(
"src.pipelines.shared.extraction.one_to_n_funcs.prompt_calls.prompt_lesser_of_check"
)
def test_lesser_of_distribution_current_kept_in_results(self, mock_lesser_check):
"""Test that EXHIBIT_SPECIFIC (CURRENT) is kept in final results."""
mock_exhibit = Mock()
mock_exhibit.exhibit_header = "Exhibit A"
mock_exhibit.get_cross_exhibit_lesser_of.return_value = []
mock_lesser_check.return_value = {
"service_term": "Services",
"reimb_term": "Services in this exhibit: lesser of rates or charges",
"scope": "EXHIBIT_SPECIFIC",
"target_exhibit": "CURRENT",
"exhibit_reference": None,
"page_num": "10",
}
input_answers = [
{
"SERVICE_TERM": "Services",
"REIMB_TERM": "Services in this exhibit: lesser of rates or charges",
}
]
result = one_to_n_funcs.lesser_of_distribution(
input_answers,
"exhibit text",
"10",
self.constants,
self.filename,
mock_exhibit,
)
# CURRENT should be in final results
self.assertEqual(len(result), 1)
self.assertEqual(
result[0]["REIMB_TERM"],
"Services in this exhibit: lesser of rates or charges",
)
@patch(
"src.pipelines.shared.extraction.one_to_n_funcs.prompt_calls.prompt_lesser_of_check"
)
@@ -975,54 +890,6 @@ class TestOneToNFuncs(unittest.TestCase):
self.assertEqual(len(result), 1)
self.assertEqual(result[0]["REIMB_TERM"], "$100")
@patch(
"src.pipelines.shared.extraction.one_to_n_funcs.prompt_calls.prompt_lesser_of_check"
)
@patch(
"src.pipelines.shared.extraction.one_to_n_funcs.prompt_calls.prompt_lesser_of_distribution"
)
def test_lesser_of_distribution_mixed_statements(
self, mock_lesser_dist, mock_lesser_check
):
"""Test mix of stored templates and applied rates (UPDATED)."""
mock_exhibit = Mock()
mock_exhibit.exhibit_header = "Exhibit A"
mock_exhibit.get_cross_exhibit_lesser_of.return_value = []
# First call: GLOBAL template (stored AND kept in results now)
mock_lesser_check.return_value = {
"service_term": "All Services",
"reimb_term": "All services: lesser of rates or charges",
"scope": "GLOBAL",
"target_exhibit": "ALL",
"exhibit_reference": None,
"page_num": "5",
}
mock_lesser_dist.return_value = "$100" # No change
input_answers = [
{
"SERVICE_TERM": "All Services",
"REIMB_TERM": "All services: lesser of rates or charges",
},
{"SERVICE_TERM": "Lab", "REIMB_TERM": "$100"},
]
result = one_to_n_funcs.lesser_of_distribution(
input_answers,
"exhibit text",
"5",
self.constants,
self.filename,
mock_exhibit,
)
# GLOBAL kept, rate included (CHANGED from 1 to 2)
self.assertEqual(len(result), 2)
self.assertEqual(result[0]["SERVICE_TERM"], "All Services")
self.assertEqual(result[1]["SERVICE_TERM"], "Lab")
@patch(
"src.pipelines.shared.extraction.one_to_n_funcs.prompt_calls.prompt_lesser_of_check"
)
@@ -1063,6 +930,131 @@ class TestOneToNFuncs(unittest.TestCase):
# STANDALONE should be in results
self.assertEqual(len(result), 1)
@patch(
"src.pipelines.shared.extraction.one_to_n_funcs.prompt_calls.prompt_lesser_of_check"
)
def test_lesser_of_distribution_current_not_in_results(self, mock_lesser_check):
"""Test that EXHIBIT_SPECIFIC (CURRENT) is NOT kept in final results - handled via text search."""
mock_exhibit = Mock()
mock_exhibit.exhibit_header = "Exhibit A"
mock_exhibit.get_cross_exhibit_lesser_of.return_value = []
mock_lesser_check.return_value = {
"service_term": "Services",
"reimb_term": "Services in this exhibit: lesser of rates or charges",
"scope": "EXHIBIT_SPECIFIC",
"target_exhibit": "CURRENT",
"exhibit_reference": None,
"page_num": "10",
}
input_answers = [
{
"SERVICE_TERM": "Services",
"REIMB_TERM": "Services in this exhibit: lesser of rates or charges",
}
]
result = one_to_n_funcs.lesser_of_distribution(
input_answers,
"exhibit text",
"10",
self.constants,
self.filename,
mock_exhibit,
)
# CURRENT should NOT be in final results - it's a template handled via text search
self.assertEqual(len(result), 0)
@patch(
"src.pipelines.shared.extraction.one_to_n_funcs.prompt_calls.prompt_lesser_of_check"
)
def test_lesser_of_distribution_global_stored_not_in_results(
self, mock_lesser_check
):
"""Test that GLOBAL statements are stored but NOT kept in final results."""
mock_exhibit = Mock()
mock_exhibit.exhibit_header = "Exhibit A"
mock_exhibit.get_cross_exhibit_lesser_of.return_value = []
mock_lesser_check.return_value = {
"service_term": "All Services",
"reimb_term": "All services: lesser of rates or charges",
"scope": "GLOBAL",
"target_exhibit": "ALL",
"exhibit_reference": None,
"page_num": "5",
}
input_answers = [
{
"SERVICE_TERM": "All Services",
"REIMB_TERM": "All services: lesser of rates or charges",
}
]
result = one_to_n_funcs.lesser_of_distribution(
input_answers,
"exhibit text",
"5",
self.constants,
self.filename,
mock_exhibit,
)
# Template should be stored for cross-exhibit use
mock_exhibit.add_lesser_of_statement.assert_called_once()
# But NOT in final results - it's a template, not a standalone rate
self.assertEqual(len(result), 0)
@patch(
"src.pipelines.shared.extraction.one_to_n_funcs.prompt_calls.prompt_lesser_of_check"
)
@patch(
"src.pipelines.shared.extraction.one_to_n_funcs.prompt_calls.prompt_lesser_of_distribution"
)
def test_lesser_of_distribution_mixed_statements(
self, mock_lesser_dist, mock_lesser_check
):
"""Test mix of stored templates and applied rates."""
mock_exhibit = Mock()
mock_exhibit.exhibit_header = "Exhibit A"
mock_exhibit.get_cross_exhibit_lesser_of.return_value = []
# First call: GLOBAL template (stored but NOT kept in results)
mock_lesser_check.return_value = {
"service_term": "All Services",
"reimb_term": "All services: lesser of rates or charges",
"scope": "GLOBAL",
"target_exhibit": "ALL",
"exhibit_reference": None,
"page_num": "5",
}
mock_lesser_dist.return_value = "$100" # No change
input_answers = [
{
"SERVICE_TERM": "All Services",
"REIMB_TERM": "All services: lesser of rates or charges",
},
{"SERVICE_TERM": "Lab", "REIMB_TERM": "$100"},
]
result = one_to_n_funcs.lesser_of_distribution(
input_answers,
"exhibit text",
"5",
self.constants,
self.filename,
mock_exhibit,
)
# GLOBAL stored but filtered out, only Lab rate remains
self.assertEqual(len(result), 1)
self.assertEqual(result[0]["SERVICE_TERM"], "Lab")
if __name__ == "__main__":
unittest.main()
Generated
-6
View File
@@ -682,7 +682,6 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "black" },
{ name = "isort" },
{ name = "jupyter" },
{ name = "mypy" },
@@ -725,7 +724,6 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "black", specifier = ">=24.10.0" },
{ name = "isort", specifier = ">=5.13.2" },
{ name = "jupyter", specifier = ">=1.1.1" },
{ name = "mypy", specifier = ">=1.12.0" },
@@ -913,7 +911,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" },
{ url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" },
{ url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" },
{ url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" },
{ url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" },
{ url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" },
{ url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" },
@@ -921,7 +918,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" },
{ url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" },
{ url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" },
{ url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" },
{ url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" },
{ url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" },
@@ -929,7 +925,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" },
{ url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" },
{ url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" },
{ url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" },
{ url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" },
{ url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" },
{ url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" },
@@ -937,7 +932,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" },
{ url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" },
{ url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" },
{ url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" },
{ url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" },
{ url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" },
{ url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" },