Merged in feature/reimbursement-level-tins (pull request #379)
Initialize Reimbursement-Level TINs * Initialized Reimbursement-Level TIN and NPI * Update unit tests * Docstrings and cleanup * Update test Approved-by: Alex Galarce
This commit is contained in:
@@ -14,7 +14,7 @@ from src import config
|
||||
import src.utils.string_utils as string_utils
|
||||
import src.utils.io_utils as io_utils
|
||||
from src.config import FIELD_JSON_PATH
|
||||
from src.investment.one_to_n_funcs import dynamic_and_exhibit_level, reimbursement_level, combine_exhibit_and_reimbursement
|
||||
from src.investment.one_to_n_funcs import dynamic_and_exhibit_level, reimbursement_level, combine_one_to_n_answers, reimbursement_tin_npi
|
||||
|
||||
def process_file(file_object, run_timestamp):
|
||||
filename, contract_text = file_object
|
||||
@@ -92,11 +92,15 @@ def run_one_to_n_prompts(filename, text_dict, exhibit_pages, exhibit_chunk_mappi
|
||||
for exhibit_page in exhibit_pages:
|
||||
exhibit_chunk = string_utils.get_exhibit_chunk(text_dict, exhibit_chunk_mapping, exhibit_page)
|
||||
if string_utils.contains_reimbursement(exhibit_chunk):
|
||||
|
||||
################## INITIALIZE FIELDS ##################
|
||||
reimbursement_level_fields = FieldSet(relationship="one_to_n", field_type="reimbursement_level", file_path=FIELD_JSON_PATH)
|
||||
exhibit_level_fields = FieldSet(relationship="one_to_n", field_type="exhibit_level", file_path=FIELD_JSON_PATH)
|
||||
dynamic_fields = FieldSet(relationship="one_to_n", field_type="dynamic", file_path=FIELD_JSON_PATH)
|
||||
|
||||
################## GET REIMBURSEMENT TIN/NPI ##################
|
||||
tin_npi_answers, reimbursement_level_fields = reimbursement_tin_npi(exhibit_chunk, reimbursement_level_fields)
|
||||
|
||||
################## GET EXHIBIT-LEVEL ANSWERS (INCLUDING DYNAMIC) ##################
|
||||
exhibit_level_answers= dynamic_and_exhibit_level(exhibit_chunk, filename, reimbursement_level_fields, exhibit_level_fields, dynamic_fields)
|
||||
exhibit_level_answers['EXHIBIT_PAGE'] = exhibit_page
|
||||
@@ -105,7 +109,7 @@ def run_one_to_n_prompts(filename, text_dict, exhibit_pages, exhibit_chunk_mappi
|
||||
reimbursement_level_answers = reimbursement_level(exhibit_chunk, filename, reimbursement_level_fields) # Return list of dictionaries
|
||||
|
||||
################## COMBINE ANSWERS ##################
|
||||
full_answer_dict = combine_exhibit_and_reimbursement(exhibit_level_answers, reimbursement_level_answers)
|
||||
full_answer_dict = combine_one_to_n_answers(exhibit_level_answers, reimbursement_level_answers, tin_npi_answers)
|
||||
one_to_n_results += full_answer_dict
|
||||
|
||||
|
||||
|
||||
@@ -7,9 +7,10 @@ import src.utils.llm_utils as llm_utils
|
||||
import src.utils.string_utils as string_utils
|
||||
from src import config, postprocessing_funcs
|
||||
from src.enums.delimiters import Delimiter
|
||||
from src.prompts.investment_prompts import FieldSet
|
||||
from src.prompts.investment_prompts import FieldSet, Field
|
||||
import src.prompts.investment_prompts as investment_prompts
|
||||
from src.config import FIELD_JSON_PATH
|
||||
from src.regex.regex_utils import find_regex_matches
|
||||
|
||||
def get_exhibit_header(exhibit_chunk, filename):
|
||||
"""
|
||||
@@ -191,6 +192,26 @@ def get_methodology_breakout(reimbursement_primary_answers, filename):
|
||||
methodology_breakout_answers.append({**answer_dict, **breakout_dict})
|
||||
return methodology_breakout_answers
|
||||
|
||||
def reimbursement_tin_npi(exhibit_chunk: str, reimbursement_level_fields: FieldSet):
|
||||
answer_dict = {}
|
||||
|
||||
fields = [("PROV_REIMBURSEMENT_TIN", r"\b\d{2}-\d{7}\b|\b\d{9}\b"),
|
||||
("PROV_REIMBURSEMENT_NPI", r"\b\d{10}\b")]
|
||||
|
||||
for field_tuple in fields:
|
||||
field_name, field_pattern = field_tuple[0], field_tuple[1]
|
||||
field_matches, page_list = find_regex_matches(field_pattern, {'1' : exhibit_chunk})
|
||||
|
||||
if not field_matches:
|
||||
answer_dict[field_name] = "N/A"
|
||||
elif len(field_matches) == 1:
|
||||
answer_dict['PROV_REIMBURSEMENT_TIN'] = field_matches[0]
|
||||
else: # Multiple tins
|
||||
reimbursement_field = Field.load_from_file(FIELD_JSON_PATH, field_name)
|
||||
reimbursement_field.update_valid_values(field_matches)
|
||||
reimbursement_level_fields.add_field(reimbursement_field)
|
||||
|
||||
return answer_dict, reimbursement_level_fields
|
||||
|
||||
|
||||
def reimbursement_level(exhibit_text, filename, reimbursement_level_fields):
|
||||
@@ -206,31 +227,33 @@ def reimbursement_level(exhibit_text, filename, reimbursement_level_fields):
|
||||
Returns:
|
||||
dict: The parsed LLM response as a list of dictionaries
|
||||
"""
|
||||
|
||||
reimbursement_primary_answers = get_reimbursement_primary(reimbursement_level_fields, exhibit_text, filename) # Returns list of dicts
|
||||
carveout_answers = get_carveout_ind(reimbursement_primary_answers, filename)
|
||||
|
||||
# Later, we will only run methodology breakout for those without certain carveout indicators. We will run carveout-specific breakouts for the others
|
||||
methodology_breakout_answers = get_methodology_breakout(carveout_answers, filename)
|
||||
|
||||
return methodology_breakout_answers
|
||||
|
||||
|
||||
|
||||
def combine_exhibit_and_reimbursement(exhibit_level_answers: dict, reimbursement_level_answers: list[dict]) -> list[dict]:
|
||||
def combine_one_to_n_answers(exhibit_level_answers: dict, reimbursement_level_answers: list[dict], tin_npi_answers: dict) -> list[dict]:
|
||||
"""
|
||||
Combines exhibit-level answers with each dictionary in the reimbursement-level answers.
|
||||
|
||||
Args:
|
||||
exhibit_level_answers (dict): A dictionary containing exhibit-level key-value pairs.
|
||||
reimbursement_level_answers (list[dict]): A list of dictionaries containing reimbursement-level answers.
|
||||
tin_npi_answers (dict): A dictionary containing exhibit-level key-value pairs, or nothing, depending on regex dynamic assignment
|
||||
|
||||
Returns:
|
||||
list[dict]: A new list of dictionaries where each dictionary combines exhibit-level answers
|
||||
with the respective reimbursement-level answers.
|
||||
"""
|
||||
|
||||
# Add exhibit-level answers to each reimbursement dictionary
|
||||
combined_answers = [
|
||||
{**reimbursement_dict, **exhibit_level_answers}
|
||||
{**reimbursement_dict, **tin_npi_answers, **exhibit_level_answers}
|
||||
for reimbursement_dict in reimbursement_level_answers
|
||||
]
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ def run_regex_fields(one_to_one_fields: FieldSet,
|
||||
top_sheet_dict: str,
|
||||
filename: str):
|
||||
# Fill NPI/TIN regex here
|
||||
regex_fields = one_to_one_fields.filter(field_type="regex")
|
||||
# regex_fields = one_to_one_fields.filter(relationship="one_to_one", field_type="regex")
|
||||
|
||||
# One to One TIN Fields
|
||||
tin_answers = get_tin_fields(filename, text_dict, top_sheet_dict)
|
||||
|
||||
@@ -143,6 +143,18 @@
|
||||
"field_type": "regex",
|
||||
"prompt": "TBD"
|
||||
},
|
||||
{
|
||||
"field_name": "PROV_REIMBURSEMENT_TIN",
|
||||
"relationship": "one_to_n",
|
||||
"field_type": "regex",
|
||||
"prompt": "What TIN (Tax ID Number) does the reimbursement term apply to? Choose only from the following: {valid_values}."
|
||||
},
|
||||
{
|
||||
"field_name": "PROV_REIMBURSEMENT_NPI",
|
||||
"relationship": "one_to_n",
|
||||
"field_type": "regex",
|
||||
"prompt": "What NPI (National Provider Identifier) does the reimbursement term apply to? Choose only from the following: {valid_values}."
|
||||
},
|
||||
{
|
||||
"field_name": "PROV_GROUP_NAME_FULL",
|
||||
"relationship": "one_to_one",
|
||||
|
||||
@@ -32,14 +32,34 @@ class Field:
|
||||
self.retrieved_text = None
|
||||
self.format = field_dict['format'] if 'format' in field_dict else None
|
||||
|
||||
@classmethod
|
||||
def load_from_file(cls, file_path, field_name):
|
||||
"""
|
||||
Reads a JSON file and creates a Field object if field_name is present in the JSON.
|
||||
|
||||
:param file_path: Path to the JSON file.
|
||||
:param field_name: The field name to look for in the JSON data.
|
||||
:return: A Field object if found, otherwise raises a ValueError.
|
||||
"""
|
||||
with open(file_path, 'r') as file:
|
||||
data = json.load(file)
|
||||
|
||||
for field_dict in data:
|
||||
if field_dict.get("field_name") == field_name:
|
||||
return cls(field_dict)
|
||||
|
||||
raise ValueError(f"Field '{field_name}' not found in {file_path}")
|
||||
|
||||
@classmethod
|
||||
def from_values(cls, **kwargs):
|
||||
"""Creates a Field instance from given keyword arguments."""
|
||||
return cls(kwargs)
|
||||
|
||||
def to_dict(self):
|
||||
"""Converts the Field instance to a dictionary."""
|
||||
return {
|
||||
"field_name": self.field_name,
|
||||
"base_field":self.base_field,
|
||||
"base_field": self.base_field,
|
||||
"relationship": self.relationship,
|
||||
"field_type": self.field_type,
|
||||
"prompt": self.prompt,
|
||||
@@ -47,9 +67,11 @@ class Field:
|
||||
"keywords": self.keywords,
|
||||
"methodology": self.methodology,
|
||||
"case_sensitive": self.case_sensitive,
|
||||
"regex_pattern": self.regex_pattern
|
||||
"regex_pattern": self.regex_pattern,
|
||||
"format": self.format
|
||||
}
|
||||
|
||||
|
||||
def print(self):
|
||||
"""Prints the details of a FieldInfo object in a readable format."""
|
||||
print(f"Field Name: {self.field_name}")
|
||||
@@ -63,6 +85,7 @@ class Field:
|
||||
print(f"Regex Pattern: {'None specified' if not self.regex_pattern else self.regex_pattern}")
|
||||
|
||||
def get_prompt_dict(self):
|
||||
"""Returns a dictionary with the field name as the key and its prompt as the value."""
|
||||
return {self.field_name : self.get_prompt()}
|
||||
|
||||
def get_prompt(self):
|
||||
|
||||
@@ -92,7 +92,7 @@ def npi_pattern_match(text_dict: dict[str, str]) -> dict[str, str]:
|
||||
|
||||
for pattern in patterns:
|
||||
matches, page_list = find_regex_matches(pattern, text_dict)
|
||||
matches = [s for s in matches if s[0] in ("1", "2")]
|
||||
matches = [s for s in matches if s[0] in ("1", "2")] # NPI can only start with 1 or 2
|
||||
matches = list(set(matches))
|
||||
if len(matches) == 1:
|
||||
NPI_dict["PROV_GROUP_NPI"] = matches[0]
|
||||
@@ -129,8 +129,6 @@ def get_npi_fields(
|
||||
|
||||
#################################################################################################################################################
|
||||
# IRS
|
||||
|
||||
|
||||
def check_tin_in_filename(filename):
|
||||
"""
|
||||
Checks for the pattern match in filename
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from src.investment.one_to_n_funcs import get_exhibit_header, prompt_dynamic, process_dynamic, dynamic_and_exhibit_level, reimbursement_level, combine_exhibit_and_reimbursement
|
||||
from src.investment.one_to_n_funcs import get_exhibit_header, prompt_dynamic, process_dynamic, dynamic_and_exhibit_level, reimbursement_level, combine_one_to_n_answers
|
||||
|
||||
class TestOneToN(unittest.TestCase):
|
||||
|
||||
@@ -40,8 +40,9 @@ class TestOneToN(unittest.TestCase):
|
||||
def test_combine_exhibit_and_reimbursement(self):
|
||||
exhibit_level_answers = {"EXHIBIT_NAME": "Exhibit Header", "field1": "value1"}
|
||||
reimbursement_level_answers = [{"field2": "value2"}, {"field3": "value3"}]
|
||||
tin_npi_answers = {}
|
||||
|
||||
result = combine_exhibit_and_reimbursement(exhibit_level_answers, reimbursement_level_answers)
|
||||
result = combine_one_to_n_answers(exhibit_level_answers, reimbursement_level_answers, tin_npi_answers)
|
||||
|
||||
self.assertEqual(result, [
|
||||
{"field2": "value2", "EXHIBIT_NAME": "Exhibit Header", "field1": "value1"},
|
||||
|
||||
Reference in New Issue
Block a user