Merged in feature/code_optimization (pull request #903)
Feature/code optimization
* Ran Black
* made a small change in code_last_check, fixed so it returns string and not single char
* Made changes to make sure that default_ind postprocess only happens to the cc output and not dashboard
* Merge bugfix/default_ind_postprocess into feature/code_optimization
- Parent-child: child_rank column init and cols_to_keep filter
- Postprocess: default_ind only on cc output
- code_last_check: parser str/list return handling + exception logging
- Prompt: FIELD ASSIGNMENT for code extraction (no CRITICAL prefix)
* More prompt changes and debugging print statements
* Added system to group same service term + bill type cd + claim type cd and then parallelize the code extraction (will improve consistency)
* Revert one-time PROV_INFO_JSON ad hoc logic; retain json_utils and output format
- Remove temporary postprocess step that filled empty TIN in PROV_INFO_JSON from
FILENAME_TIN (fill_prov_info_tin_from_filename_tin call in postprocess.py).
This behavior is intended to move upstream per JIRA (tin_npi_funcs / extraction).
- Revert postprocess_existing_output.py to generic config: empty INPUT_DIR,
INPUT_FILENAME, OUTPUT_CSV. Remove one-time hyphen-strip for TIN/NPI in
PROV_INFO_JSON and remove _strip_hyphens_from_prov_info_json_cell.
- Keep json_utils PROV_INFO_JSON helpers (parse_prov_info_json_cell,
serialize_prov_info_json, format_prov_info_json) and (str,str) serialization
in contract_config_postprocess. Keep fill_prov_info_tin_from_filename_tin and
related helpers in postprocessing_funcs for potential upstream reuse.
- Tests in test_json_parsers.py updated for (str,str) PROV_INFO_JSON output.
* Add arbitration-based implicit code flow and prefer Level 2 RAG (Fix 5d, 4c+7)
Implicit flow no longer uses first-match-wins. We build a case from all
applicable strategies and run a single arbitration step to choose the
best candidate.
- build_implicit_candidates: Run Category (when explicit had Category),
Special, and RAG; return list of {source, code_answer_dict}. RAG
result uses preferred level when both return (Fix 5d).
- code_implicit_rag (Fix 5d): Run both Level 1 and Level 2, collect
results; when both return non-empty, prefer Level 2 over Level 1.
- code_implicit_arbitration: 0 candidates -> None; 1 candidate -> return
it with CODE_METHODOLOGY = 'Implicit - Arbitration (Source)' (no LLM);
2+ candidates -> one CODE_IMPLICIT_ARBITRATION LLM call (chosen_index or
no_match). If no candidates or no_match, code_last_check runs as before.
- prompt_templates: Add CODE_IMPLICIT_ARBITRATION and
CODE_IMPLICIT_ARBITRATION_INSTRUCTION (cached). Use
_create_json_dict_parser(field_names=['chose…
* fix: test_code_breakout avoid LLM call and flaky order on CI
- Add BILL_TYPE_CD_DESC to test data so pre-pass skips fill_bill_type (no Bedrock on CI)
- Use side_effect function keyed by SERVICE_TERM for deterministic result under ThreadPoolExecutor
* ran black
* feat: hybrid code validation with unmatched tagging and code quality fixes
Validation and unmatched tagging:
- Add format-only filtering for explicit codes (drop wrong-format, preserve valid)
- Add unified _has_unmatched_codes() used by both explicit and implicit paths
- Tag CODE_METHODOLOGY with "- Unmatched" when codes are format-valid but
not explicit keys in our mappings (codes and range keys count as mapped)
- Remove retry logic and "Explicit - Validation Failed" in favor of single
LLM call with Unmatched tagging
- Include all revenue level mappings in valid revenue set
Code quality and bug fixes:
- Fix fill_bill_type += string bug (was splitting "11X" into chars)
- Fix bare except clauses in code_explicit, code_category,
code_implicit_special, fill_bill_type (now except Exception as e)
- Replace eval() with json.loads() in fill_grouper_cd_desc
- Implement INVALID_SERVICE short-circuit in RAG (Fix 5a)
- Cache valid procedure/revenue code sets per Constants instance
- Store mappings in level_dicts…
* chore: remove debug print statements from code extraction pipeline
Remove all [DEBUG_CODE_EXTRACTION] print statements added during
development of the hybrid validation and implicit arbitration flow.
* refactor: robust PROV_INFO_JSON sanitization with multi-format parsing
Replace the simple format_prov_info_json with a layered
sanitize_prov_info_json that handles all known malformed variants:
single-quoted dicts, empty-value-after-colon patterns, list-typed
field values, and hyphenated TINs. Extract _normalize_prov_entries
and _prov_value_to_str helpers for uniform str-valued output.
format_prov_info_json now delegates to sanitize_prov_info_json.
* merge: sync feature/code_optimization with DEV
Resolve conflict in prompt_templates.py: keep detailed CRITICAL field
assignment instructions from feature branch; accept new
AARETE_DERIVED_PROVIDER_NAME prompt function from DEV.
* Minor change to explict prompt
* fix: resolve mypy errors and remove stale pipe-delim splits in RAG
- Narrow grouper_cd type to str|None with explicit None check to
satisfy mypy in fill_grouper_cd_desc.
- Remove dead pipe-delim split logic from CPT and HCPCS matching
blocks (no pipes in those mappings).
- Retain pipe split for revenue block only (rev_level1.csv keys are
pipe-delimited, e.g. "0810|0811|0812").
* Code extraction: format-only explicit, keep unmapped tag, RAG Level 1/2 as separate candidates
- Explicit: accept all format-valid codes (single + range); do not drop for mapping.
Validity check only appends "- Unmatched" to methodology when code not in mappings.
- Add _procedure_code_or_range_format_valid; use in validate_explicit_codes and _has_unmatched_codes.
- RAG: return list of Level 1 and Level 2 candidates (distinct source keys); remove Level 2 preference.
- build_implicit_candidates: consume RAG list and append each as separate candidate.
- CODE_EXPLICIT: clarify range extraction when service term describes a range.
- CODE_IMPLICIT_ARBITRATION: choose most appropriate candidate(s), avoid overly broad.
- Tests updated for new RAG return shape and validate_explicit_codes behavior.
* Resolved Merge Conflicts
* Fixed failing tests
* Remove unused functions
* Remove unused functions
* Revert "Remove unused functions"
This reverts commit 39954db6a421e55501282d4e6a270c88624049fe.
* remove unused
* Merged DEV into feature/code_optimization
* Merged DEV into feature/code_optimization
Approved-by: Katon Minhas
This commit is contained in:
committed by
Katon Minhas
parent
ea1f4a2c8c
commit
863c9db9a1
+602
-27
@@ -4,7 +4,9 @@ import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pandas as pd
|
||||
|
||||
import src.codes.code_funcs as code_funcs
|
||||
import src.prompts.prompt_templates as prompt_templates
|
||||
from src.constants.constants import Constants
|
||||
from src.constants.delimiters import Delimiter
|
||||
|
||||
@@ -13,7 +15,10 @@ class TestCodeFuncs(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures before each test method."""
|
||||
# Create a mock Constants object
|
||||
# Clear the module-level valid code set caches between tests.
|
||||
code_funcs._VALID_PROC_CACHE.clear()
|
||||
code_funcs._VALID_REV_CACHE.clear()
|
||||
|
||||
self.constants = MagicMock(spec=Constants)
|
||||
|
||||
# Configure mock constants
|
||||
@@ -32,7 +37,16 @@ class TestCodeFuncs(unittest.TestCase):
|
||||
|
||||
self.constants.CPT_LEVEL2_MAPPING = {"67890": "Advanced Procedure"}
|
||||
self.constants.HCPCS_LEVEL2_MAPPING = {"J0002": "Advanced Drug"}
|
||||
self.constants.REV_MAPPING = {"456": "Advanced Revenue"}
|
||||
self.constants.REV_MAPPING = {"0456": "Advanced Revenue"}
|
||||
# Full mappings for validation and description fill (Fix 1 / Fix 2)
|
||||
self.constants.CPT_FULL_MAPPING = {
|
||||
"12345": "Test Procedure",
|
||||
"99213": "Office visit, established patient",
|
||||
}
|
||||
self.constants.HCPCS_FULL_MAPPING = {
|
||||
"J0001": "Test Drug",
|
||||
"G0525": "Care management",
|
||||
}
|
||||
|
||||
# Bill type mappings matching the actual JSON structure
|
||||
self.constants.BILL_TYPE_MAPPING = {
|
||||
@@ -56,6 +70,10 @@ class TestCodeFuncs(unittest.TestCase):
|
||||
mock_encoded.astype.return_value = "mock_vector"
|
||||
self.constants.EMBEDDING_MODEL.encode.return_value = mock_encoded
|
||||
|
||||
def tearDown(self):
|
||||
"""Clear prompt_templates constants cache so a MagicMock is not reused by other tests."""
|
||||
prompt_templates.set_constants_for_instruction_cache(None)
|
||||
|
||||
def test_clean_service(self):
|
||||
"""Test the clean_service function with various inputs."""
|
||||
# Test empty input
|
||||
@@ -229,22 +247,76 @@ class TestCodeFuncs(unittest.TestCase):
|
||||
"TEST SERVICE", implicit_run_dict, "test.pdf", self.constants
|
||||
)
|
||||
|
||||
# Verify results - code_implicit_rag returns lists
|
||||
self.assertEqual(result["PROCEDURE_CD"], ["12345"])
|
||||
self.assertEqual(result["PROCEDURE_CD_DESC"], ["Test Procedure"])
|
||||
self.assertEqual(result["CODE_METHODOLOGY"], "Implicit - Level 1")
|
||||
# Verify results - code_implicit_rag returns list of candidates with source Level 1/Level 2
|
||||
self.assertIsInstance(result, list)
|
||||
self.assertGreaterEqual(len(result), 1)
|
||||
codes_found = False
|
||||
for item in result:
|
||||
self.assertIn("source", item)
|
||||
self.assertIn("code_answer_dict", item)
|
||||
d = item["code_answer_dict"]
|
||||
if d.get("PROCEDURE_CD") == ["12345"]:
|
||||
codes_found = True
|
||||
self.assertEqual(d["PROCEDURE_CD_DESC"], ["Test Procedure"])
|
||||
break
|
||||
self.assertTrue(
|
||||
codes_found, "Expected PROCEDURE_CD ['12345'] in a candidate"
|
||||
)
|
||||
|
||||
# Test exception handling
|
||||
# Test exception handling - returns list (possibly empty)
|
||||
mock_json_load.side_effect = Exception("Test error")
|
||||
result = code_funcs.code_implicit_rag(
|
||||
"TEST SERVICE", implicit_run_dict, "test.pdf", self.constants
|
||||
)
|
||||
self.assertIn("CODE_METHODOLOGY", result)
|
||||
self.assertIsInstance(result, list)
|
||||
|
||||
@patch("src.utils.llm_utils.invoke_claude")
|
||||
@patch("src.utils.string_utils.universal_json_load")
|
||||
@patch("src.codes.code_funcs.get_match_list")
|
||||
def test_code_implicit_rag_invalid_service_short_circuits(
|
||||
self, mock_get_match_list, mock_json_load, mock_invoke_claude
|
||||
):
|
||||
"""INVALID_SERVICE in RAG response is a hard stop: no codes added."""
|
||||
mock_get_match_list.return_value = (["Test Procedure"], 0.9)
|
||||
mock_invoke_claude.return_value = '["INVALID_SERVICE", "Test Procedure"]'
|
||||
mock_json_load.return_value = ["INVALID_SERVICE", "Test Procedure"]
|
||||
implicit_run_dict = {"PROCEDURE_CD": True, "REVENUE_CD": True}
|
||||
|
||||
with patch(
|
||||
"src.codes.code_funcs.get_embedding_levels"
|
||||
) as mock_get_embedding_levels:
|
||||
mock_get_embedding_levels.return_value = (
|
||||
["cpt_level1"],
|
||||
{"12345": "Test Procedure"},
|
||||
{},
|
||||
{},
|
||||
)
|
||||
result = code_funcs.code_implicit_rag(
|
||||
"TEST SERVICE", implicit_run_dict, "test.pdf", self.constants
|
||||
)
|
||||
|
||||
# INVALID_SERVICE yields no candidates with codes
|
||||
self.assertIsInstance(result, list)
|
||||
self.assertEqual(len(result), 0)
|
||||
|
||||
@patch("src.utils.llm_utils.invoke_claude")
|
||||
def test_fill_bill_type_list_reverse_mapping(self, mock_invoke_claude):
|
||||
"""fill_bill_type handles list values in BILL_TYPE_REVERSE_MAPPING."""
|
||||
mock_invoke_claude.return_value = '["Inpatient Hospital"]'
|
||||
reverse_mapping = {"Inpatient Hospital": ["11X", "12X"]}
|
||||
answer_dict = {}
|
||||
result = code_funcs.fill_bill_type(
|
||||
"INPATIENT SERVICE",
|
||||
answer_dict,
|
||||
self.constants.BILL_TYPE_MAPPING,
|
||||
reverse_mapping,
|
||||
)
|
||||
self.assertEqual(result["BILL_TYPE_CD"], ["11X", "12X"])
|
||||
|
||||
@patch("src.utils.llm_utils.invoke_claude")
|
||||
def test_code_last_check(self, mock_invoke_claude):
|
||||
"""Test the code_last_check function for categorizing non-matched services."""
|
||||
# Setup mocks - parser returns a list, function extracts first element
|
||||
# Parser is format-aware and returns 'Specific' or 'Generic' directly.
|
||||
# Test specific case
|
||||
mock_invoke_claude.return_value = '["Specific"]'
|
||||
result = code_funcs.code_last_check("SPECIAL SERVICE", "test.pdf")
|
||||
@@ -255,10 +327,218 @@ class TestCodeFuncs(unittest.TestCase):
|
||||
result = code_funcs.code_last_check("GENERIC SERVICE", "test.pdf")
|
||||
self.assertEqual(result, "Generic")
|
||||
|
||||
def test_format_implicit_candidate_for_prompt(self):
|
||||
"""Test _format_implicit_candidate_for_prompt formats a candidate for arbitration."""
|
||||
candidate = {
|
||||
"source": "Category",
|
||||
"code_answer_dict": {
|
||||
"PROCEDURE_CD": ["J0001", "J0002"],
|
||||
"PROCEDURE_CD_DESC": ["Drug A", "Drug B"],
|
||||
},
|
||||
}
|
||||
result = code_funcs._format_implicit_candidate_for_prompt(candidate, 0)
|
||||
self.assertIn("Candidate 0 (Source: Category)", result)
|
||||
self.assertIn("PROCEDURE_CD", result)
|
||||
self.assertIn("J0001", result)
|
||||
|
||||
@patch("src.codes.code_funcs.code_implicit_rag")
|
||||
@patch("src.codes.code_funcs.code_implicit_special")
|
||||
@patch("src.codes.code_funcs.code_category")
|
||||
def test_build_implicit_candidates_empty_when_none_return(
|
||||
self, mock_category, mock_special, mock_rag
|
||||
):
|
||||
"""Test build_implicit_candidates returns empty list when no strategy returns."""
|
||||
mock_category.return_value = {}
|
||||
mock_special.return_value = {}
|
||||
mock_rag.return_value = [] # RAG returns list of candidates
|
||||
implicit_run_dict = {"PROCEDURE_CD": True, "REVENUE_CD": True}
|
||||
result = code_funcs.build_implicit_candidates(
|
||||
"SERVICE",
|
||||
{},
|
||||
implicit_run_dict,
|
||||
"test.pdf",
|
||||
self.constants,
|
||||
)
|
||||
self.assertEqual(result, [])
|
||||
|
||||
@patch("src.codes.code_funcs.code_implicit_rag")
|
||||
@patch("src.codes.code_funcs.code_implicit_special")
|
||||
@patch("src.codes.code_funcs.code_category")
|
||||
def test_build_implicit_candidates_category_when_explicit_has_category(
|
||||
self, mock_category, mock_special, mock_rag
|
||||
):
|
||||
"""Test build_implicit_candidates runs category and adds candidate when explicit had Category."""
|
||||
mock_category.return_value = {
|
||||
"PROCEDURE_CD": ["J0001"],
|
||||
"PROCEDURE_CD_DESC": ["Drug A"],
|
||||
}
|
||||
mock_special.return_value = {}
|
||||
mock_rag.return_value = [] # RAG returns list of candidates
|
||||
implicit_run_dict = {"PROCEDURE_CD": True, "REVENUE_CD": True}
|
||||
code_after_explicit = {"PROCEDURE_CD": ["Category: J"]}
|
||||
result = code_funcs.build_implicit_candidates(
|
||||
"J CODES",
|
||||
code_after_explicit,
|
||||
implicit_run_dict,
|
||||
"test.pdf",
|
||||
self.constants,
|
||||
)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0]["source"], "Category")
|
||||
self.assertEqual(result[0]["code_answer_dict"]["PROCEDURE_CD"], ["J0001"])
|
||||
mock_category.assert_called_once()
|
||||
|
||||
@patch("src.codes.code_funcs.code_implicit_rag")
|
||||
@patch("src.codes.code_funcs.code_implicit_special")
|
||||
@patch("src.codes.code_funcs.code_category")
|
||||
def test_build_implicit_candidates_skips_category_without_category_in_explicit(
|
||||
self, mock_category, mock_special, mock_rag
|
||||
):
|
||||
"""Test build_implicit_candidates does not run category when explicit has no Category."""
|
||||
mock_special.return_value = {
|
||||
"PROCEDURE_CD": ["J0000-J9999"],
|
||||
"PROCEDURE_CD_DESC": ["Drugs"],
|
||||
}
|
||||
mock_rag.return_value = [] # RAG returns list of candidates
|
||||
implicit_run_dict = {"PROCEDURE_CD": True, "REVENUE_CD": True}
|
||||
result = code_funcs.build_implicit_candidates(
|
||||
"DRUG SERVICE",
|
||||
{},
|
||||
implicit_run_dict,
|
||||
"test.pdf",
|
||||
self.constants,
|
||||
)
|
||||
mock_category.assert_not_called()
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0]["source"], "Special")
|
||||
|
||||
@patch("src.codes.code_funcs.code_implicit_rag")
|
||||
@patch("src.codes.code_funcs.code_implicit_special")
|
||||
@patch("src.codes.code_funcs.code_category")
|
||||
def test_build_implicit_candidates_collects_category_special_rag(
|
||||
self, mock_category, mock_special, mock_rag
|
||||
):
|
||||
"""Test build_implicit_candidates collects candidates from all three when all return."""
|
||||
mock_category.return_value = {
|
||||
"PROCEDURE_CD": ["A0000-A9999"],
|
||||
"PROCEDURE_CD_DESC": ["A range"],
|
||||
}
|
||||
mock_special.return_value = {
|
||||
"PROCEDURE_CD": ["J0000-J9999"],
|
||||
"PROCEDURE_CD_DESC": ["Drugs"],
|
||||
}
|
||||
mock_rag.return_value = [
|
||||
{
|
||||
"source": "Level 2",
|
||||
"code_answer_dict": {
|
||||
"PROCEDURE_CD": ["99213"],
|
||||
"PROCEDURE_CD_DESC": ["Office visit"],
|
||||
"CODE_METHODOLOGY": "Implicit - Level 2",
|
||||
},
|
||||
},
|
||||
]
|
||||
implicit_run_dict = {"PROCEDURE_CD": True, "REVENUE_CD": True}
|
||||
code_after_explicit = {"PROCEDURE_CD": ["Category: A"]}
|
||||
result = code_funcs.build_implicit_candidates(
|
||||
"SERVICE",
|
||||
code_after_explicit,
|
||||
implicit_run_dict,
|
||||
"test.pdf",
|
||||
self.constants,
|
||||
)
|
||||
self.assertEqual(len(result), 3)
|
||||
sources = [c["source"] for c in result]
|
||||
self.assertIn("Category", sources)
|
||||
self.assertIn("Special", sources)
|
||||
self.assertIn("Level 2", sources)
|
||||
|
||||
def test_code_implicit_arbitration_zero_candidates_returns_none(self):
|
||||
"""Test code_implicit_arbitration returns None when candidates list is empty."""
|
||||
result = code_funcs.code_implicit_arbitration("SERVICE", [], "test.pdf")
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_code_implicit_arbitration_single_candidate_returns_it_no_llm(self):
|
||||
"""Test code_implicit_arbitration with one candidate returns it without calling LLM."""
|
||||
candidates = [
|
||||
{
|
||||
"source": "Category",
|
||||
"code_answer_dict": {
|
||||
"PROCEDURE_CD": ["J0001"],
|
||||
"PROCEDURE_CD_DESC": ["Drug A"],
|
||||
},
|
||||
}
|
||||
]
|
||||
result = code_funcs.code_implicit_arbitration("J CODES", candidates, "test.pdf")
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["PROCEDURE_CD"], ["J0001"])
|
||||
self.assertEqual(
|
||||
result["CODE_METHODOLOGY"],
|
||||
"Implicit - Arbitration (Category)",
|
||||
)
|
||||
|
||||
@patch("src.utils.llm_utils.invoke_claude")
|
||||
def test_code_implicit_arbitration_multiple_candidates_uses_chosen_index(
|
||||
self, mock_invoke_claude
|
||||
):
|
||||
"""Test code_implicit_arbitration with 2+ candidates calls LLM and returns chosen candidate."""
|
||||
mock_invoke_claude.return_value = '{"chosen_index": 1}'
|
||||
candidates = [
|
||||
{
|
||||
"source": "Category",
|
||||
"code_answer_dict": {"PROCEDURE_CD": ["A0000-A9999"]},
|
||||
},
|
||||
{"source": "Level 2", "code_answer_dict": {"PROCEDURE_CD": ["99213"]}},
|
||||
]
|
||||
result = code_funcs.code_implicit_arbitration(
|
||||
"Office visit", candidates, "test.pdf"
|
||||
)
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["PROCEDURE_CD"], ["99213"])
|
||||
self.assertEqual(
|
||||
result["CODE_METHODOLOGY"],
|
||||
"Implicit - Arbitration (Level 2)",
|
||||
)
|
||||
mock_invoke_claude.assert_called_once()
|
||||
|
||||
@patch("src.utils.llm_utils.invoke_claude")
|
||||
def test_code_implicit_arbitration_no_match_returns_none(self, mock_invoke_claude):
|
||||
"""Test code_implicit_arbitration returns None when LLM returns no_match."""
|
||||
mock_invoke_claude.return_value = '{"no_match": true}'
|
||||
candidates = [
|
||||
{
|
||||
"source": "Category",
|
||||
"code_answer_dict": {"PROCEDURE_CD": ["A0000-A9999"]},
|
||||
},
|
||||
{
|
||||
"source": "Special",
|
||||
"code_answer_dict": {"PROCEDURE_CD": ["10004-69990"]},
|
||||
},
|
||||
]
|
||||
result = code_funcs.code_implicit_arbitration(
|
||||
"Unclear service", candidates, "test.pdf"
|
||||
)
|
||||
self.assertIsNone(result)
|
||||
|
||||
@patch("src.utils.llm_utils.invoke_claude")
|
||||
def test_code_implicit_arbitration_invalid_index_fallback_to_zero(
|
||||
self, mock_invoke_claude
|
||||
):
|
||||
"""Test code_implicit_arbitration falls back to index 0 when chosen_index out of range."""
|
||||
mock_invoke_claude.return_value = '{"chosen_index": 99}'
|
||||
candidates = [
|
||||
{"source": "Category", "code_answer_dict": {"PROCEDURE_CD": ["J0001"]}},
|
||||
]
|
||||
result = code_funcs.code_implicit_arbitration("SERVICE", candidates, "test.pdf")
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["PROCEDURE_CD"], ["J0001"])
|
||||
self.assertEqual(
|
||||
result["CODE_METHODOLOGY"],
|
||||
"Implicit - Arbitration (Category)",
|
||||
)
|
||||
|
||||
@patch("src.utils.llm_utils.invoke_claude")
|
||||
def test_fill_bill_type(self, mock_invoke_claude):
|
||||
"""Test the fill_bill_type function for populating bill type information."""
|
||||
# Setup mocks - parser returns a list
|
||||
mock_invoke_claude.return_value = '["Inpatient Hospital"]'
|
||||
|
||||
# Test with valid bill type
|
||||
@@ -353,7 +633,7 @@ class TestCodeFuncs(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(result["PROCEDURE_CD_DESC"], "UNLISTED")
|
||||
|
||||
# Test category match
|
||||
# Test category match (arbitration with single candidate returns it)
|
||||
mock_clean_service.return_value = "CLEAN SERVICE"
|
||||
mock_explicit.return_value = {"PROCEDURE_CD": ["Category: J"]}
|
||||
mock_category.return_value = {
|
||||
@@ -363,9 +643,11 @@ class TestCodeFuncs(unittest.TestCase):
|
||||
result = code_funcs.extract_codes_from_service(
|
||||
{"SERVICE_TERM": "J CODES"}, self.constants
|
||||
)
|
||||
self.assertEqual(result["CODE_METHODOLOGY"], "Implicit - Letter Category")
|
||||
self.assertEqual(
|
||||
result["CODE_METHODOLOGY"], "Implicit - Arbitration (Category)"
|
||||
)
|
||||
|
||||
# Test special case match
|
||||
# Test special case match (arbitration with single candidate)
|
||||
mock_explicit.return_value = {}
|
||||
mock_implicit_special.return_value = {
|
||||
"PROCEDURE_CD": "J0000-J9999",
|
||||
@@ -374,18 +656,25 @@ class TestCodeFuncs(unittest.TestCase):
|
||||
result = code_funcs.extract_codes_from_service(
|
||||
{"SERVICE_TERM": "DRUG SERVICE"}, self.constants
|
||||
)
|
||||
self.assertEqual(result["CODE_METHODOLOGY"], "Implicit - Special Case")
|
||||
# Range J0000-J9999 not in mock mapping so methodology gets - Unmatched
|
||||
self.assertIn("Implicit - Arbitration (Special)", result["CODE_METHODOLOGY"])
|
||||
self.assertIn("Unmatched", result["CODE_METHODOLOGY"])
|
||||
|
||||
# Test RAG match
|
||||
# Test RAG match (arbitration with single candidate; RAG returns list with Level 1)
|
||||
mock_implicit_special.return_value = {}
|
||||
mock_implicit_rag.return_value = {
|
||||
"PROCEDURE_CD": "12345",
|
||||
"CODE_METHODOLOGY": "Implicit - Level 1",
|
||||
}
|
||||
mock_implicit_rag.return_value = [
|
||||
{
|
||||
"source": "Level 1",
|
||||
"code_answer_dict": {
|
||||
"PROCEDURE_CD": "12345",
|
||||
"CODE_METHODOLOGY": "Implicit - Level 1",
|
||||
},
|
||||
},
|
||||
]
|
||||
result = code_funcs.extract_codes_from_service(
|
||||
{"SERVICE_TERM": "TEST SERVICE"}, self.constants
|
||||
)
|
||||
self.assertEqual(result["CODE_METHODOLOGY"], "Implicit - Level 1")
|
||||
self.assertEqual(result["CODE_METHODOLOGY"], "Implicit - Arbitration (Level 1)")
|
||||
|
||||
# Test no match - specific
|
||||
mock_implicit_rag.return_value = {}
|
||||
@@ -402,19 +691,305 @@ class TestCodeFuncs(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(result["CODE_METHODOLOGY"], "Generic - No Match")
|
||||
|
||||
def test_code_in_range(self):
|
||||
"""Test _code_in_range for numeric and alphanumeric range keys."""
|
||||
self.assertTrue(code_funcs._code_in_range("00100-01999", "01234"))
|
||||
self.assertTrue(code_funcs._code_in_range("00100-01999", "00100"))
|
||||
self.assertTrue(code_funcs._code_in_range("00100-01999", "01999"))
|
||||
self.assertFalse(code_funcs._code_in_range("00100-01999", "99999"))
|
||||
self.assertFalse(code_funcs._code_in_range("00100-01999", "02000"))
|
||||
self.assertTrue(code_funcs._code_in_range("A0021-A0999", "A0500"))
|
||||
self.assertFalse(code_funcs._code_in_range("A0021-A0999", "B0001"))
|
||||
self.assertFalse(code_funcs._code_in_range("", "12345"))
|
||||
self.assertFalse(code_funcs._code_in_range("00100-01999", ""))
|
||||
|
||||
def test_lookup_procedure_description(self):
|
||||
"""Test _lookup_procedure_description: exact match and range match."""
|
||||
desc = code_funcs._lookup_procedure_description("12345", self.constants)
|
||||
self.assertEqual(desc, "Test Procedure")
|
||||
desc = code_funcs._lookup_procedure_description("99213", self.constants)
|
||||
self.assertEqual(desc, "Office visit, established patient")
|
||||
desc = code_funcs._lookup_procedure_description(
|
||||
"NOT_ESTABLISHED", self.constants
|
||||
)
|
||||
self.assertEqual(desc, "")
|
||||
desc = code_funcs._lookup_procedure_description("UNMAPPED99", self.constants)
|
||||
self.assertEqual(desc, "")
|
||||
|
||||
def test_lookup_revenue_description(self):
|
||||
"""Test _lookup_revenue_description."""
|
||||
desc = code_funcs._lookup_revenue_description("0456", self.constants)
|
||||
self.assertEqual(desc, "Advanced Revenue")
|
||||
desc = code_funcs._lookup_revenue_description("9999", self.constants)
|
||||
self.assertEqual(desc, "")
|
||||
|
||||
def test_fill_code_descriptions_from_mappings(self):
|
||||
"""Test fill_code_descriptions_from_mappings fills _DESC from mappings."""
|
||||
answer_dict = {
|
||||
"PROCEDURE_CD": ["12345", "99213"],
|
||||
"REVENUE_CD": ["0456"],
|
||||
}
|
||||
code_funcs.fill_code_descriptions_from_mappings(answer_dict, self.constants)
|
||||
self.assertEqual(
|
||||
answer_dict["PROCEDURE_CD_DESC"],
|
||||
["Test Procedure", "Office visit, established patient"],
|
||||
)
|
||||
self.assertEqual(answer_dict["REVENUE_CD_DESC"], ["Advanced Revenue"])
|
||||
|
||||
def test_fill_code_descriptions_leave_unmapped_unchanged(self):
|
||||
"""Test fill leaves _DESC empty for unmapped codes and NOT_ESTABLISHED."""
|
||||
answer_dict = {
|
||||
"PROCEDURE_CD": ["12345", "NOT_ESTABLISHED", "UNMAPPED99"],
|
||||
"PROCEDURE_CD_DESC": ["", "", ""],
|
||||
}
|
||||
code_funcs.fill_code_descriptions_from_mappings(answer_dict, self.constants)
|
||||
self.assertEqual(answer_dict["PROCEDURE_CD_DESC"][0], "Test Procedure")
|
||||
self.assertEqual(answer_dict["PROCEDURE_CD_DESC"][1], "")
|
||||
self.assertEqual(answer_dict["PROCEDURE_CD_DESC"][2], "")
|
||||
|
||||
def test_validate_explicit_codes_keeps_valid_filters_invalid(self):
|
||||
"""Format-invalid codes dropped; format-valid preserved; has_unmatched if not in mapping."""
|
||||
code_answer_dict = {
|
||||
"PROCEDURE_CD": ["12345", "99213", "garbage7"],
|
||||
"REVENUE_CD": ["0456", "9999"],
|
||||
}
|
||||
filtered, has_unmatched = code_funcs.validate_explicit_codes(
|
||||
code_answer_dict, self.constants
|
||||
)
|
||||
self.assertTrue(has_unmatched)
|
||||
self.assertEqual(filtered["PROCEDURE_CD"], ["12345", "99213"])
|
||||
self.assertEqual(filtered["REVENUE_CD"], ["0456", "9999"])
|
||||
|
||||
def test_validate_explicit_codes_keeps_category_and_not_established(self):
|
||||
"""Test validate_explicit_codes keeps Category: and NOT_ESTABLISHED; no unmatched."""
|
||||
code_answer_dict = {"PROCEDURE_CD": ["Category: J", "NOT_ESTABLISHED"]}
|
||||
filtered, has_unmatched = code_funcs.validate_explicit_codes(
|
||||
code_answer_dict, self.constants
|
||||
)
|
||||
self.assertFalse(has_unmatched)
|
||||
self.assertEqual(filtered["PROCEDURE_CD"], ["Category: J", "NOT_ESTABLISHED"])
|
||||
|
||||
def test_validate_explicit_codes_all_valid(self):
|
||||
"""Test validate_explicit_codes returns has_unmatched False when all in mapping."""
|
||||
code_answer_dict = {"PROCEDURE_CD": ["12345"], "REVENUE_CD": ["0456"]}
|
||||
filtered, has_unmatched = code_funcs.validate_explicit_codes(
|
||||
code_answer_dict, self.constants
|
||||
)
|
||||
self.assertFalse(has_unmatched)
|
||||
self.assertEqual(filtered["PROCEDURE_CD"], ["12345"])
|
||||
self.assertEqual(filtered["REVENUE_CD"], ["0456"])
|
||||
|
||||
def test_validate_explicit_codes_multiple_and_range(self):
|
||||
"""Range key that is an explicit key in mapping is kept and counts as mapped."""
|
||||
self.constants.CPT_LEVEL1_MAPPING["00100-01999"] = "Anesthesia"
|
||||
code_answer_dict = {
|
||||
"PROCEDURE_CD": ["12345", "99213", "00100-01999"],
|
||||
"REVENUE_CD": ["0456"],
|
||||
}
|
||||
filtered, has_unmatched = code_funcs.validate_explicit_codes(
|
||||
code_answer_dict, self.constants
|
||||
)
|
||||
self.assertFalse(has_unmatched)
|
||||
self.assertEqual(filtered["PROCEDURE_CD"], ["12345", "99213", "00100-01999"])
|
||||
self.assertEqual(filtered["REVENUE_CD"], ["0456"])
|
||||
|
||||
def test_validate_explicit_codes_range_not_in_mapping_kept_tagged_unmatched(self):
|
||||
"""Format-valid range not in mapping is kept; has_unmatched True for tag."""
|
||||
# Mock has 99213 in CPT_FULL_MAPPING but not 10021-69990; range is kept and tagged
|
||||
code_answer_dict = {
|
||||
"PROCEDURE_CD": ["99213", "10021-69990"],
|
||||
"REVENUE_CD": ["0456"],
|
||||
}
|
||||
filtered, has_unmatched = code_funcs.validate_explicit_codes(
|
||||
code_answer_dict, self.constants
|
||||
)
|
||||
self.assertTrue(has_unmatched)
|
||||
self.assertEqual(filtered["PROCEDURE_CD"], ["99213", "10021-69990"])
|
||||
self.assertEqual(filtered["REVENUE_CD"], ["0456"])
|
||||
|
||||
def test_validate_explicit_codes_single_code_unmapped(self):
|
||||
"""Format-valid code not in exact mapping is preserved and has_unmatched True."""
|
||||
code_answer_dict = {"PROCEDURE_CD": ["01234"], "REVENUE_CD": []}
|
||||
filtered, has_unmatched = code_funcs.validate_explicit_codes(
|
||||
code_answer_dict, self.constants
|
||||
)
|
||||
self.assertTrue(has_unmatched)
|
||||
self.assertEqual(filtered["PROCEDURE_CD"], ["01234"])
|
||||
|
||||
def test_validate_explicit_codes_rejects_icd10_pcs_in_procedure(self):
|
||||
"""ICD-10-PCS (7-char) filtered from PROCEDURE_CD; CPT/HCPCS format kept; S2900 in mapping."""
|
||||
self.constants.HCPCS_FULL_MAPPING["S2900"] = "Robotic surgical system"
|
||||
code_answer_dict = {
|
||||
"PROCEDURE_CD": ["0E0V0CZ", "0E0W0CZ", "S2900"],
|
||||
"REVENUE_CD": [],
|
||||
}
|
||||
filtered, has_unmatched = code_funcs.validate_explicit_codes(
|
||||
code_answer_dict, self.constants
|
||||
)
|
||||
self.assertFalse(has_unmatched)
|
||||
self.assertEqual(filtered["PROCEDURE_CD"], ["S2900"])
|
||||
|
||||
def test_has_unmatched_codes_true_for_unmapped_procedure(self):
|
||||
"""_has_unmatched_codes returns True when procedure code is format-valid but not in mapping."""
|
||||
d = {"PROCEDURE_CD": ["99999"], "REVENUE_CD": []}
|
||||
self.assertTrue(code_funcs._has_unmatched_codes(d, self.constants))
|
||||
|
||||
def test_has_unmatched_codes_true_for_unmapped_revenue(self):
|
||||
"""_has_unmatched_codes returns True when revenue code is format-valid but not in mapping."""
|
||||
d = {"PROCEDURE_CD": [], "REVENUE_CD": ["9999"]}
|
||||
self.assertTrue(code_funcs._has_unmatched_codes(d, self.constants))
|
||||
|
||||
def test_has_unmatched_codes_false_when_all_mapped(self):
|
||||
"""_has_unmatched_codes returns False when all procedure/revenue codes are in mapping."""
|
||||
d = {"PROCEDURE_CD": ["99213"], "REVENUE_CD": ["0456"]}
|
||||
self.assertFalse(code_funcs._has_unmatched_codes(d, self.constants))
|
||||
|
||||
@patch("src.codes.code_funcs.code_explicit")
|
||||
def test_extract_codes_explicit_unmatched(self, mock_explicit):
|
||||
"""Explicit path sets Unmatched when any format-valid code not in mapping; no retry."""
|
||||
mock_explicit.return_value = {
|
||||
"PROCEDURE_CD": ["12345", "99999"],
|
||||
"REVENUE_CD": [],
|
||||
}
|
||||
with patch("src.codes.code_funcs.clean_service", return_value="CLEAN"):
|
||||
with patch("src.codes.code_funcs.fill_bill_type", return_value={}):
|
||||
with patch(
|
||||
"src.codes.code_funcs.get_implicit_runs",
|
||||
return_value={"PROCEDURE_CD": True, "REVENUE_CD": True},
|
||||
):
|
||||
result = code_funcs.extract_codes_from_service(
|
||||
{"SERVICE_TERM": "TEST"}, self.constants
|
||||
)
|
||||
self.assertEqual(result["CODE_METHODOLOGY"], "Explicit - Unmatched")
|
||||
self.assertIn("12345", str(result["PROCEDURE_CD"]))
|
||||
self.assertIn("99999", str(result["PROCEDURE_CD"]))
|
||||
self.assertEqual(mock_explicit.call_count, 1)
|
||||
|
||||
@patch("src.codes.code_funcs.code_explicit")
|
||||
def test_extract_codes_explicit_description_filled(self, mock_explicit):
|
||||
"""Test explicit path fills PROCEDURE_CD_DESC from mapping."""
|
||||
mock_explicit.return_value = {
|
||||
"PROCEDURE_CD": ["99213"],
|
||||
"REVENUE_CD": ["0456"],
|
||||
}
|
||||
with patch("src.codes.code_funcs.clean_service", return_value="CLEAN"):
|
||||
with patch("src.codes.code_funcs.fill_bill_type", return_value={}):
|
||||
with patch(
|
||||
"src.codes.code_funcs.get_implicit_runs",
|
||||
return_value={"PROCEDURE_CD": True, "REVENUE_CD": True},
|
||||
):
|
||||
result = code_funcs.extract_codes_from_service(
|
||||
{"SERVICE_TERM": "Office visit"}, self.constants
|
||||
)
|
||||
self.assertEqual(result["CODE_METHODOLOGY"], "Explicit")
|
||||
self.assertIn("99213", str(result["PROCEDURE_CD"]))
|
||||
self.assertIn("Office visit", str(result["PROCEDURE_CD_DESC"]))
|
||||
self.assertIn("Advanced Revenue", str(result["REVENUE_CD_DESC"]))
|
||||
|
||||
@patch("src.codes.code_funcs.code_implicit_arbitration")
|
||||
@patch("src.codes.code_funcs.build_implicit_candidates")
|
||||
@patch("src.codes.code_funcs.code_explicit")
|
||||
def test_extract_codes_implicit_arbitration_unmatched(
|
||||
self, mock_explicit, mock_build_candidates, mock_arbitration
|
||||
):
|
||||
"""Implicit path appends ' - Unmatched' when chosen candidate has unmapped code."""
|
||||
mock_explicit.return_value = {
|
||||
"PROCEDURE_CD": ["Category: J"],
|
||||
"REVENUE_CD": [],
|
||||
}
|
||||
mock_build_candidates.return_value = [
|
||||
{
|
||||
"source": "RAG",
|
||||
"code_answer_dict": {
|
||||
"PROCEDURE_CD": ["99999"],
|
||||
"REVENUE_CD": [],
|
||||
},
|
||||
}
|
||||
]
|
||||
chosen = {
|
||||
"PROCEDURE_CD": ["99999"],
|
||||
"REVENUE_CD": [],
|
||||
"CODE_METHODOLOGY": "Implicit - Arbitration (RAG)",
|
||||
}
|
||||
mock_arbitration.return_value = chosen
|
||||
with patch("src.codes.code_funcs.clean_service", return_value="CLEAN"):
|
||||
with patch("src.codes.code_funcs.fill_bill_type", return_value={}):
|
||||
with patch(
|
||||
"src.codes.code_funcs.get_implicit_runs",
|
||||
return_value={"PROCEDURE_CD": True, "REVENUE_CD": True},
|
||||
):
|
||||
result = code_funcs.extract_codes_from_service(
|
||||
{"SERVICE_TERM": "J CODES"}, self.constants
|
||||
)
|
||||
self.assertEqual(
|
||||
result["CODE_METHODOLOGY"], "Implicit - Arbitration (RAG) - Unmatched"
|
||||
)
|
||||
self.assertIn("99999", str(result["PROCEDURE_CD"]))
|
||||
|
||||
@patch("src.codes.code_funcs.code_implicit_arbitration")
|
||||
@patch("src.codes.code_funcs.build_implicit_candidates")
|
||||
@patch("src.codes.code_funcs.code_explicit")
|
||||
def test_extract_codes_implicit_arbitration_all_mapped(
|
||||
self, mock_explicit, mock_build_candidates, mock_arbitration
|
||||
):
|
||||
"""Implicit path does not append ' - Unmatched' when all codes are in mapping."""
|
||||
mock_explicit.return_value = {
|
||||
"PROCEDURE_CD": ["Category: J"],
|
||||
"REVENUE_CD": [],
|
||||
}
|
||||
mock_build_candidates.return_value = [
|
||||
{
|
||||
"source": "RAG",
|
||||
"code_answer_dict": {
|
||||
"PROCEDURE_CD": ["99213"],
|
||||
"REVENUE_CD": ["0456"],
|
||||
},
|
||||
}
|
||||
]
|
||||
chosen = {
|
||||
"PROCEDURE_CD": ["99213"],
|
||||
"REVENUE_CD": ["0456"],
|
||||
"CODE_METHODOLOGY": "Implicit - Arbitration (RAG)",
|
||||
}
|
||||
mock_arbitration.return_value = chosen
|
||||
with patch("src.codes.code_funcs.clean_service", return_value="CLEAN"):
|
||||
with patch("src.codes.code_funcs.fill_bill_type", return_value={}):
|
||||
with patch(
|
||||
"src.codes.code_funcs.get_implicit_runs",
|
||||
return_value={"PROCEDURE_CD": True, "REVENUE_CD": True},
|
||||
):
|
||||
result = code_funcs.extract_codes_from_service(
|
||||
{"SERVICE_TERM": "J CODES"}, self.constants
|
||||
)
|
||||
self.assertEqual(result["CODE_METHODOLOGY"], "Implicit - Arbitration (RAG)")
|
||||
self.assertIn("99213", str(result["PROCEDURE_CD"]))
|
||||
self.assertIn("0456", str(result["REVENUE_CD"]))
|
||||
|
||||
@patch("src.codes.code_funcs.extract_codes_from_service")
|
||||
def test_code_breakout(self, mock_extract_codes):
|
||||
"""Test the code_breakout function for processing DataFrame records."""
|
||||
# Create test data
|
||||
# BILL_TYPE_CD_DESC set so pre-pass skips fill_bill_type (avoids LLM on CI).
|
||||
test_df = pd.DataFrame(
|
||||
[{"SERVICE_TERM": "SERVICE A"}, {"SERVICE_TERM": "SERVICE B"}]
|
||||
[
|
||||
{
|
||||
"SERVICE_TERM": "SERVICE A",
|
||||
"BILL_TYPE_CD_DESC": "Inpatient Hospital",
|
||||
},
|
||||
{
|
||||
"SERVICE_TERM": "SERVICE B",
|
||||
"BILL_TYPE_CD_DESC": "Inpatient Hospital",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Setup mocks - extract_codes_from_service is called directly
|
||||
mock_extract_codes.side_effect = [
|
||||
{"SERVICE_TERM": "SERVICE A", "PROCEDURE_CD": ["12345"]},
|
||||
{"SERVICE_TERM": "SERVICE B", "PROCEDURE_CD": ["67890"]},
|
||||
]
|
||||
# Return value by SERVICE_TERM so order is correct under ThreadPoolExecutor.
|
||||
def return_by_service(rep, constants):
|
||||
st = rep.get("SERVICE_TERM", "")
|
||||
if st == "SERVICE A":
|
||||
return {"SERVICE_TERM": "SERVICE A", "PROCEDURE_CD": ["12345"]}
|
||||
return {"SERVICE_TERM": "SERVICE B", "PROCEDURE_CD": ["67890"]}
|
||||
|
||||
mock_extract_codes.side_effect = return_by_service
|
||||
|
||||
# Test function
|
||||
result_df = code_funcs.code_breakout(test_df, self.constants)
|
||||
|
||||
Reference in New Issue
Block a user