2024-10-28 23:39:36 +00:00
import json
import re
2025-04-25 20:28:52 +00:00
import traceback
2024-12-05 22:46:41 +00:00
import warnings
2025-04-25 20:28:52 +00:00
from datetime import datetime
2025-01-06 15:39:29 +00:00
import numpy as np
import pandas as pd
2025-01-08 21:59:47 +00:00
import src . utils . llm_utils as llm_utils
2025-01-13 21:11:36 +00:00
from src import config
2025-01-08 21:59:47 +00:00
from src . enums . delimiters import Delimiter
2025-04-25 20:28:52 +00:00
from src . prompts import preprocessing_prompts
2025-01-08 21:59:47 +00:00
from src . regex . regex_patterns import ( BACKTICK_PATTERN , PIPE_PATTERN ,
TRIPLE_BACKTICK_PATTERN )
2024-10-28 23:39:36 +00:00
2024-12-30 21:41:21 +00:00
def extract_text_from_delimiters (
raw_output : str , delimiter : Delimiter , match_index : int = - 1
) - > str :
2024-12-05 22:46:41 +00:00
"""
Extracts a match enclosed in specified delimiters from the raw output.
This function searches for text enclosed by a specified delimiter in the given raw output string.
It returns the match specified by the `which_match` index. If no match is found, it returns " N/A " .
- raw_output (str): The raw output string to search within.
- which_match (int, optional): The index of the match to return. Defaults to -1, which returns the last match. If the index is out of range, the last match is returned.
- str: The extracted text or " N/A " if no match is found.
2024-12-30 21:41:21 +00:00
2024-12-05 22:46:41 +00:00
Raises:
- TypeError: If `raw_output` is not a string or `delimiter` is not a Delimiter enum value.
- ValueError: If an unsupported delimiter is provided.
Returns:
- str: The extracted answer or " N/A " if no match is found.
"""
if not isinstance ( raw_output , str ) :
2024-12-30 21:41:21 +00:00
raise TypeError (
" Expected a string for raw_output, got {0} . " . format (
type ( raw_output ) . __name__
)
)
2024-12-05 22:46:41 +00:00
if not isinstance ( delimiter , Delimiter ) :
2024-12-30 21:41:21 +00:00
raise TypeError (
" Expected a Delimiter enum value, got {0} . " . format ( type ( delimiter ) . __name__ )
)
2024-12-05 22:46:41 +00:00
# Define a pattern based on the delimiter type
if delimiter == Delimiter . PIPE :
pattern = PIPE_PATTERN
elif delimiter == Delimiter . BACKTICK :
pattern = BACKTICK_PATTERN
elif delimiter == Delimiter . TRIPLE_BACKTICK :
pattern = TRIPLE_BACKTICK_PATTERN
else :
raise ValueError ( " Unsupported delimiter. Use one of the Delimiter enum values. " )
2024-12-30 21:41:21 +00:00
2024-12-05 22:46:41 +00:00
# Find all matches based on the pattern
matches = re . findall ( pattern = pattern , string = raw_output )
2024-12-30 21:41:21 +00:00
2024-12-05 22:46:41 +00:00
if len ( matches ) == 0 :
return " N/A "
2024-12-30 21:41:21 +00:00
2024-12-05 22:46:41 +00:00
# Adjust for negative indices
if match_index < 0 :
match_index + = len ( matches )
2024-12-30 21:41:21 +00:00
2024-12-05 22:46:41 +00:00
if not 0 < = match_index < len ( matches ) :
warnings . warn ( " Index out of range. Returning the last match by default. " )
match_index = - 1
2024-12-30 21:41:21 +00:00
2024-12-05 22:46:41 +00:00
return matches [ match_index ]
2025-06-09 21:49:45 +00:00
def page_key_sort ( page_key : str ) - > tuple [ int , float | str ] :
2025-04-25 20:28:52 +00:00
""" Sorts page keys, prioritizing numeric keys first.
Args:
page_key (str): The page key to sort.
Returns:
2025-06-09 21:49:45 +00:00
tuple[int, float|str]: A tuple where the first element is 0 for numeric keys and 1 for non-numeric keys, and the second element is the numeric value for numeric keys or the original string for non-numeric keys.
2025-04-25 20:28:52 +00:00
Example:
>>> page_key_sort( " 1 " )
2025-06-09 21:49:45 +00:00
(0, 1.0)
2025-04-25 20:28:52 +00:00
>>> page_key_sort( " A " )
(1, " A " )
Example Usage with mixed types:
>>> L = [ " 10 " , " 2 " , " A " , " 1 " ]
>>> sort(L, key=page_key_sort)
>>> print(L)
[ ' 1 ' , ' 2 ' , ' 10 ' , ' A ' ]
"""
try :
2025-06-09 21:49:45 +00:00
return ( 0 , float ( page_key ) ) # Numeric pages first, in numerical order
2025-04-25 20:28:52 +00:00
except ValueError :
return ( 1 , str ( page_key ) ) # Non-numeric pages after, in alphabetical order
2025-01-09 17:59:28 +00:00
def json_parsing_search ( response_text : str , field_list : list [ str ] ) - > dict [ str , str ] :
"""
Parses a JSON-like string to extract values for fields.
This function attempts to clean and format the input string to resemble a valid JSON structure.
It then searches for the specified fields within the string and extracts their corresponding values.
Parameters:
response_text (str): The JSON-like string to parse.
field_list (list[str]): A list of field names to search for in the response_text.
2024-12-05 22:46:41 +00:00
2025-01-09 17:59:28 +00:00
Returns:
dict[str, str]: A dictionary where keys are field names and values are the extracted values from the response_text.
"""
2024-10-28 23:39:36 +00:00
try :
if response_text . split ( " { " , 1 ) [ 1 ] . strip ( ) [ 0 ] == ' " ' :
response_text = " { " + response_text . split ( " { " , 1 ) [ 1 ]
else :
response_text = " { " + response_text
except :
response_text = " { " + response_text
if len ( response_text . split ( " } " , 1 ) ) > 1 :
if response_text . rsplit ( " } " , 1 ) [ 0 ] . strip ( ) [ - 1 ] == ' " ' :
response_text = response_text . rsplit ( " } " , 1 ) [ 0 ] + " } "
elif response_text . rsplit ( " } " , 1 ) [ 0 ] . strip ( ) [ - 1 ] == " } " :
response_text = response_text . rsplit ( " } " , 1 ) [ 0 ]
else :
response_text = response_text . rstrip ( " , " ) + " } "
2025-01-09 17:59:28 +00:00
2024-10-28 23:39:36 +00:00
field_l = [ ]
answer_l = [ ]
position_dict = { }
2025-01-09 17:59:28 +00:00
2024-10-28 23:39:36 +00:00
for f in field_list :
location = response_text . find ( ' " ' + f + ' " ' )
if location != - 1 :
position_dict [ location ] = f
2025-01-09 17:59:28 +00:00
2024-10-28 23:39:36 +00:00
field_list = list ( dict ( sorted ( position_dict . items ( ) ) ) . values ( ) )
2025-01-09 17:59:28 +00:00
2024-10-28 23:39:36 +00:00
for f in field_list :
if f in response_text :
field_l . append ( f )
value = response_text . split ( ' " ' + f + ' " ' ) [ 0 ]
response_text = response_text . split ( ' " ' + f + ' " ' ) [ 1 ]
if f != field_list [ 0 ] and f != field_list [ - 1 ] :
answer_l . append (
value . strip ( " \n " )
. strip ( ' " ' )
. strip ( " : " )
. strip ( " " )
. strip ( " \n " )
. strip ( " " )
. strip ( " , " )
. strip ( ' " ' )
)
elif f == field_list [ - 1 ] :
answer_l . append (
value . strip ( " \n " )
. strip ( ' " ' )
. strip ( " : " )
. strip ( " " )
. strip ( " \n " )
. strip ( " " )
. strip ( " , " )
. strip ( ' " ' )
)
answer_l . append (
response_text . strip ( " \n " )
. strip ( ' " ' )
. strip ( " : " )
. strip ( " " )
. strip ( " } " )
. strip ( " \n " )
. strip ( " " )
. strip ( ' " ' )
)
return dict ( zip ( field_l , answer_l ) )
2025-01-06 15:39:29 +00:00
2025-01-09 17:59:28 +00:00
def secondary_string_to_dict ( dict_string : str , filename : str ) - > dict :
2025-01-06 15:39:29 +00:00
"""
Converts a string representation of a dictionary into an actual dictionary object, handling potential formatting issues.
This function cleans the string by removing specific unwanted characters and markers that may interfere with JSON parsing.
It then locates the substring that correctly forms a dictionary format, attempts to parse it as JSON, and handles common
parsing errors by replacing problematic single quotes with double quotes before re-parsing.
Parameters:
dict_string (str): The string containing the dictionary-like content, potentially surrounded by extra text or characters.
2025-01-09 17:59:28 +00:00
filename (str): The filename associated with these entries, used to tag each resulting dictionary.
2025-01-06 15:39:29 +00:00
Returns:
dict: The dictionary obtained from parsing the cleaned and corrected string.
"""
start_index = dict_string . find ( " { " )
end_index = dict_string . rfind ( " } " ) + 1
dict_substring = dict_string [ start_index : end_index ]
try :
result_dict = json . loads ( dict_substring )
except :
try :
dict_substring = dict_substring . replace ( " ' " , ' " ' )
result_dict = json . loads ( dict_substring )
except :
try :
2025-01-13 21:11:36 +00:00
prompt = preprocessing_prompts . FIX_JSON ( dict_substring )
2025-01-08 21:59:47 +00:00
dict_substring = llm_utils . invoke_claude (
2025-01-06 15:39:29 +00:00
prompt , config . MODEL_ID_CLAUDE3_HAIKU , filename
)
result_dict = json . loads ( dict_substring )
except Exception as e :
print ( e )
result_dict = { }
return result_dict
def primary_string_to_dict ( string_dict , filename ) :
"""
Converts a dictionary of strings, where each string represents multiple dictionary entries, into a list of dictionaries,
augmenting each with metadata such as page number and filename.
This function iterates over each page number ' s string data, extracting and converting string representations of
dictionaries into actual dictionary objects. It handles and cleans the string format to properly parse it into dictionaries.
All dictionaries are then augmented with their respective page number and the filename before being compiled into a list.
Parameters:
string_dict (dict): A dictionary where keys are page numbers and values are strings containing multiple dictionary entries.
filename (str): The filename associated with these entries, used to tag each resulting dictionary.
Returns:
list of dict: A list of dictionaries, each representing data extracted and converted from the input string, tagged with
their page number and filename.
"""
data = [ ]
pattern = r " \ { .*? \ } "
for page_num in string_dict . keys ( ) :
primary_list = string_dict [ page_num ]
dicts = primary_list . split ( " [ " ) [ 1 ] # Strip front
dicts = dicts . split ( " ] " ) [ 0 ] # Strip back
dicts = dicts . replace ( " \n " , " " ) # Remove new lines
dicts = dicts . replace ( " <<< " , " " )
dicts = dicts . replace ( " >>> " , " " )
dicts = re . sub ( r " (?<! \\ ) ' ([^ ' ]*?) ' (?<! \\ ): " , r ' " \ 1 " : ' , dicts )
dict_list = re . findall ( pattern , dicts )
dict_list = [ secondary_string_to_dict ( d , filename ) for d in dict_list ]
for dict_ in dict_list :
dict_ [ " page_num " ] = page_num
data . append ( dict_ )
return data
2025-04-22 21:48:45 +00:00
def universal_json_load ( string_dict : str ) :
""" Matches json dicts and list of dicts - NOT simple lists
Args:
string_dict (str): The string containing the JSON-like content.
Returns:
list[dict] | dict: The dictionary or list of dictionaries obtained from parsing the cleaned and corrected string.
Raises:
ValueError: If no valid JSON object is found in the input string. In addition,
the traceback as well as the offending string are printed.
"""
2025-05-19 20:26:24 +00:00
# Try to match dictionary or list of dictionaries first
dict_match = re . search ( r ' \ { .* \ }| \ [ \ s*? \ { .* \ } \ s*? \ ] ' , string_dict , re . DOTALL )
if dict_match :
matched_str = dict_match . group ( )
2025-01-27 19:39:23 +00:00
try :
2025-04-22 21:48:45 +00:00
return json . loads ( matched_str )
except json . JSONDecodeError as e :
print ( f " Failed to parse JSON: { e } " )
2025-04-25 21:34:51 +00:00
print ( f " Original string: { string_dict } " )
2025-04-22 21:48:45 +00:00
print ( f " Traceback: { traceback . format_exc ( ) } " )
raise ValueError ( f " JSON parsing failed: { e . msg } at position { e . pos } " ) from e
2025-05-19 20:26:24 +00:00
# If no dict pattern found, try to match list of strings
list_match = re . search ( r ' \ [(.*?) \ ] ' , string_dict , re . DOTALL )
if list_match :
matched_str = list_match . group ( )
try :
# Clean up the string list before parsing
cleaned_str = re . sub ( r ' (?<! \\ ) " ' , ' " ' , matched_str ) # Handle escaped quotes
cleaned_str = re . sub ( r " ' " , ' " ' , cleaned_str ) # Replace single quotes with double quotes
return json . loads ( cleaned_str )
except json . JSONDecodeError as e :
print ( f " Failed to parse list: { e } " )
print ( f " Original string: { string_dict } " )
print ( f " Traceback: { traceback . format_exc ( ) } " )
raise ValueError ( f " JSON parsing failed: { e . msg } at position { e . pos } " ) from e
return { }
2025-01-27 19:39:23 +00:00
2025-01-24 22:46:59 +00:00
reimbursement_strings = [ # These are for `method='keyword'`
" % " ,
" $ " ,
" percent " ,
2025-03-06 21:42:43 +00:00
" billed charges "
2025-01-24 22:46:59 +00:00
]
reimb_regex = r " (?<![$ % ])(?: \ $ \ d+| \ d+[$ % ])(?![$ % ]) "
2025-03-06 21:42:43 +00:00
# TODO: check should this always be checking just page 1? Bc in file_processing and table_utils it is but in preprocessing_funcs it's checking specific pages
2025-01-27 19:39:23 +00:00
def contains_reimbursement ( text , page = " 1 " , method = " keyword " ) :
2025-01-24 22:46:59 +00:00
"""
Checks if the given text contains any reimbursement-related keywords or patterns.
Args:
text (str or dict): The text to check. If a dictionary is provided, it should have page numbers as keys.
page (str, optional): The page number to check in the dictionary. Defaults to " 1 " .
method (str, optional): The method to use for checking. Options are " keyword "
and " regex " . Defaults to " keyword " .
Returns:
bool: True if any reimbursement-related keyword or pattern is found, False otherwise.
"""
if method == " keyword " :
if isinstance ( text , dict ) :
text_to_check = text . get ( page , " " ) . lower ( )
elif isinstance ( text , str ) :
text_to_check = text . lower ( )
else :
print ( " contains_reimbursement - Invalid data type " )
return False
return any ( keyword in text_to_check for keyword in reimbursement_strings )
elif method == " regex " :
return bool ( re . search ( reimb_regex , text ) )
2025-01-06 15:39:29 +00:00
else :
2025-01-24 22:46:59 +00:00
raise ValueError ( " Invalid method. Choose ' keyword ' or ' regex ' . " )
2025-01-06 15:39:29 +00:00
2025-01-24 22:46:59 +00:00
def count_reimbursements_in_exhibit ( exhibit_text : str ) - > int : #JUST by regex
""" Counts reimbursements in an exhibit text. Reimbursements are detected by a regex
search as defined by `reimb_regex`.
Args:
exhibit_text (str): Input exhibit text
Returns:
int: Number of reimbursements detected
"""
return len ( re . findall ( reimb_regex , exhibit_text ) )
2025-01-06 15:39:29 +00:00
2025-06-26 21:26:27 +00:00
def is_empty ( value : str | list | pd . Series , pd_mask : bool = True ) - > bool | pd . Series :
2025-01-27 19:39:23 +00:00
"""
Checks if a value is considered empty or invalid.
Args:
value: The value to check.
2025-04-16 14:52:37 +00:00
Can be a string, list, or pandas Series.
pd_mask (bool): Only relevant for Series inputs.
If True, returns a mask for empty values in a pandas Series.
Otherwise, the function returns True iff the entire Series is empty.
2025-01-27 19:39:23 +00:00
Returns:
2025-06-26 21:26:27 +00:00
bool | pd.Series:
- When the input is a string, returns True if the value is empty or invalid, False otherwise.
- When the input is a list, returns True if the list is empty or all elements are empty/invalid.
- When the input is a pandas Series:
- If `pd_mask` is True, returns a boolean mask indicating which elements are empty or invalid.
- If `pd_mask` is False, returns True iff all elements are empty/invalid, or if the entire Series is empty.
2025-01-27 19:39:23 +00:00
"""
2025-04-16 14:52:37 +00:00
empty_values = [ None , " " , " N/A " , " NA " , " null " , " none " , " NaN " , np . nan , " nan " , " None " ]
2025-02-21 14:44:36 +00:00
if isinstance ( value , list ) : # Handle list inputs
return not value or all ( is_empty ( v ) for v in value )
2025-04-16 14:52:37 +00:00
# if it's a pd.Series, return the mask (True for empty values)
if isinstance ( value , pd . Series ) :
return value . isna ( ) | ( value . isin ( empty_values ) ) if pd_mask else value . isna ( ) . all ( )
2025-01-06 15:39:29 +00:00
if pd . isna ( value ) :
return True
else :
2025-01-10 17:24:44 +00:00
return value in empty_values
2025-01-24 22:46:59 +00:00
2025-01-10 17:24:44 +00:00
2025-03-04 22:08:16 +00:00
def datetime_str ( ) :
return datetime . now ( ) . strftime ( " [ % Y- % m- %d % H: % M: % S] " )
2025-04-25 20:28:52 +00:00
def extract_signature_page ( text_dict : dict , filename : str ) - > dict :
""" Extracts the signature page(s) from a contract text dictionary and returns it as a dictionary.
First tries to match the textract marker " this page has N signature(s) " , then falls back to looking
for the word " signature " if no matches are found. " this page has N signature(s) " is Textract ' s marker for signatures
Args:
text_dict (dict): A dictionary containing text data, organized by pages.
filename (str): The name of the file being processed.
Returns:
dict: A dictionary containing the extracted signature page(s).
"""
signature_pages = { }
# This regex pattern matches the Textract marker for signatures
# It will look for both numeric and a few textual representations of numbers (one-ten)
textract_pattern = re . compile ( r " this page has (?: \ d+|one|two|three|four|five|six|seven|eight|nine|ten) signature " , re . IGNORECASE )
# First pass: Try to match the full textract pattern
for page_num , page_text in text_dict . items ( ) :
if textract_pattern . search ( page_text ) :
signature_pages [ page_num ] = page_text
2025-05-07 18:21:09 +00:00
# If no pages found with textract pattern, try with keyword
2025-04-25 20:28:52 +00:00
if not signature_pages :
2025-05-07 18:21:09 +00:00
page_items = list ( text_dict . items ( ) )
for i , ( page_num , page_text ) in enumerate ( page_items ) :
lower_text = page_text . lower ( )
if " signature page follow " in lower_text or " signature authorization " in lower_text :
# curr_page = int(float(page_num))
if page_num not in signature_pages :
signature_pages [ page_num ] = page_text # current page
if i + 1 < len ( page_items ) :
next_page_num , next_page_text = page_items [ i + 1 ]
# next_page = int(float(next_page_num))
if next_page_num not in signature_pages :
signature_pages [ next_page_num ] = next_page_text # next page
2025-04-25 20:28:52 +00:00
return signature_pages
2025-05-07 18:21:09 +00:00
def extract_effective_date_pages ( text_dict : dict , filename : str ) - > dict :
"""
Extracts the effective date page(s) from a contract text dictionary and returns it as a dictionary.
This function identifies pages that either contain the Textract marker
" this page has N signature(s) " or the keyword ' effective date: ' .
Additionally, it ensures that the first page (page 1) is always included
in the result, as it often contains effective date.
Args:
text_dict (dict): A dictionary containing text data, organized by pages.
filename (str): The name of the file being processed.
Returns:
dict: A dictionary containing the extracted effective date page(s),
where keys are page numbers and values are the corresponding page text.
"""
effective_date_pages = { }
# Regex pattern to match the Textract marker for signature pages which often has effective dates
# This pattern accounts for both numeric and textual representations of numbers (one-ten).
textract_pattern = re . compile ( r " this page has (?: \ d+|one|two|three|four|five|six|seven|eight|nine|ten) signature " , re . IGNORECASE )
# Iterate through each page in the text dictionary.
for page_num , page_text in text_dict . items ( ) :
# page_num = int(float(page_num))
# Check if the page contains the Textract marker or the keyword 'effective date:'.
if textract_pattern . search ( page_text ) or ' effective date: ' in page_text . lower ( ) :
effective_date_pages [ page_num ] = page_text
# Always include the first page (page 1) as it often contains effective date.
2025-06-02 13:04:52 +00:00
elif page_num == 1 or page_num == " 1 " or page_num == " 1.0 " :
2025-05-07 18:21:09 +00:00
effective_date_pages [ page_num ] = page_text
return effective_date_pages