2024-10-28 23:39:36 +00:00
|
|
|
from itertools import groupby, count
|
|
|
|
|
import re
|
|
|
|
|
from collections import defaultdict
|
|
|
|
|
import utils
|
|
|
|
|
import valid
|
|
|
|
|
from valid import DERIVED_INDICATOR_FIELDS
|
2024-11-13 17:08:19 +00:00
|
|
|
import keywords
|
2024-10-28 23:39:36 +00:00
|
|
|
from collections import defaultdict
|
|
|
|
|
import ac_smart_chunking
|
2024-11-13 17:08:19 +00:00
|
|
|
import ac_funcs
|
|
|
|
|
import prompts
|
|
|
|
|
import claude_funcs
|
|
|
|
|
import config
|
2024-10-28 23:39:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def clean_newlines(contract_text: str) -> str:
|
|
|
|
|
"""Clean textract output by removing newlines and page number indicators.
|
|
|
|
|
|
|
|
|
|
This function processes input text to:
|
|
|
|
|
1. Remove all newline characters.
|
|
|
|
|
2. Remove lines that indicate page numbers (e.g. 'Page 1 of 10')
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
contract_text (str): Raw text output from Textract to be cleaned
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
str: cleaned text with newlines and page number indicators removed
|
|
|
|
|
"""
|
|
|
|
|
cleaned_text = re.sub(r"(?<!\n)\n(?!\n)", " ", contract_text)
|
|
|
|
|
cleaned_text = re.sub(r"Page [0-9]+ of [0-9]+\n\n", " ", contract_text)
|
|
|
|
|
return cleaned_text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def split_text(text: str) -> dict[str, str]:
|
|
|
|
|
"""Split text on pages by the string `Start of Page No. = '
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
text (str): Raw text output from Textract to be split
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
dict[str, str]: A dictionary, keyed by the string page number and valued by the page text.
|
|
|
|
|
"""
|
|
|
|
|
temp_list = text.split("Start of Page No. = ")
|
|
|
|
|
text_list = re.split(r"Start of Page No. = [0-9]+\n", text)
|
|
|
|
|
|
|
|
|
|
text_dict = {}
|
|
|
|
|
for i in range(len(text_list)):
|
|
|
|
|
text_dict[temp_list[i].split()[0]] = text_list[i]
|
|
|
|
|
|
|
|
|
|
return {k: v for k, v in text_dict.items() if k != "Document"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def highlight_rates(text_dict):
|
|
|
|
|
for page, text in text_dict.items():
|
|
|
|
|
words = text.split()
|
|
|
|
|
highlighted_words = []
|
|
|
|
|
for word in words:
|
|
|
|
|
if "%" in word or "$" in word:
|
|
|
|
|
word = f">>>{word}<<<"
|
|
|
|
|
highlighted_words.append(word)
|
|
|
|
|
text_dict[page] = " ".join(highlighted_words)
|
|
|
|
|
return text_dict
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clean_law_symbols(contract_text):
|
|
|
|
|
contract_text = contract_text.replace("$$", "$")
|
|
|
|
|
|
|
|
|
|
contract_text = re.sub(r"(U\.?S\.?C\.?) \$", r"\1§", contract_text)
|
|
|
|
|
contract_text = re.sub(r"(C\.?F\.?R\.?) \$", r"\1§", contract_text)
|
|
|
|
|
|
|
|
|
|
# Second correction: Replace '$' with '§' when followed by a number with three decimal places
|
|
|
|
|
contract_text = re.sub(r"\$(?=\d+\.\d{3})", "§", contract_text)
|
|
|
|
|
|
|
|
|
|
return contract_text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def chunk_consecutive(text_dict, exhibit_pages):
|
|
|
|
|
exhibit_pages = set(str(page) for page in exhibit_pages)
|
|
|
|
|
reimbursement_pages = {
|
|
|
|
|
page_num
|
|
|
|
|
for page_num in text_dict.keys()
|
|
|
|
|
if utils.contains_reimbursement(text_dict, page_num)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
page_dict = {}
|
|
|
|
|
current_chunk_start = None
|
|
|
|
|
last_exhibit = None
|
|
|
|
|
in_exhibit = False
|
|
|
|
|
|
|
|
|
|
def word_count(text):
|
|
|
|
|
return len(text.split())
|
|
|
|
|
|
|
|
|
|
for page_str in sorted(text_dict.keys(), key=int):
|
|
|
|
|
page_num = int(page_str)
|
|
|
|
|
|
|
|
|
|
if page_str in exhibit_pages:
|
|
|
|
|
current_chunk_start = page_str
|
|
|
|
|
last_exhibit = page_str
|
|
|
|
|
in_exhibit = True
|
|
|
|
|
page_dict[current_chunk_start] = [page_str]
|
|
|
|
|
# print(f"found page {page_num} in exhibit, starting new chunk")
|
|
|
|
|
|
|
|
|
|
if page_str in reimbursement_pages:
|
|
|
|
|
if not in_exhibit or current_chunk_start is None:
|
|
|
|
|
# Check if this page has less than 200 words and should be added to the previous chunk
|
|
|
|
|
if current_chunk_start and word_count(text_dict[page_str]) < 200:
|
|
|
|
|
page_dict[current_chunk_start].append(page_str)
|
|
|
|
|
# print(f"found page {page_num} in reimbursement with less than 200 words, adding to previous chunk")
|
|
|
|
|
else:
|
|
|
|
|
current_chunk_start = page_str
|
|
|
|
|
page_dict[current_chunk_start] = [page_str]
|
|
|
|
|
# print(f"found page {page_num} in reimbursement, starting new chunk")
|
|
|
|
|
else:
|
|
|
|
|
page_dict[current_chunk_start].append(page_str)
|
|
|
|
|
# print(f"found page {page_num} in reimbursement, adding to current chunk")
|
|
|
|
|
else:
|
|
|
|
|
if in_exhibit and current_chunk_start is not None:
|
|
|
|
|
page_dict[current_chunk_start].append(page_str)
|
|
|
|
|
# print(f"found non-reimbursement page {page_num} in exhibit, adding to current chunk")
|
|
|
|
|
else:
|
|
|
|
|
in_exhibit = False
|
|
|
|
|
|
|
|
|
|
# Check next page if it's non-reimbursement and not in exhibit
|
|
|
|
|
next_page_str = str(page_num + 1)
|
|
|
|
|
if (
|
|
|
|
|
next_page_str in text_dict
|
|
|
|
|
and next_page_str not in reimbursement_pages
|
|
|
|
|
and next_page_str not in exhibit_pages
|
|
|
|
|
):
|
|
|
|
|
if current_chunk_start is not None:
|
|
|
|
|
page_dict[current_chunk_start].append(next_page_str)
|
|
|
|
|
# print(f'next page {next_page_str} was found to be a non-reimbursement and added')
|
|
|
|
|
|
|
|
|
|
# If we've moved past the last exhibit page, reset in_exhibit
|
|
|
|
|
if in_exhibit and int(page_str) > int(last_exhibit):
|
|
|
|
|
in_exhibit = False
|
|
|
|
|
|
|
|
|
|
# Remove duplicates and sort page numbers in each chunk
|
|
|
|
|
for key in page_dict:
|
|
|
|
|
page_dict[key] = sorted(list(set(page_dict[key])), key=int)
|
|
|
|
|
|
|
|
|
|
final_dict = {page_num: "" for page_num in page_dict.keys()}
|
|
|
|
|
for page_num in page_dict.keys():
|
|
|
|
|
for p in page_dict[page_num]:
|
|
|
|
|
final_dict[page_num] += text_dict[p]
|
|
|
|
|
|
|
|
|
|
# print("Final page_dict:", {k: v for k, v in page_dict.items()})
|
|
|
|
|
return final_dict
|
|
|
|
|
|
|
|
|
|
|
2024-11-13 17:08:19 +00:00
|
|
|
def smart_chunk_ac(
|
2024-11-14 21:16:43 +00:00
|
|
|
text_dict: dict,
|
|
|
|
|
contract_text: str,
|
2024-11-20 19:01:26 +00:00
|
|
|
keyword_mappings: dict = keywords.GROUPED_KEYWORD_MAPPINGS,
|
2024-11-13 17:08:19 +00:00
|
|
|
) -> dict:
|
2024-10-28 23:39:36 +00:00
|
|
|
"""Performs a keyword search on textract outputs to retrieve relevant chunks for a given field.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
text_dict (dict): Dictionary keyed by string page-number and valued by actual textract output page
|
2024-11-20 19:01:26 +00:00
|
|
|
contract_text(str): Contract Text
|
2024-10-28 23:39:36 +00:00
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
ValueError: When the `key_dict` from keyword_mappings has a methodology that is not implemented.
|
|
|
|
|
Current options are `or` and `hierarchy`.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
dict: A dictionary keyed by field (defined by the keys in keyword_mappings) valued by
|
|
|
|
|
corresponding chunks of text (input strings delimited by newlines).
|
|
|
|
|
"""
|
|
|
|
|
field_pages = defaultdict(set) # TODO: remove this, it's unused
|
|
|
|
|
chunks = defaultdict(str)
|
|
|
|
|
|
|
|
|
|
full_context = "\n".join(
|
|
|
|
|
[f"Page {page}:\n{content}" for page, content in text_dict.items()]
|
|
|
|
|
)
|
|
|
|
|
|
2024-11-13 17:08:19 +00:00
|
|
|
for field_group, key_dict in keyword_mappings.items():
|
2024-10-28 23:39:36 +00:00
|
|
|
if key_dict["methodology"] == "or":
|
2024-11-13 17:08:19 +00:00
|
|
|
page_list = ac_smart_chunking.chunk_or(
|
|
|
|
|
text_dict, key_dict["keywords"], key_dict["case_sensitive"]
|
|
|
|
|
)
|
|
|
|
|
keyword_page_dict = ac_smart_chunking.keyword_search(
|
|
|
|
|
text_dict, key_dict["keywords"], key_dict["case_sensitive"]
|
|
|
|
|
)
|
2024-10-28 23:39:36 +00:00
|
|
|
elif key_dict["methodology"] == "hierarchy":
|
2024-11-13 17:08:19 +00:00
|
|
|
page_list = ac_smart_chunking.chunk_hierarchical(
|
|
|
|
|
text_dict, key_dict["keywords"], key_dict["case_sensitive"]
|
|
|
|
|
)
|
|
|
|
|
keyword_page_dict = ac_smart_chunking.keyword_search(
|
|
|
|
|
text_dict, key_dict["keywords"], key_dict["case_sensitive"]
|
|
|
|
|
)
|
|
|
|
|
elif key_dict["methodology"] == "and":
|
|
|
|
|
page_list = ac_smart_chunking.chunk_and(
|
|
|
|
|
text_dict, key_dict["keywords"], key_dict["case_sensitive"]
|
|
|
|
|
)
|
|
|
|
|
keyword_page_dict = ac_smart_chunking.keyword_search(
|
|
|
|
|
text_dict, key_dict["keywords"], key_dict["case_sensitive"]
|
|
|
|
|
)
|
|
|
|
|
elif key_dict["methodology"] == "regex":
|
|
|
|
|
page_list = ac_smart_chunking.chunk_regex(text_dict, key_dict["regex"])
|
|
|
|
|
keyword_page_dict = ac_smart_chunking.keyword_search(
|
|
|
|
|
text_dict, key_dict["keywords"], key_dict["case_sensitive"], True
|
|
|
|
|
)
|
2024-10-28 23:39:36 +00:00
|
|
|
else:
|
|
|
|
|
page_list = list(text_dict.keys())
|
2024-11-13 17:08:19 +00:00
|
|
|
keyword_page_dict = {"All pages": list(text_dict.keys())}
|
2024-10-28 23:39:36 +00:00
|
|
|
raise ValueError(
|
|
|
|
|
"methodology must be 'hierarchy' or 'or' - using entire contract as chunk"
|
|
|
|
|
)
|
2024-11-13 17:08:19 +00:00
|
|
|
if field_group == "initial_page_group":
|
|
|
|
|
keyword_page_dict["initial_pages"] = [1, 2]
|
2024-11-14 21:16:43 +00:00
|
|
|
|
|
|
|
|
# chunks[field_group] = concatenated page texts
|
|
|
|
|
if len(page_list) == len(text_dict.keys()):
|
|
|
|
|
chunks[field_group] = contract_text
|
|
|
|
|
else:
|
|
|
|
|
chunks[field_group] = "\n".join(
|
|
|
|
|
[text_dict[str(page_num)] for page_num in page_list]
|
|
|
|
|
)
|
|
|
|
|
|
2024-11-13 17:08:19 +00:00
|
|
|
chunks[field_group + "_pages"] = keyword_page_dict
|
|
|
|
|
chunks[field_group + "_methodology"] = key_dict["methodology"]
|
|
|
|
|
chunks[field_group + "_case"] = key_dict["case_sensitive"]
|
2024-10-28 23:39:36 +00:00
|
|
|
return chunks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def filter_quick_review(text_dict):
|
|
|
|
|
return {
|
|
|
|
|
page_num: page_text
|
|
|
|
|
for page_num, page_text in text_dict.items()
|
|
|
|
|
if "QUICK REVIEW" not in page_text.upper()
|
|
|
|
|
and "TOP SHEET" not in page_text.upper()
|
|
|
|
|
and "COVER SHEET" not in page_text.upper()
|
|
|
|
|
}
|