Merged in bugfix/effective_date_fix (pull request #504)

DRAFT: Bugfix/effective date fix

* Merged in bugfix/extra-fields (pull request #500)

Do not run Exhibit level prompt if there are no exhibit level fields

* Do not run Exhibit level prompt if there are no exhibit level fields

* Update dynamic test

* Remove test from one-to-n test module

* Update test

* Fix unit test


Approved-by: Alex Galarce

* Merged in bugfix/tin-npi-prompt-fixes (pull request #499)

Bugfix/tin npi prompt fixes

* Change delimiter for other provider information from comma to pipe

* filter out invalid providers

* Update extraction and formatting instructions for provider entities in TIN_NPI_TEMPLATE

* Merged main into bugfix/tin-npi-small-bugfixes

* Refactor deduplication logic to use string_utils for checking empty provider fields

* Merge branch 'bugfix/tin-npi-small-bugfixes' of https://bitbucket.org/aarete/doczy.ai into bugfix/tin-npi-small-bugfixes

* Update deduplication logic to exclude providers with 'UNKNOWN' TIN, NPI, or NAME

* Merge remote-tracking branch 'origin/main' into bugfix/tin-npi-small-bugfixes

* Fix deduplication logic to not drop subsequent names

* Fix formatting of provider names in deduplication logic

* add payer name filtering

* reorder execution order to allow to find payer_name first

* Add handling for previously unfound group providers in identify_group_provider function

* Merge remote-trackin…
* Merged in feature/update-testbed-script (pull request #501)

Feature/update testbed script

* Update preprocessing

* Update preprocessing

* remove quit

* Update date for filename

* Merged main into feature/update-testbed-script


Approved-by: Alex Galarce

* Merged in bugfix/smart-chunks (pull request #503)

missing of text in chunks fixed

* missing of text in chunks fixed


Approved-by: Katon Minhas

* Merged in feature/rework-reimb-tin-npi (pull request #502)

Feature/rework reimb tin npi

* use LLM for reimb_[tin/npi/name]

* Merge remote-tracking branch 'origin/main' into feature/rework-reimb-tin-npi

* Merge remote-tracking branch 'origin/main' into feature/rework-reimb-tin-npi

* deduplicate tin/name/npi before adding to prompt

* Pull out prompt and llm_response in methodology_breakout_single_row for easier debugging

* Refactor reimbursement_tin_npi to handle multiple providers and update valid TIN, NPI, and NAME fields accordingly

* Reorder REIMB_PROV_NAME field in investment_prompts.json

* Update REIMB_PROV_NAME prompt to specify valid values for provider names

* Remove valid_values constraint from prov_name to raise accuracy

* Remove debugging print statements from reimbursement_tin_npi and related functions

* Remove debugging print statements from run_provider_info_fields function

* Merge remote-tracking branch 'origin/main' into feature/rework-reimb-tin-npi

* Remove debugging print statem…
* Merged in feature/preformat-single-quotes-input (pull request #506)

Replace double quotes with single quotes in context strings for parsability

* Replace double quotes with single quotes in context strings for parsability

* Merged main into feature/preformat-single-quotes-input


Approved-by: Katon Minhas

* updated sig pages fnxn

* Merge branch 'main' into bugfix/effective_date_fix

* poetry add pymupdf

* created vision funcs

* Merged main into bugfix/effective_date_fix

* fixed pipeline issue

* updated string_utils_test

* updated test values

* updated extract signature page fxn

* updated test values

* updated test values

* Merged main into bugfix/effective_date_fix

* Prompt template in all-caps

* remove debugging prints

* Merged main into bugfix/effective_date_fix

* Change vision to False by default

* Change vision to False by default

* fix tests

* fix tests


Approved-by: Katon Minhas
This commit is contained in:
VenkataKrishna Reddy Avula
2025-05-07 18:21:09 +00:00
committed by Katon Minhas
parent 806bd9b3bb
commit 0d6bdf9539
16 changed files with 713 additions and 453 deletions
+141 -390
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -29,6 +29,8 @@ sentence-transformers = "^3.4.1"
faiss-cpu = "^1.10.0"
word2number = "^1.1"
orjson = "^3.10.16"
pymupdf = "^1.25.5"
pillow = "^11.2.1"
[tool.poetry.group.dev.dependencies]
black = "^24.10.0"
+9 -2
View File
@@ -233,13 +233,13 @@ if WRITE_TO_S3:
MODEL_ID_CLAUDE3_HAIKU = "anthropic.claude-3-haiku-20240307-v1:0"
MODEL_ID_CLAUDE35_SONNET = "anthropic.claude-3-5-sonnet-20240620-v1:0"
MODEL_ID_CLAUDE2 = "anthropic.claude-instant-v1"
MODEL_ID_CLAUDE35_SONNET_V2 = "anthropic.claude-3-5-sonnet-20241022-v2:0"
MODEL_ID_CLAUDE37_SONNET = "anthropic.claude-3-7-sonnet-20250219-v1:0"
######################################## CLAUDE AND PROMPT TRACKING ########################################
ENABLE_ANSWER_TRACKING = False
ANSWER_TRACKING_DICT = {}
######################################## POSTPROCESSING SETTINGS ########################################
FUZZY_MATCH_THRESHOLD = 0.8
FILE_NAME_COLUMN = 'Contract Name'
@@ -303,3 +303,10 @@ dev_role_name = "doczy-dev-bedrock-lambda-role"
prod_account_id = "211125720533"
prod_role_name = "doczy-prod-bedrock-lambda-role"
############## S3 BUCKETS FOR PDF FILES ##############
DOCZY_PDF_FILES_BUCKET_NAME = "doczy-pdf-files" # sample bucket name added. we can either use this or create a new one
DOCZY_PDF_FILES_BUCKET_S3_URL = f"https://{DOCZY_PDF_FILES_BUCKET_NAME}.s3.us-east-2.amazonaws.com/" # sample s3 url added. we can either use this or create a new one
############## VISION API SETTINGS ##############
ENABLE_VISION = get_arg_value("enable_vision", "False") == "True" # False by default
@@ -103,7 +103,7 @@ def run_one_to_one_prompts(filename, contract_text, text_dict, top_sheet_dict, d
################## RUN SMART CHUNKED PROMPTS ##################
smart_chunked_answers_dict = one_to_one_funcs.run_smart_chunked_fields(
one_to_one_fields, contract_text, filename
one_to_one_fields, contract_text, filename,text_dict
)
################## RUN FULL CONTEXT PROMPTS ##################
@@ -1,5 +1,5 @@
from rapidfuzz import fuzz
import os
import src.prompts.investment_prompts as prompts
import src.utils.llm_utils as llm_utils
import src.utils.string_utils as string_utils
@@ -13,6 +13,9 @@ from src.investment.tin_npi_funcs import (clean_provider_info,
get_provider_info,
identify_group_provider)
from src.prompts.investment_prompts import Field, FieldSet
from src.investment import investment_postprocessing_funcs
from src import postprocessing_funcs as generic_postprocessing_funcs
from src.investment.vision_funcs import get_image_array_based_answer
MAX_CONTEXT_LENGTH = 100000
@@ -115,7 +118,8 @@ def filter_payer_from_providers(providers: list[dict], payer_name: str, threshol
def run_smart_chunked_fields(one_to_one_fields: FieldSet,
contract_text: str,
filename: str) -> dict[str, str]:
filename: str,
text_dict: dict)-> dict[str, str]:
"""Process fields that require smart chunking and return the extracted answers.
Args:
@@ -209,14 +213,54 @@ def run_smart_chunked_fields(one_to_one_fields: FieldSet,
except Exception as e:
print(f"Error processing field {field}: {type(e)}, {str(e)}")
answers_dict[field.field_name] = f"Error: {str(e)}"
if "AARETE_DERIVED_EFFECTIVE_DT" in answers_dict:
answers_dict = check_and_update_effective_date(answers_dict, text_dict, filename)
# even after running the vision api, if the effective date is still N/A, we need to run the prompt again with full context
if "AARETE_DERIVED_EFFECTIVE_DT" in answers_dict and string_utils.is_empty(answers_dict.get("AARETE_DERIVED_EFFECTIVE_DT")):
# if the effective date is N/A, we need to run the prompt again with full context
if answers_dict['AARETE_DERIVED_EFFECTIVE_DT'] == "N/A":
smart_chunked_fields.update_field_attr( field_name = 'AARETE_DERIVED_EFFECTIVE_DT',
attr = 'field_type',
value = "full_context")
smart_chunked_fields.update_field_attr( field_name = 'AARETE_DERIVED_EFFECTIVE_DT',
attr = 'field_type',
value = "full_context")
return answers_dict
def check_and_update_effective_date(answers_dict, text_dict, filename):
"""
Checks and updates the effective date in the answers dictionary. If the effective date is empty,
it attempts to extract the effective date using vision-based LLM or full context processing.
Args:
answers_dict (dict): Dictionary containing extracted answers.
text_dict (dict): Dictionary containing text data, typically organized by pages or sections.
filename (str): The name of the file being processed.
Returns:
dict: Updated answers dictionary with the effective date.
"""
# print(f"Effective date for {filename} from smart chunking: {answers_dict.get('AARETE_DERIVED_EFFECTIVE_DT')}")
# Postprocess the effective date
answers_dict["AARETE_DERIVED_EFFECTIVE_DT"] = generic_postprocessing_funcs.date_postprocessing(
answers_dict.get("AARETE_DERIVED_EFFECTIVE_DT", "N/A")
)
answers_dict["AARETE_DERIVED_EFFECTIVE_DT"] = investment_postprocessing_funcs.validate_and_reformat_date(
answers_dict["AARETE_DERIVED_EFFECTIVE_DT"]
)
# If the effective date is empty, Get the effective date using the vision-based LLM
if config.ENABLE_VISION and string_utils.is_empty(answers_dict.get("AARETE_DERIVED_EFFECTIVE_DT")):
# if answers_dict.get("AARETE_DERIVED_EFFECTIVE_DT") in ["N/A", "UNKNOWN"]:
effective_date_page_numbers = list(string_utils.extract_effective_date_pages(text_dict, filename).keys())
pdf_base_path = "/root/doczy.ai/fieldExtraction/src/investment/pdf_files" # local path
pdf_path = os.path.join(pdf_base_path, filename.replace("txt", "pdf"))
# pdf_path = os.path.join(DOCZY_PDF_FILES_BUCKET_S3_URL, filename.replace("txt", "pdf")) # s3 path
image_answer_dict = get_image_array_based_answer(
pdf_path, effective_date_page_numbers, filename, fields=["AARETE_DERIVED_EFFECTIVE_DT"]
)
answers_dict["AARETE_DERIVED_EFFECTIVE_DT"] = image_answer_dict.get("AARETE_DERIVED_EFFECTIVE_DT", "N/A")
return answers_dict
def run_full_context_fields(
one_to_one_fields: FieldSet,
@@ -240,4 +284,4 @@ def run_full_context_fields(
full_context_answers_dict = {}
for field in full_context_fields.list_fields():
full_context_answers_dict[field] = f"Error: {str(e)}"
return full_context_answers_dict
return full_context_answers_dict
@@ -0,0 +1,111 @@
import fitz # PyMuPDF
import base64
from typing import List
from io import BytesIO
from PIL import Image # Pillow library
from src.config import DOCZY_PDF_FILES_BUCKET_S3_URL
from src.utils import io_utils
import src.prompts.investment_prompts as prompts
import src.utils.llm_utils as llm_utils
import src.utils.string_utils as string_utils
def get_image_array_based_answer(
pdf_path: str,
effective_date_page_numbers: List[int],
filename: str,
fields: List[str] = ["AARETE_DERIVED_EFFECTIVE_DT"]
) -> dict:
"""
Processes specific pages of a PDF, converts them to a base64-encoded image,
and retrieves answers using the Claude 3.5 Sonnet model.
Args:
pdf_path (str): Path to the PDF file.
effective_date_page_numbers (List[int]): List of page numbers to process and extract data from.
filename (str): Name of the file being operated upon, used for logging or tracking purposes.
fields (List[str]): List of fields to extract from the response. Defaults to ["AARETE_DERIVED_EFFECTIVE_DT"].
Returns:
dict: Extracted answers from the response in JSON format. Example:
{"AARETE_DERIVED_EFFECTIVE_DT": "YYYY/MM/DD"}
"""
# Convert specified PDF pages to a base64-encoded image
base64_images = process_pdf_pages_to_base64_images(pdf_path, filename, effective_date_page_numbers)
# print("No of base64 images:", len(base64_images)) # Log a snippet of the base64 data for debugging
# Prepare the prompt for the LLM
extended_prompt = prompts.effective_date_fix_prompt()
# Invoke the LLM to get the response
response_dict = llm_utils.get_image_array_answer(extended_prompt, filename, base64_images)
return response_dict
def process_pdf_pages_to_base64_images(pdf_path: str, filename: str, page_numbers: List[int]) -> List[str]:
"""
Convert multiple specific pages of a PDF into a list of base64-encoded JPEG images.
Inputs:
pdf_path (str): Path to the PDF file.
page_numbers (List[int]): List of 1-based page numbers to process.
Output:
List[str]: List of base64-encoded JPEG images, one for each page.
Functionality:
- Renders each specified PDF page into a high-resolution image.
- Converts each rendered image into a base64-encoded JPEG.
- Returns the list of base64-encoded images.
"""
page_numbers = sorted({int(float(x)) for x in page_numbers}) # Ensure unique and sorted page numbers
if not page_numbers:
raise ValueError("No valid page numbers provided")
try:
# Check if the PDF path is an S3 URL
if pdf_path.startswith("s3://"):
temp_pdf_path = io_utils.read_s3_pdf(pdf_path)
else:
temp_pdf_path = pdf_path
with fitz.open(temp_pdf_path) as pdf_document:
zoom = 2 # Resolution multiplier
mat = fitz.Matrix(zoom, zoom)
base64_images = []
for page_num in page_numbers:
page_num = int(page_num) # Ensure page number is an integer
page_idx = page_num - 1
if page_idx < 0 or page_idx >= len(pdf_document):
raise ValueError(f"Page {page_num} is out of range")
# Get the page and convert to image
page = pdf_document[page_idx]
pix = page.get_pixmap(matrix=mat)
img_bytes = pix.tobytes("jpeg")
# Save the image to a file (optional)
# with open(f"page_{page_num}_{filename}.jpg", "wb") as img_file:
# img_file.write(img_bytes) # saving for debugging purposes
# Resize the image to ensure it is less than 1 megapixel
img = Image.open(BytesIO(img_bytes))
max_pixels = 1_000_000 # 1 megapixel
if img.width * img.height > max_pixels:
scale_factor = (max_pixels / (img.width * img.height)) ** 0.5
new_width = int(img.width * scale_factor)
new_height = int(img.height * scale_factor)
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
# Save resized image to bytes
output_buffer = BytesIO()
img.save(output_buffer, format="JPEG")
img_bytes = output_buffer.getvalue()
# Convert to base64 JPEG
base64_image = base64.b64encode(img_bytes).decode("utf-8")
base64_images.append(base64_image)
if not base64_images:
raise ValueError("No valid pages provided")
return base64_images
except (ValueError, fitz.FileDataError) as e:
print(f"Error processing PDF pages: {str(e)}")
raise
+1 -1
View File
@@ -1183,7 +1183,7 @@ def date_postprocessing(date: str) -> str:
if is_empty(date):
return "N/A"
prompt = investment_prompts.date_fix_prompt(date)
prompt = investment_prompts.DATE_FIX_PROMPT(date)
response = llm_utils.invoke_claude(prompt, config.MODEL_ID_CLAUDE35_SONNET, "date_fix")
return extract_text_from_delimiters(response, Delimiter.PIPE)
@@ -51,6 +51,9 @@ def split_text(text: str) -> dict[str, str]:
def clean_law_symbols(contract_text):
if not contract_text:
return contract_text
# First correction: Replace '$$' with '$'
contract_text = contract_text.replace("$$", "$")
contract_text = re.sub(r"(U\.?S\.?C\.?) \$", r"\", contract_text) # replaces $ with § when it follows abbreviations like "U.S.C." (United States Code).
@@ -504,7 +504,7 @@ def ONE_TO_ONE_SINGLE_FIELD_TEMPLATE(context, question):
{PIPE_FORMAT_INSTRUCTIONS}
"""
def date_fix_prompt(context:str) -> str:
def DATE_FIX_PROMPT(context:str) -> str:
return f"""Given a date string, convert it to YYYY/MM/DD format following these rules:
Input: Text potentially containing a date
Output: Date in YYYY/MM/DD format, or input if it's a duration, or "N/A" if ambiguous/incomplete
@@ -546,7 +546,7 @@ Please analyze this date:
{PIPE_FORMAT_INSTRUCTIONS}
"""
def derived_term_date_prompt(effective_date:str=None, termination_information:str=None) -> str:
def DERIVED_TERM_DATE_PROMPT(effective_date:str=None, termination_information:str=None) -> str:
"""
Generates a prompt to extract the termination date from a legal contract.
@@ -561,7 +561,7 @@ def derived_term_date_prompt(effective_date:str=None, termination_information:st
@cache
def invoke_derived_term_date(effective_date:str=None, termination_information:str=None) -> str:
prompt = derived_term_date_prompt(effective_date, termination_information)
prompt = DERIVED_TERM_DATE_PROMPT(effective_date, termination_information)
response = llm_utils.invoke_claude(prompt, MODEL_ID_CLAUDE35_SONNET, "derive_term_date")
return extract_text_from_delimiters(response, Delimiter.PIPE)
@@ -730,3 +730,13 @@ def GROUP_TIN_NPI_TEMPLATE(key_sections: str, provider_list: str) -> str:
Feel free to justify your answer, but enclose your final answer in |pipes|.
"""
def EFFECTIVE_DATE_FIX_PROMPT() -> str:
return """Extract the effective date of the contract or contract effective date or Effective Date of Agreement or Effective Date of Amendment or the amendment effective date from the given image data. ALSO LOOK FOR dates in phrases that semantically indicate an effective date (may or may not be explictly labelled as effective date), even if they do not exactly match the phrases above.\nExamples include but ARE NOT LIMITED TO:\nPhrases indicating when an contract or amendment begins or takes effect, Phrases specifying the start date of the contract or amendment, Phrases defining when the contract or amendment becomes operational, or Any phrase indicating contract or amendment creation or execution date.\nIt is possible that the contract will specify that the effective date is derived from elsewhere in the contract. Give the date in a JSON FORMAT with AARETE_DERIVED_EFFECTIVE_DATE as the key and effective date value in YYYY/MM/DD format as the value.
Rules:
1. Add 2000 to 2-digit years 0-49, 1900 to 50-99
2. Return N/A if: missing/ambiguous month or year components, or invalid dates. If just the day is missing, assume it's the first of the month.
3. For ambiguous input cases, assume American format (MM/DD/YYYY) and apply YYYY/MM/DD formatting
4. Any day values with spaces or leading zeros (e.g., '0 1' or '01') are interpreted as single-digit days
Return N/A only for date if NO valid effective date is found. give the answer in the format: {{\"AARETE_DERIVED_EFFECTIVE_DT\": \"YYYY/MM/DD\"}}. nothing else."""
+27
View File
@@ -10,6 +10,7 @@ from src import config
from src.investment.code_funcs import crosswalk_levels, get_mappings
import src.constants.investment_values as investment_values
from src.utils.string_utils import datetime_str
import tempfile
def read_local(file_path): # io_utils.py
@@ -50,6 +51,32 @@ def list_s3_files(prefix=config.S3_PREFIX, bucket=config.S3_BUCKET): # io_utils.
response = config.S3_CLIENT.list_objects_v2(Bucket=bucket, Prefix=prefix)
return [item["Key"] for item in response.get("Contents", [])]
def read_s3_pdf(pdf_path):
"""
Downloads a PDF file from an S3 bucket and returns a temporary file path.
Args:
pdf_path (str): The S3 URL of the PDF file (e.g., s3://bucket-name/path/to/file.pdf).
Returns:
str: The local temporary file path of the downloaded PDF.
"""
s3_client = config.S3_CLIENT
s3_parts = pdf_path[5:].split("/", 1)
bucket = s3_parts[0]
prefix = s3_parts[1] if len(s3_parts) > 1 else ""
try:
response = s3_client.get_object(Bucket=bucket, Key=prefix)
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_pdf:
temp_pdf.write(response["Body"].read())
temp_pdf_path = temp_pdf.name
return temp_pdf_path
except ClientError as e:
print(f"Error fetching PDF from S3: {e}")
return None
def read_s3():
s3_client = config.S3_CLIENT
+142 -1
View File
@@ -15,7 +15,7 @@ from collections import OrderedDict
import hashlib
import inspect
import csv
import base64
def update_stats(filename, tokens_sent, tokens_received, elapsed_time, model_type):
# Update per-file statistics
@@ -164,6 +164,54 @@ def invoke_claude(prompt, model_id, filename, max_tokens=4096):
return response
def get_cache_key_with_image_array(prompt, model_id, image_base64_list):
"""Generates a unique hash key for caching with a list of base64 images."""
combined_images = ":".join(image_base64_list) # Combine all base64 images into a single string
return hashlib.sha256(f"{model_id}:{prompt}:{combined_images}".encode()).hexdigest()
def invoke_claude_with_image_array(prompt, model_id, filename, base64_images,max_tokens=4096):
cache_key = get_cache_key_with_image_array(prompt, model_id, base64_images)
# print(f"Cache key for {filename}: {cache_key}") # printing for debugging
# Check cache first - use get() method from LRUCache
if cache_key in claude_cache:
claude_cache.move_to_end(cache_key) # Mark as recently used
return claude_cache[cache_key]
# Select execution mode
if config.RUN_MODE == "local":
if model_id == config.MODEL_ID_CLAUDE35_SONNET:
response = local_claude_3_with_image_array(prompt, model_id, filename, max_tokens,base64_images)
print(f"Eff Dt Response from local Claude 3 for {filename}", response)
input_cost_per_1k, output_cost_per_1k = 0.003, 0.015
elif config.RUN_MODE == "ec2":
if model_id == config.MODEL_ID_CLAUDE2:
response = ec2_claude2(prompt, model_id, filename, max_tokens)
input_cost_per_1k, output_cost_per_1k = 0.008, 0.024
else:
print("Usage: python local_main.py <-local> OR python main.py <ec2>")
raise
# Store response in cache with LRU eviction
if len(claude_cache) >= CACHE_LIMIT:
claude_cache.popitem(last=False) # Remove the oldest (least recently used) item
claude_cache[cache_key] = response # Insert new response
claude_cache.move_to_end(cache_key) # Mark as recently used
# Cost logging
current_frame = inspect.currentframe()
caller_frame = current_frame.f_back
caller_name = caller_frame.f_code.co_name
del current_frame, caller_frame # Avoid reference cycles
cost_log_entry = create_cost_log_entry(
filename, caller_name, prompt, response, input_cost_per_1k, output_cost_per_1k
)
with open(config.COST_LOG_NAME, 'a', newline="", encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=config.COST_LOG_FIELDS)
writer.writerow(cost_log_entry)
return response
# Claude calls
def local_claude_2(prompt, model_id, filename, max_tokens):
@@ -247,6 +295,94 @@ def local_claude_3(
raise Exception("Max retries exceeded")
def local_claude_3_with_image_array(
prompt, model_id, filename, max_tokens, base64_images, max_retries=5, initial_delay=1, image_media_type="image/png"
):
"""
Processes a prompt along with an image in base64 format using the Claude 3 model via AWS Bedrock.
Args:
prompt (str): Input prompt to LLM.
model_id (str): Model ID to use.
filename (str): Name of the file being operated upon.
max_tokens (int): Maximum tokens to sample.
image_base64 (str): Base64 encoded image data.
max_retries (int): Maximum number of retries for the request.
initial_delay (int): Initial delay for exponential backoff.
image_media_type (str): Media type of the image (default: "image/jpeg").
Returns:
str: Response text from the model.
"""
if config.ENABLE_ANSWER_TRACKING and prompt in config.ANSWER_TRACKING_DICT.keys():
return config.ANSWER_TRACKING_DICT[prompt]
# Create the request body with the new message format
body = json.dumps(
{
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": max_tokens,
"temperature": 0.0,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Images:"
},
*[
{
"type": "image",
"source": {
"type": "base64",
"media_type": image_media_type,
"data": image_base64,
},
}
for image_base64 in base64_images
],
{
"type": "text",
"text": prompt
}
],
}
],
}
)
for attempt in range(max_retries):
try:
response = config.BEDROCK_RUNTIME.invoke_model(
body=body,
modelId=model_id,
accept="application/json",
contentType="application/json",
)
response_body = json.loads(response.get("body").read())
response_text = response_body["content"][0]["text"]
if config.ENABLE_ANSWER_TRACKING:
config.ANSWER_TRACKING_DICT[prompt] = response_text
return response_text
except ClientError as e:
if e.response["Error"]["Code"] in [
"ThrottlingException",
"TooManyRequestsException",
]:
if attempt < max_retries - 1:
delay = (2**attempt + random.uniform(0, 1)) * initial_delay
time.sleep(delay)
else:
raise
else:
raise
raise Exception("Max retries exceeded")
def ec2_claude2(prompt, model_id, filename, max_tokens):
start_time = time.time()
body = json.dumps(
@@ -452,3 +588,8 @@ def get_ac_answer(prompt, filename, fields):
# response_dict = dict_operations.secondary_string_to_dict(response_text, filename)
response_dict = string_utils.json_parsing_search(response_text, fields)
return response_dict
def get_image_array_answer(prompt,filename,base64_images):
response_text = invoke_claude_with_image_array(prompt,config.MODEL_ID_CLAUDE35_SONNET,filename,base64_images)
response_dict = json.loads(response_text)
return response_dict
+51 -5
View File
@@ -384,7 +384,6 @@ def get_exhibit_chunk(text_dict: dict,
def datetime_str():
return datetime.now().strftime("[%Y-%m-%d %H:%M:%S]")
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
@@ -408,10 +407,57 @@ def extract_signature_page(text_dict: dict, filename: str) -> dict:
if textract_pattern.search(page_text):
signature_pages[page_num] = page_text
# If no pages found with textract pattern, fall back to "signature" keyword
# If no pages found with textract pattern, try with keyword
if not signature_pages:
for page_num, page_text in text_dict.items():
if "signature" in page_text.lower():
signature_pages[page_num] = page_text
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
return signature_pages
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.
elif page_num == 1:
effective_date_pages[page_num] = page_text
return effective_date_pages
+36 -36
View File
@@ -196,42 +196,42 @@ class TestStringUtils:
assert result == expected
@pytest.mark.parametrize("text_dict, filename, expected", [
(
{"1": "This page has 2 signatures.", "2": "None here.", "3": "Signature of the party (but ignored)"},
"test_file.txt",
{"1": "This page has 2 signatures."}
),
(
{"1": "No relevant content here.", "2": "Nor over here."},
"test_file.txt",
{}
),
(
{"1": "This page has a signature.", "2": "Another signature is found here."},
"test_file.txt",
{"1": "This page has a signature.", "2": "Another signature is found here."}
),
(
{"1": "This page has 1 signature.", "2": "No relevant content.", "3": "Signature block here. But ignored bc of the first textract-like page"},
"test_file.txt",
{"1": "This page has 1 signature."}
),
(
{"1": "No signed names at all.", "2": "Just some random text."},
"test_file.txt",
{}
),
(
{"1": "This page has a signature.", "2": "Signature is mentioned here too."},
"test_file.txt",
{"1": "This page has a signature.", "2": "Signature is mentioned here too."}
),
(
{},
"test_file.txt",
{}
),
])
(
{"1": "This page has 2 signatures.", "2": "None here.", "3": "Signature of the party (but ignored)"},
"test_file.txt",
{"1": "This page has 2 signatures."}
),
(
{"1": "No relevant content here.", "2": "Nor over here."},
"test_file.txt",
{}
),
(
{"1": "This page has a signature.", "2": "signature page follow.","3": "Signature of the party (but ignored)"},
"test_file.txt",
{"2": "signature page follow.","3": "Signature of the party (but ignored)"}
),
(
{"1": "This page has 1 signature.", "2": "No relevant content.", "3": "Signature block here. But ignored bc of the first textract-like page"},
"test_file.txt",
{"1": "This page has 1 signature."}
),
(
{"1": "No signed names at all.", "2": "Just some random text."},
"test_file.txt",
{}
),
(
{"1": "some random text", "2": "Signature authorization", "3": "Signature of the party"},
"test_file.txt",
{"2": "Signature authorization", "3": "Signature of the party"}
),
(
{},
"test_file.txt",
{}
),
])
def test_extract_signature_page(self, text_dict, filename, expected):
result = extract_signature_page(text_dict, filename)
assert result == expected
@@ -1,27 +1,27 @@
import pytest
from src.prompts.investment_prompts import derived_term_date_prompt
from src.prompts.investment_prompts import DERIVED_TERM_DATE_PROMPT
from src.prompts.investment_prompts import PIPE_FORMAT_INSTRUCTIONS
def test_derived_term_date_prompt_with_all_params():
effective_date = "January 1, 2024"
termination_information = "The contract terminates after 3 years."
expected_output = f"The following are excerpts from a legal contract related to effective and termination dates. Determine the initial termination date of the contract, excluding any renewals or extensions.\nIf termination information comes in as a duration, derive the termination date exclusive of the final day. For example, if effective date is 2023/02/01 and termination information is 'two years', termination date would be 2025/01/31 and not 2025/02/01. If effective date is 2023/02/01 and termination information is 'year-to-year', termination date would be 2024/01/31.\nIf the termination information comes in as a date, return that date. For example, if effective date is 2023/02/01 and termination information is 2024/07/01, return 2024/07/01.\nEffective Date: January 1, 2024\nTermination Information: The contract terminates after 3 years.\nOnly provide the first termination date, ignoring any automatic or optional renewals. Provide the date in YYYY/MM/DD format only. \n{PIPE_FORMAT_INSTRUCTIONS}"
assert derived_term_date_prompt(effective_date, termination_information) == expected_output
assert DERIVED_TERM_DATE_PROMPT(effective_date, termination_information) == expected_output
def test_derived_term_date_prompt_with_only_effective_date():
effective_date = "January 1, 2024"
termination_information = None
expected_output = f"The following are excerpts from a legal contract related to effective and termination dates. Determine the initial termination date of the contract, excluding any renewals or extensions.\nIf termination information comes in as a duration, derive the termination date exclusive of the final day. For example, if effective date is 2023/02/01 and termination information is 'two years', termination date would be 2025/01/31 and not 2025/02/01. If effective date is 2023/02/01 and termination information is 'year-to-year', termination date would be 2024/01/31.\nIf the termination information comes in as a date, return that date. For example, if effective date is 2023/02/01 and termination information is 2024/07/01, return 2024/07/01.\nEffective Date: January 1, 2024\nTermination Information: None\nOnly provide the first termination date, ignoring any automatic or optional renewals. Provide the date in YYYY/MM/DD format only. \n{PIPE_FORMAT_INSTRUCTIONS}"
assert derived_term_date_prompt(effective_date, termination_information) == expected_output
assert DERIVED_TERM_DATE_PROMPT(effective_date, termination_information) == expected_output
def test_derived_term_date_prompt_with_only_termination_information():
effective_date = None
termination_information = "The contract terminates after 3 years."
expected_output = f"The following are excerpts from a legal contract related to effective and termination dates. Determine the initial termination date of the contract, excluding any renewals or extensions.\nIf termination information comes in as a duration, derive the termination date exclusive of the final day. For example, if effective date is 2023/02/01 and termination information is 'two years', termination date would be 2025/01/31 and not 2025/02/01. If effective date is 2023/02/01 and termination information is 'year-to-year', termination date would be 2024/01/31.\nIf the termination information comes in as a date, return that date. For example, if effective date is 2023/02/01 and termination information is 2024/07/01, return 2024/07/01.\nEffective Date: None\nTermination Information: The contract terminates after 3 years.\nOnly provide the first termination date, ignoring any automatic or optional renewals. Provide the date in YYYY/MM/DD format only. \n{PIPE_FORMAT_INSTRUCTIONS}"
assert derived_term_date_prompt(effective_date, termination_information) == expected_output
assert DERIVED_TERM_DATE_PROMPT(effective_date, termination_information) == expected_output
def test_derived_term_date_prompt_with_no_params():
effective_date = None
termination_information = None
expected_output = f"The following are excerpts from a legal contract related to effective and termination dates. Determine the initial termination date of the contract, excluding any renewals or extensions.\nIf termination information comes in as a duration, derive the termination date exclusive of the final day. For example, if effective date is 2023/02/01 and termination information is 'two years', termination date would be 2025/01/31 and not 2025/02/01. If effective date is 2023/02/01 and termination information is 'year-to-year', termination date would be 2024/01/31.\nIf the termination information comes in as a date, return that date. For example, if effective date is 2023/02/01 and termination information is 2024/07/01, return 2024/07/01.\nEffective Date: None\nTermination Information: None\nOnly provide the first termination date, ignoring any automatic or optional renewals. Provide the date in YYYY/MM/DD format only. \n{PIPE_FORMAT_INSTRUCTIONS}"
assert derived_term_date_prompt(effective_date, termination_information) == expected_output
assert DERIVED_TERM_DATE_PROMPT(effective_date, termination_information) == expected_output
Generated
+117 -1
View File
@@ -740,6 +740,105 @@ files = [
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
name = "pillow"
version = "11.2.1"
description = "Python Imaging Library (Fork)"
optional = false
python-versions = ">=3.9"
files = [
{file = "pillow-11.2.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047"},
{file = "pillow-11.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95"},
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61"},
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1"},
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c"},
{file = "pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d"},
{file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97"},
{file = "pillow-11.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579"},
{file = "pillow-11.2.1-cp310-cp310-win32.whl", hash = "sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d"},
{file = "pillow-11.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad"},
{file = "pillow-11.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2"},
{file = "pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70"},
{file = "pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf"},
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7"},
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8"},
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600"},
{file = "pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788"},
{file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e"},
{file = "pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e"},
{file = "pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6"},
{file = "pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193"},
{file = "pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7"},
{file = "pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f"},
{file = "pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b"},
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d"},
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4"},
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d"},
{file = "pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4"},
{file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443"},
{file = "pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c"},
{file = "pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3"},
{file = "pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941"},
{file = "pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb"},
{file = "pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28"},
{file = "pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830"},
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0"},
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1"},
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f"},
{file = "pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155"},
{file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14"},
{file = "pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b"},
{file = "pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2"},
{file = "pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691"},
{file = "pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c"},
{file = "pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22"},
{file = "pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7"},
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16"},
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b"},
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406"},
{file = "pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91"},
{file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751"},
{file = "pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9"},
{file = "pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd"},
{file = "pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e"},
{file = "pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681"},
{file = "pillow-11.2.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7491cf8a79b8eb867d419648fff2f83cb0b3891c8b36da92cc7f1931d46108c8"},
{file = "pillow-11.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b02d8f9cb83c52578a0b4beadba92e37d83a4ef11570a8688bbf43f4ca50909"},
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:014ca0050c85003620526b0ac1ac53f56fc93af128f7546623cc8e31875ab928"},
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3692b68c87096ac6308296d96354eddd25f98740c9d2ab54e1549d6c8aea9d79"},
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:f781dcb0bc9929adc77bad571b8621ecb1e4cdef86e940fe2e5b5ee24fd33b35"},
{file = "pillow-11.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2b490402c96f907a166615e9a5afacf2519e28295f157ec3a2bb9bd57de638cb"},
{file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd6b20b93b3ccc9c1b597999209e4bc5cf2853f9ee66e3fc9a400a78733ffc9a"},
{file = "pillow-11.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4b835d89c08a6c2ee7781b8dd0a30209a8012b5f09c0a665b65b0eb3560b6f36"},
{file = "pillow-11.2.1-cp39-cp39-win32.whl", hash = "sha256:b10428b3416d4f9c61f94b494681280be7686bda15898a3a9e08eb66a6d92d67"},
{file = "pillow-11.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ebce70c3f486acf7591a3d73431fa504a4e18a9b97ff27f5f47b7368e4b9dd1"},
{file = "pillow-11.2.1-cp39-cp39-win_arm64.whl", hash = "sha256:c27476257b2fdcd7872d54cfd119b3a9ce4610fb85c8e32b70b42e3680a29a1e"},
{file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156"},
{file = "pillow-11.2.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772"},
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363"},
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0"},
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01"},
{file = "pillow-11.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193"},
{file = "pillow-11.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013"},
{file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed"},
{file = "pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c"},
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd"},
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076"},
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b"},
{file = "pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f"},
{file = "pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044"},
{file = "pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6"},
]
[package.extras]
docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
fpx = ["olefile"]
mic = ["olefile"]
test-arrow = ["pyarrow"]
tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"]
typing = ["typing-extensions"]
xmp = ["defusedxml"]
[[package]]
name = "platformdirs"
version = "4.3.6"
@@ -895,6 +994,23 @@ files = [
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pymupdf"
version = "1.25.5"
description = "A high performance Python library for data extraction, analysis, conversion & manipulation of PDF (and other) documents."
optional = false
python-versions = ">=3.9"
files = [
{file = "pymupdf-1.25.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cde4e1c9cfb09c0e1e9c2b7f4b787dd6bb34a32cfe141a4675e24af7c0c25dd3"},
{file = "pymupdf-1.25.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5a35e2725fae0ab57f058dff77615c15eb5961eac50ba04f41ebc792cd8facad"},
{file = "pymupdf-1.25.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d94b800e9501929c42283d39bc241001dd87fdeea297b5cb40d5b5714534452f"},
{file = "pymupdf-1.25.5-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee22155d3a634642d76553204867d862ae1bdd9f7cf70c0797d8127ebee6bed5"},
{file = "pymupdf-1.25.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6ed7fc25271004d6d3279c20a80cb2bb4cda3efa9f9088dcc07cd790eca0bc63"},
{file = "pymupdf-1.25.5-cp39-abi3-win32.whl", hash = "sha256:65e18ddb37fe8ec4edcdbebe9be3a8486b6a2f42609d0a142677e42f3a0614f8"},
{file = "pymupdf-1.25.5-cp39-abi3-win_amd64.whl", hash = "sha256:7f44bc3d03ea45b2f68c96464f96105e8c7908896f2fb5e8c04f1fb8dae7981e"},
{file = "pymupdf-1.25.5.tar.gz", hash = "sha256:5f96311cacd13254c905f6654a004a0a2025b71cabc04fda667f5472f72c15a0"},
]
[[package]]
name = "pytest"
version = "8.3.3"
@@ -1241,4 +1357,4 @@ zstd = ["zstandard (>=0.18.0)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "b437b66e0a7d68067a1615d3adbcd90a7ae55d4230f6ed639de8b1b143dcb92d"
content-hash = "11f240679ad504f8cfc3a866067918339f2dd77f3764b1bbf77f992dac4e7bb3"
+2
View File
@@ -11,6 +11,8 @@ python = "^3.12"
pandas = "^2.2.3"
boto3 = "^1.35.40"
anthropic = "^0.36.0"
pymupdf = "^1.25.5"
pillow = "^11.2.1"
[tool.poetry.group.dev.dependencies]
pytest = "^8.3.3"