Merged in feature/lesser-table-caching-refactor-hybrid (pull request #847)

Feature/lesser table caching refactor hybrid

* chore: Remove unused duplicate main.py from shared pipeline

* fix: Correct crosswalk paths in aarete_derived.py

* chore: Remove unused documentation files from fieldExtraction

* docs: Add documentation files to documentation folder

* docs: Update README with uv setup, expanded project structure, and branching conventions

* docs: Add uv installation steps with Ubuntu/WSL emphasis

* Enable prompt caching for all remaining LLM calls

- Add _INSTRUCTION() functions for: EXHIBIT_HEADER, EXHIBIT_LINKAGE,
  EXHIBIT_TITLE_MATCH, DATE_FIX, DERIVED_TERM_DATE, CHECK_PROVIDER_NAME_MATCH,
  SPECIAL_CASE_ASSIGNMENT
- Update all invoke_claude() calls in saas and clover pipelines to use
  cache=True with corresponding _INSTRUCTION() functions
- Add new instructions to get_cacheable_instructions() for cache warming
- Update tests for new instruction functions

Functions now using caching:
- prompt_exhibit_level
- prompt_exhibit_lesser (EXHIBIT_LEVEL_LESSER_OF)
- prompt_fee_schedule_breakout
- prompt_grouper_breakout
- prompt_special_case_assignment
- prompt_exhibit_linkage
- prompt_exhibit_header
- prompt_smart_chunked (ONE_TO_ONE templates)
- prompt_date_fix
- prompt_derived_term_date
- prompt_exhibit_title_match
- provider_name_match_check

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Reorder

* feat: Add bcbs_promise client pipeline with OFFSET_TERM extraction

- Add new bcbs_promise client with HSC-based OFFSET_TERM field extraction
- Extract full paragraph text of offset/recoupment provisions from contracts
- Derive OFFSET_INDICATOR (Y/N) from OFFSET_TERM presence
- Fix reorder_columns to preserve extra columns not in COLUMN_ORDER
- Update QC/QA output path to outputs/qc_qa/

* fix: Update dev deps and test assertions for QC/QA output path

- Add pytest/pytest-mock to dev dependencies for mypy type checking
- Update test assertions to expect outputs/qc_qa instead of qa_qc_output

* style: Apply black formatting to prompt_templates.py

* Merge main, move scripts

* Archive some scripts

* update py version

* remove .py version file

* Remove ASCII characters

* Restore testbed code

* restore tracking

* Update testbed metrics

* Enable prompt caching for CODE_LAST_CHECK, FILL_BILL_TYPE, DUAL_LOB_CHECK, and GROUPER_BREAKOUT

- Add CODE_LAST_CHECK_INSTRUCTION() for service specificity classification
- Add FILL_BILL_TYPE_INSTRUCTION() for bill type code determination
- Add DUAL_LOB_CHECK_INSTRUCTION() for Medicare/Medicaid classification
- Update code_funcs.py to use caching for CODE_LAST_CHECK, FILL_BILL_TYPE, GROUPER_BREAKOUT
- Update postprocessing_funcs.py to use caching for DUAL_LOB_CHECK
- Add new instructions to get_cacheable_instructions() for cache warming
- Add unit tests for new instruction functions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix postprocessing_funcs to remove invalid columns

* Merge branch 'main' into feature/lesser-table-caching-refactor-hybrid

* Revert prompt caching changes from aed1b73c

* update formatting

* Update imports


Approved-by: Sha Brown
Approved-by: Praneel Panchigar
This commit is contained in:
Katon Minhas
2026-01-26 16:52:55 +00:00
parent dcec3f4b7a
commit afb6d5185d
584 changed files with 17548 additions and 33208 deletions
+49
View File
@@ -0,0 +1,49 @@
import pandas as pd
import os
def read_and_compare_csvs(csv_file1, csv_file2):
# Read CSV files into pandas DataFrames
df1 = pd.read_csv(csv_file1)
df2 = pd.read_csv(csv_file2)
df2 = df2[df1.columns]
# Check for any differences
diff = (df1 != df2) & df1.notnull() & df2.notnull()
# Create a writer for Excel
writer = pd.ExcelWriter("data/differences.xlsx", engine="xlsxwriter")
df2.to_excel(writer, sheet_name="Differences", index=False)
# Get the xlsxwriter workbook and worksheet objects
workbook = writer.book
worksheet = writer.sheets["Differences"]
# Define a format for changed cells
highlight_fmt = workbook.add_format({"font_color": "red", "bg_color": "yellow"})
# Apply formatting to the changed cells
for col in df1.columns:
col_idx = (
df1.columns.get_loc(col) + 1
) # Excel index starts from 1, and there is an index column
for row in diff.index[diff[col]]:
value = df2.at[row, col]
if pd.isna(value):
value = (
"NaN" # Change this if you want a different representation for NaN
)
worksheet.write(row + 1, col_idx, value, highlight_fmt)
# Close the Pandas Excel writer and output the Excel file
writer.close()
# Example usage
base_path = "output/57-2883440-Cordia Anderson-Hopkins LCSW-ICMProviderAgreement_221673"
read_and_compare_csvs(
os.path.join(base_path, "combined_results_post_processed.csv"),
os.path.join(base_path, "test_3.5.csv"),
)
+31
View File
@@ -0,0 +1,31 @@
import src.utils.embedding_utils as embedding_utils
from src.utils.crosswalk_utils import CrosswalkBuilder
import src.codes.code_funcs as code_funcs
import os
import pandas as pd
from sentence_transformers import SentenceTransformer
# Load model
roberta_model = SentenceTransformer("all-roberta-large-v1")
mapping_dir = "crosswalk/mapping_csvs/proc_cd"
pkl_dir = "embeddings"
for filename in os.listdir(mapping_dir):
if filename.endswith(".csv") and "level" in filename:
print(f"Creating embeddings for {filename}")
stripped_filename = filename.replace(".csv", "")
mapping_df = pd.read_csv(os.path.join(mapping_dir, filename))
proc_crosswalk = CrosswalkBuilder().from_df(
mapping_df, from_col="Code", to_col="Description"
)
proc_choices = [x for x in proc_crosswalk.mapping.values()]
os.makedirs(os.path.join(pkl_dir, stripped_filename), exist_ok=True)
proc_index = embedding_utils.create_faiss_index(
choices=proc_choices,
model=roberta_model,
save_path=os.path.join(pkl_dir, stripped_filename, "faiss_index.bin"),
embedding_path=os.path.join(pkl_dir, stripped_filename, "embeddings.npy"),
choices_path=os.path.join(pkl_dir, stripped_filename, "choices.pkl"),
)
+98
View File
@@ -0,0 +1,98 @@
import concurrent.futures
import traceback
import re
import os
import pandas as pd
import utils
import config
import preprocessing_funcs
COMPLEX_LIST = []
def count_reimbursements(page_text: str) -> int:
"""Count number of reimbursement items found on a page text, through use of a
regex that catches either '$' or '%'.
Args:
page_text (str): Input page text, as a string
Returns:
int: Number of reimbursement items
"""
pattern = r"[%$]"
matches = re.findall(pattern, page_text)
return len(matches)
def detect_complex(filename, contract_text):
print(f"Processing {filename}")
contract_text = preprocessing_funcs.clean_newlines(contract_text)
contract_text = preprocessing_funcs.clean_law_symbols(contract_text)
text_dict = preprocessing_funcs.split_text(contract_text)
rate_dict = {}
table_dict = {}
for page_num, page_text in text_dict.items():
rate_dict[page_num] = count_reimbursements(page_text)
table_dict[page_num] = "-------Table Start--------" in page_text
page_count, consecutive_page_count, consecutive_rate_count = 0, 0, 0
for page_num in table_dict.keys():
if table_dict[page_num] and rate_dict[page_num] > 0:
consecutive_rate_count += rate_dict[page_num]
consecutive_page_count += 1
else:
if consecutive_rate_count >= config.RATE_THRESHOLD:
page_count += consecutive_page_count
consecutive_page_count, consecutive_rate_count = 0, 0
COMPLEX_LIST.append(
{"Filename": filename, "COMPLEX_FLAG": page_count >= config.TABLE_THRESHOLD}
)
def process_item(contract):
data = s3_client.get_object(Bucket=config.BUCKET, Key=contract)
contents = data["Body"].read()
contract_text = contents.decode("utf-8")
detect_complex(contract, contract_text)
s3_client = config.S3_CLIENT
file_list = []
paginator = s3_client.get_paginator("list_objects_v2")
pages = paginator.paginate(Bucket=config.BUCKET, Prefix=config.PREFIX)
for page in pages:
for obj in page["Contents"]:
if not obj["Key"].endswith("/"):
file_list.append(obj["Key"])
contract_list = sorted(file_list)
files = {}
with concurrent.futures.ThreadPoolExecutor(
max_workers=config.COMPLEX_MAX_WORKERS
) as executor:
# Process each item individually
futures = [executor.submit(process_item, contract) for contract in contract_list]
# Wait for all futures to complete
for future in concurrent.futures.as_completed(futures):
# Retrieve any exceptions raised by the thread
try:
future.result()
except Exception as e:
print(f"Error: {e}")
complex_df = pd.DataFrame(COMPLEX_LIST)
# complex_df.to_csv(os.path.join(config.COMPLEX_OUTPUT_PATH, config.COMPLEX_OUTPUT_FILENAME))
complex_df.to_csv(
os.path.join(
config.COMPLEX_OUTPUT_PATH, "TX_priority_tin_complex_categorization.csv"
)
)
+31
View File
@@ -0,0 +1,31 @@
import utils
import config
import pandas as pd
print(f"Input Dir: {config.LOCAL_PATH}")
print(f"S3 Bucket: {config.S3_BUCKET}")
print(f"S3 Prefix: {config.S3_PREFIX}")
input_dict = (
utils.read_input()
) # keys are contract names, values are full contract text
total_files = len(input_dict)
print(f"Total Input Files : {len(input_dict)}")
# Filter B
input_dict_b = {
k: v for k, v in input_dict.items() if utils.contains_reimbursement(str(v))
} # Filter out non-contracts
print(
f"B Input Files after reimbursement filter: {len(input_dict_b)} | {total_files-len(input_dict_b)} files removed"
)
# Filter already processed
if config.FILTER_ALREADY_PROCESSED:
input_dict_ac, input_dict_b = utils.filter_already_processed(
input_dict, input_dict_b
)
print(f"AC Input Files left to be processed : {len(input_dict_ac)}")
print(f"B Input Files left to be processed : {len(input_dict_b)}")
@@ -0,0 +1,154 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Manual Test Bed\n",
"Runs contracts through from `input_data`\n",
"\n",
"To make this work: in `config.py`, change READ_MODE and RUN_MODE to 'local'.\n",
"Create an .env in the base directory of your project according to these instructions:\n",
"https://aarete.atlassian.net/wiki/spaces/DoczyAI/pages/1283751954/Using+.env+for+AWS+keys\n",
"\n",
"Feel free to comment out parts of `file_processing.py` that do not apply to the fields you are optimizing. For instance, if you're optimizing a smart chunked field, comment out the `one_to_n_fields` block and feed just `one_to_n_results` into `postprocess.postprocess`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#### BASE IMPORTS ####\n",
"\n",
"import logging\n",
"import sys\n",
"import os\n",
"import pandas as pd"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#### LOGGING ####\n",
"\n",
"os.chdir(\"../../\") # go to FieldExtraction folder\n",
"\n",
"import src.config as config\n",
"# For this testing notebook, we want per-file logging\n",
"config.ENABLE_PER_FILE_LOGGING = True\n",
"config.PER_FILE_LOG_LEVEL = \"DEBUG\"\n",
"\n",
"import src.utils.logging_utils as logging_utils\n",
"\n",
"# Clear any existing handlers first\n",
"logging.getLogger().handlers.clear()\n",
"\n",
"# Set up per-file logging (this includes console output)\n",
"# logging_utils.setup_per_file_logging()\n",
"per_file_handler = logging_utils.PerFileHandler()\n",
"per_file_handler.setLevel(logging.DEBUG)\n",
"logging.getLogger().addHandler(per_file_handler)\n",
"\n",
"# Add a single console handler for notebook monitoring\n",
"console_handler = logging.StreamHandler(sys.stdout)\n",
"console_handler.setLevel(logging.INFO) # Only show INFO and above in notebook\n",
"console_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')\n",
"console_handler.setFormatter(console_formatter)\n",
"logging.getLogger().addHandler(console_handler)\n",
"\n",
"# Set level for the root logger\n",
"logging.getLogger().setLevel(logging.DEBUG)\n",
"\n",
"# Suppress noisy third-party loggers\n",
"for logger_name in ['botocore', 'boto3', 'faiss', 'urllib3', 'sentence_transformers', 's3transfer']:\n",
" logging.getLogger(logger_name).setLevel(logging.WARNING)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#### TEST CONFIG ####\n",
"\n",
"TEST_DATA_INPUT_DIR = \"scripts/investment_field_testing/input_data\"\n",
"\n",
"# Also change config.RUN_MODE and config.READ_MODE to 'local'\n",
"test_params = {}\n",
"test_params['local_input_dir'] = TEST_DATA_INPUT_DIR\n",
"test_params['max_workers'] = 1\n",
"test_params['input_files'] = [ # you can specify specific files to run here\n",
" \"Akron General Health System_Eleventh Amendment_20171001.txt\", # replace with your test file(s)\n",
"]\n",
"\n",
"# Specify state(s) for testing, if needed\n",
"# Specify them here instead of in `test_params` bc `config` is initialized \n",
"# before `test_params` is read in main.py, which means the state-level constants\n",
"# won't be set correctly if we put it in `test_params`.\n",
"config.STATE = [\"OH\"] # for single states\n",
"# config.STATE = [\"OH\", \"CA\"] # for multi-state testing"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": "#### MAIN IMPORTS ####\n\nfrom src.pipelines.saas.main import main as investment_main"
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#### TEST EXECUTION ####\n",
"\n",
"df_out, df_error = investment_main(testing=True, test_params=test_params)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"df_out"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "doczy-field-extraction-py3.12 (3.12.7)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.7"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
+146
View File
@@ -0,0 +1,146 @@
import os
import re
import csv
def split_text(text):
text_list = text.split("Start of Page No. = ")
text_dict = {page.split()[0]: page for page in text_list[1:]}
return text_dict
def contains_reimbursement(text, page):
if isinstance(text, dict):
return page.isdigit() and (
"%" in text[page] or "$" in text[page] or "percent " in text[page].lower()
)
elif isinstance(text, str):
return "%" in text or "$" in text or "percent " in text.lower()
else:
print("contains_reimbursement - Invalid data type")
return False
def search_files(directory):
pattern = re.compile(r"'',\s*'',")
results = []
total_files = 0
files_with_pattern = 0
for root, dirs, files in os.walk(directory):
for file in files:
if file.endswith(".txt"):
total_files += 1
file_path = os.path.join(root, file)
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
pages = split_text(content)
reimbursement_pages = [
page
for page in pages
if contains_reimbursement(pages, page)
]
table_pages = [
page
for page in reimbursement_pages
if "-------Table Start--------" in pages[page]
]
matches = []
for page in table_pages:
matches.extend(pattern.findall(pages[page]))
if matches:
files_with_pattern += 1
results.append(
(
file_path,
len(matches),
True,
len(reimbursement_pages),
len(table_pages),
)
)
else:
results.append(
(
file_path,
0,
False,
len(reimbursement_pages),
len(table_pages),
)
)
except Exception as e:
print(f"Error reading {file_path}: {str(e)}")
results.append((file_path, 0, False, 0, 0))
return results, total_files, files_with_pattern
def write_to_csv(results, total_files, files_with_pattern, output_file):
with open(output_file, "w", newline="", encoding="utf-8") as csvfile:
writer = csv.writer(csvfile)
writer.writerow(
[
"File Name",
"Match Count",
"Contains Pattern",
"Reimbursement Pages",
"Table Pages",
"Percentage of Total",
]
)
for (
file_path,
match_count,
contains_pattern,
reimbursement_pages,
table_pages,
) in results:
percentage = (match_count / total_files) * 100 if total_files > 0 else 0
writer.writerow(
[
file_path,
match_count,
contains_pattern,
reimbursement_pages,
table_pages,
f"{percentage:.2f}%",
]
)
percentage_with_pattern = (
(files_with_pattern / total_files) * 100 if total_files > 0 else 0
)
writer.writerow(["", "", "", "", "", ""])
writer.writerow(["Summary", "", "", "", "", ""])
writer.writerow(["Total Files", total_files, "", "", "", ""])
writer.writerow(
[
"Files with Pattern",
files_with_pattern,
"",
"",
"",
f"{percentage_with_pattern:.2f}%",
]
)
if __name__ == "__main__":
current_dir = os.path.dirname(os.path.abspath(__file__))
facility_dir = "batch1_priority"
if os.path.isdir(facility_dir):
results, total_files, files_with_pattern = search_files(facility_dir)
output_file = os.path.join(current_dir, "contract_analysis_results.csv")
write_to_csv(results, total_files, files_with_pattern, output_file)
print(f"Results have been written to {output_file}")
else:
print(f"Error: The directory {facility_dir} does not exist.")
+193
View File
@@ -0,0 +1,193 @@
import src.config as config
from datetime import datetime
import pandas as pd
import src.utils.string_utils as string_utils
import io
def find_inv_test_results():
"""
Efficiently find the 30 most recent directories containing 'inv-test'
and list their RESULTS.csv files.
Strategy: Only list top-level directories first, filter for 'inv-test',
then search only those directories for RESULTS.csv files.
"""
s3_client = config.S3_CLIENT
bucket_name = config.S3_OUTPUT_BUCKET
print(f"\nSearching for 'inv-test' directories in bucket: {bucket_name}")
print("=" * 80)
# Step 1: Get only top-level directories (much faster than scanning all files)
print("Step 1: Listing top-level directories...")
response = s3_client.list_objects_v2(Bucket=bucket_name, Delimiter="/")
if "CommonPrefixes" not in response:
print("No directories found in bucket")
return []
all_top_level_dirs = [
prefix["Prefix"].rstrip("/") for prefix in response["CommonPrefixes"]
]
print(f"Found {len(all_top_level_dirs)} total top-level directories")
# Step 2: Filter for 'inv-test' directories only
inv_test_dirs = [d for d in all_top_level_dirs if "inv-test" in d]
print(f"Found {len(inv_test_dirs)} directories containing 'inv-test'")
if not inv_test_dirs:
print("No 'inv-test' directories found")
return []
# Step 3: Get timestamps for inv-test directories only (not all files in bucket)
print("\nStep 2: Getting timestamps for inv-test directories...")
dir_timestamps = []
for dir_name in inv_test_dirs:
# Get just the first file in each directory to get a timestamp
response = s3_client.list_objects_v2(
Bucket=bucket_name, Prefix=f"{dir_name}/", MaxKeys=1
)
if "Contents" in response and len(response["Contents"]) > 0:
timestamp = response["Contents"][0]["LastModified"]
dir_timestamps.append((dir_name, timestamp))
# Sort by timestamp (most recent first)
dir_timestamps.sort(key=lambda x: x[1], reverse=True)
# Get the 30 most recent
recent_inv_test_dirs = dir_timestamps[:30]
print(
f"\nStep 3: Searching 30 most recent 'inv-test' directories for RESULTS.csv files:"
)
print("=" * 80)
total_results_files = 0
all_results_files = {} # Dictionary to store directory -> list of file paths
for i, (dir_name, timestamp) in enumerate(recent_inv_test_dirs, 1):
print(f"\n[{i}/30] Directory: {dir_name}")
print(f" Last Modified: {timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
# Search only this directory for RESULTS.csv files
results_files = []
paginator = s3_client.get_paginator("list_objects_v2")
for page in paginator.paginate(Bucket=bucket_name, Prefix=f"{dir_name}/"):
if "Contents" not in page:
continue
for obj in page["Contents"]:
key = obj["Key"]
# Ignore files in subdirectories named "individual"
if "/individual/" in key:
continue
if key.endswith("-RESULTS.csv"):
results_files.append(key)
if results_files:
print(f" Found {len(results_files)} RESULTS.csv file(s):")
for file_key in results_files:
print(f"{file_key}")
total_results_files += 1
all_results_files[dir_name] = results_files
else:
print(f" ✗ No -RESULTS.csv files found")
print("\n" + "=" * 80)
print(f"Summary:")
print(f" - Searched 30 most recent 'inv-test' directories")
print(f" - Total -RESULTS.csv files found: {total_results_files}")
print("=" * 80)
# Step 4: Generate missingness report
if total_results_files > 0:
print(f"\nStep 4: Generating missingness report...")
generate_missingness_report(s3_client, bucket_name, all_results_files)
return recent_inv_test_dirs
def generate_missingness_report(s3_client, bucket_name, results_files_dict):
"""
Generate a missingness report for all RESULTS.csv files.
Args:
s3_client: S3 client object
bucket_name: S3 bucket name
results_files_dict: Dictionary mapping directory names to lists of result file paths
"""
print("=" * 80)
print("Generating Missingness Report")
print("=" * 80)
missingness_data = []
for dir_name, file_paths in results_files_dict.items():
for file_path in file_paths:
print(f"\nProcessing: {file_path}")
try:
# Read CSV from S3
response = s3_client.get_object(Bucket=bucket_name, Key=file_path)
csv_content = response["Body"].read().decode("utf-8")
df = pd.read_csv(io.StringIO(csv_content))
if df.empty:
print(f" ⚠ File is empty, skipping")
continue
# Calculate missingness for each column
row_data = {"csv_file": file_path}
for column in df.columns:
empty_mask = string_utils.is_empty(df[column], pd_mask=True)
missingness_pct = (empty_mask.sum() / len(df)) * 100
row_data[column] = round(missingness_pct, 2)
missingness_data.append(row_data)
print(f" ✓ Processed {len(df)} rows, {len(df.columns)} columns")
except Exception as e:
print(f" ✗ Error processing file: {e}")
continue
if not missingness_data:
print("\nNo valid data to create report")
return
# Create missingness DataFrame
missingness_df = pd.DataFrame(missingness_data)
# Set csv_file as index for better readability
missingness_df.set_index("csv_file", inplace=True)
# Save report
output_filename = (
f"missingness_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
)
missingness_df.to_csv(output_filename)
print("\n" + "=" * 80)
print(f"Missingness Report Summary:")
print(f" - Total CSV files processed: {len(missingness_data)}")
print(f" - Total columns analyzed: {len(missingness_df.columns)}")
print(f" - Report saved to: {output_filename}")
print("=" * 80)
# Display sample of the report
print("\nSample of Missingness Report (first 5 columns):")
print("-" * 80)
if len(missingness_df.columns) > 5:
print(missingness_df.iloc[:, :5].to_string())
print(f"\n... and {len(missingness_df.columns) - 5} more columns")
else:
print(missingness_df.to_string())
print("-" * 80)
if __name__ == "__main__":
find_inv_test_results()
@@ -0,0 +1,25 @@
import pandas as pd
from constants.constants import Constants
from src.investment.postprocess import postprocess
# File paths
input_excel_path = "input.xlsx"
output_csv_path = "output.csv"
# Read Excel file into DataFrame
print(f"Reading Excel file: {input_excel_path}")
df = pd.read_excel(input_excel_path, dtype=str)
print(f"Loaded {len(df)} rows from Excel")
# Initialize Constants
constants = Constants()
# Process the DataFrame
print("Processing data through postprocess()...")
df_processed = postprocess(df, constants)
print(f"Processing complete. Output has {len(df_processed)} rows")
# Write to CSV
print(f"Writing CSV file: {output_csv_path}")
df_processed.to_csv(output_csv_path, index=False)
print("Done!")
+101
View File
@@ -0,0 +1,101 @@
import table_funcs
import config
import utils
import preprocessing_funcs
import concurrent.futures
import pandas as pd
import os
input_dict = utils.read_input()
table_analysis_dicts = []
def table_analysis(file_object):
filename, contract_text = file_object
text_dict = preprocessing_funcs.split_text(contract_text)
file_dict = {"Filename": filename, "Filename_pdf": filename.replace(".txt", ".pdf")}
contains_reimbursement = utils.contains_reimbursement(contract_text)
file_dict["Contains Reimbursement"] = contains_reimbursement
# Determine if the file should be present in the output
file_dict["Present in Output"] = contains_reimbursement
stats_dict = table_funcs.get_table_stats(text_dict)
file_dict.update(stats_dict)
table_analysis_dicts.append(file_dict)
with concurrent.futures.ThreadPoolExecutor(max_workers=config.MAX_WORKERS) as executor:
futures = [executor.submit(table_analysis, item) for item in input_dict.items()]
for future in concurrent.futures.as_completed(futures):
try:
future.result()
except Exception as e:
print(f"Error: {e}")
# Create the main table analysis DataFrame
table_analysis_df = pd.DataFrame(table_analysis_dicts)
table_analysis_df.to_csv(
os.path.join(config.REPORTING_OUTPUT_DIRECTORY, config.TABLE_ANALYSIS_NAME)
)
# Define the columns we want in our report
report_columns = [
"Filename",
"Contains Reimbursement",
"Present in Output",
"Table Count",
"Table Page Count",
"Rate Count",
"Num Rate Pages",
"Num >=10 Rates",
]
# Create the new report DataFrame with available columns
report_df = table_analysis_df[
[col for col in report_columns if col in table_analysis_df.columns]
]
# Function to safely create summary columns
def create_summary_column(df, source_col, target_col):
if source_col in df.columns:
df[target_col] = df[source_col].apply(
lambda x: ", ".join(map(str, eval(x))) if x != "[]" else "None"
)
else:
df[target_col] = "N/A"
# Add summary columns if the data is available
create_summary_column(report_df, "Table Pages", "Table Pages Summary")
create_summary_column(report_df, "Rate Pages", "Rate Pages Summary")
create_summary_column(report_df, ">=10 Rate Pages", ">=10 Rate Pages Summary")
# Define the final column order
final_columns = [
"Filename",
"Contains Reimbursement",
"Present in Output",
"Table Count",
"Table Page Count",
"Table Pages Summary",
"Rate Count",
"Num Rate Pages",
"Rate Pages Summary",
"Num >=10 Rates",
">=10 Rate Pages Summary",
]
# Reorder columns, including only those that exist
report_df = report_df[[col for col in final_columns if col in report_df.columns]]
# Save the new report to a CSV file
report_output_path = os.path.join(
config.REPORTING_OUTPUT_DIRECTORY, "enhanced_reimbursement_report.csv"
)
report_df.to_csv(report_output_path, index=False)
print(f"Enhanced reimbursement report saved to: {report_output_path}")
print("Columns in the report:", ", ".join(report_df.columns))
+111
View File
@@ -0,0 +1,111 @@
import pandas as pd
import os
import sys
from datetime import datetime
import re
from openpyxl import Workbook
from openpyxl.utils.dataframe import dataframe_to_rows
import argparse
import boto3
from botocore.exceptions import ClientError
sys.path.append("src")
import utils
import config
from qa_qc_helpers import perform_qc_qa, save_qcqa_wb
from consolidate_output import consolidate_output
file_name_column = config.FILE_NAME_COLUMN
"""python scripts/qa_qc.py input_dir=______ output_dir=_______ ac_df=________ b_df=______ read_mode=____ df_read_mode=________
You should have both input and output (input is local or s3, ouput is just an empty folder) and then one of ac_df or b_df or both
The final report will be saved in the parent directory of when you are running under reports/
Run this from the base folder of the repo."""
def main():
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
script_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(script_dir)
reports_dir = os.path.join(parent_dir, "reports")
os.makedirs(reports_dir, exist_ok=True)
if config.AC_DF:
print(f"AC DataFrame: {config.AC_DF}")
if config.B_DF:
print(f"B DataFrame: {config.B_DF}")
if config.LOCAL_PATH:
print(f"Input Dir: {config.LOCAL_PATH}")
if (config.S3_BUCKET) and (config.S3_PREFIX):
print(f"S3 Bucket: {config.S3_BUCKET}")
print(f"S3 Prefix: {config.S3_PREFIX}")
print(f"Output Dir: {config.OUTPUT_DIRECTORY}")
print(f"Reports Dir: {reports_dir}")
if not config.LOCAL_PATH or (config.S3_BUCKET and config.S3_PREFIX):
print("Needs an Input Directory!")
if not config.AC_DF and not config.B_DF:
print("Needs at least one of AC Outputs and B Outputs!")
return
input_dict = utils.read_input()
ac_df, b_df = utils.read_input_csv()
total_files = len(input_dict)
print(f"Total Input Files: {total_files}")
all_input_filenames = (
[utils.remove_txt_extension(f.lower()) for f in input_dict.keys()]
if input_dict
else []
)
if ac_df is not None:
ac_df = utils.remove_unnamed_columns(ac_df)
ac_df[file_name_column] = ac_df[file_name_column].apply(
lambda x: utils.remove_txt_extension(str(x)).lower()
)
ac_filenames = set(ac_df[file_name_column])
if b_df is not None:
b_df = utils.remove_unnamed_columns(b_df)
b_filenames = set(b_df[file_name_column])
b_df[file_name_column] = b_df[file_name_column].apply(
lambda x: utils.remove_txt_extension(str(x)).lower()
)
if ac_df is not None and b_df is not None:
unmatched_b_filenames = b_filenames - ac_filenames
unmatched_b_df = pd.DataFrame({file_name_column: list(unmatched_b_filenames)})
unmatched_b_output_path = os.path.join(
config.OUTPUT_DIRECTORY, "unmatched_B_filenames.csv"
)
unmatched_b_df.to_csv(unmatched_b_output_path, index=False)
print(f"Unmatched B filenames saved to '{unmatched_b_output_path}'")
# Perform an inner merge to keep only matching rows for the main output
ac_df, b_df, merged_df = consolidate_output(timestamp, ac_df, b_df)
merged_df = utils.remove_unnamed_columns(merged_df)
ac_rows_before = len(ac_df)
b_rows_before = len(b_df)
merged_rows = len(merged_df)
unmatched_b_count = len(unmatched_b_filenames)
print(f"Rows in AC DataFrame before merge: {ac_rows_before}")
print(f"Rows in B DataFrame before merge: {b_rows_before}")
print(f"Rows in merged DataFrame: {merged_rows}")
print(f"Unique filenames in B not found in AC: {unmatched_b_count}")
merged_output_path = os.path.join(
config.OUTPUT_DIRECTORY, "DOCZYAI_merged_results.csv"
)
merged_df.to_csv(merged_output_path, index=False)
print(f"Merged data saved to '{merged_output_path}'")
wb = perform_qc_qa(ac_df, b_df, merged_df, input_dict)
save_qcqa_wb(wb, timestamp)
if __name__ == "__main__":
main()
+38
View File
@@ -0,0 +1,38 @@
import table_funcs
import config
import utils
import preprocessing_funcs
import concurrent.futures
import pandas as pd
import os
input_dict = utils.read_input()
table_analysis_dicts = []
def table_analysis(file_object):
filename, contract_text = file_object
text_dict = preprocessing_funcs.split_text(contract_text)
file_dict = {"Filename": filename, "Filename_pdf": filename.replace(".txt", ".pdf")}
if utils.contains_reimbursement(contract_text):
file_dict["Contains Reimbursement"] = True
else:
file_dict["Contains Reimbursement"] = False
stats_dict = table_funcs.get_table_stats(text_dict)
file_dict.update(stats_dict)
table_analysis_dicts.append(file_dict)
with concurrent.futures.ThreadPoolExecutor(max_workers=config.MAX_WORKERS) as executor:
futures = [executor.submit(table_analysis, item) for item in input_dict.items()]
for future in concurrent.futures.as_completed(futures):
try:
future.result()
except Exception as e:
print(f"Error: {e}")
table_analysis_df = pd.DataFrame(table_analysis_dicts)
table_analysis_df.to_csv(
os.path.join(config.REPORTING_OUTPUT_DIRECTORY, config.TABLE_ANALYSIS_NAME)
)
+223
View File
@@ -0,0 +1,223 @@
"""
This script was written to extract .txt files from pdf for adhoc client runs.
Ensure you have permissions for API gateway, S3, Lambda function and SQS to execute this (DEVELOPER & above roles in DEV & UAT for Doczy should suffice)
If you have Analyst / Test roles and need the permissions to be elevated or policies to be updated then please ask Sannan Iqbal to make the changes.
The flow to execute the pipeline is:
Create batch using api req
Add files to the newly created batch along with the batch id as the tag
Construct payload to trigger pipeline
Post request to trigger pipeline with payload
The output text file can be found in the client_bucket/contract_text_file/batch_123456
And the final LLM parsed outputs can be found in client_bucket/final_output/batch_123456
"""
import os
import boto3
import requests
import json
import boto3
from datetime import datetime
import os
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from botocore.credentials import get_credentials
from botocore.session import Session
import time
# Function to upload files to S3
def upload_files_to_s3(directory, bucket, batch_id):
contract_list = []
for filename in os.listdir(directory):
if filename.endswith(".pdf"):
file_path = os.path.join(directory, filename)
s3_key = f"contracts-landing-zone/{batch_id}/{filename}"
# Upload file to S3
s3_client.upload_file(file_path, bucket, s3_key)
print(f"Uploaded {filename} to S3 bucket\n")
# Add tags to the uploaded file
s3_client.put_object_tagging(
Bucket=bucket,
Key=s3_key,
Tagging={"TagSet": [{"Key": "BatchId", "Value": batch_id}]},
)
print(f"Added tags to {filename}\n")
# Add file details to contract list
# For now we can leave it as is since only A, C have been operationalized
contract_list.append(
{
"contract_name": filename,
"groups": [
"A", # This needs to be dynamic in UI 1 based on what group has been selected
"C",
],
"contract_source_path": s3_key,
}
)
return contract_list
def create_batch(client_bucket, create_batch_url):
myobj = {"client-bucket-name": client_bucket}
# Call create batch API endpoint
response = requests.post(create_batch_url, json=myobj)
if response.status_code >= 200 and response.status_code < 300:
try:
new_batch_id = json.loads(json.loads(response.text)["body"])["batch_id"]
landing_zone = json.loads(json.loads(response.text)["body"])["landing_zone"]
except:
print(myobj)
print(response.text)
new_batch_id = "failed_cases"
landing_zone = "contracts_landing_zone"
else:
print(response.text)
new_batch_id = "failed_cases"
landing_zone = "contracts_landing_zone"
return new_batch_id, landing_zone
def list_filtered_files(bucket_name, prefix, start_date, end_date, profile_name):
"""
List all files in an S3 bucket filtered by date range.
Parameters:
- bucket_name: str, the name of the S3 bucket
- prefix: str, the prefix (folder) in the S3 bucket
- start_date: str, the start date in ISO 8601 format
- end_date: str, the end date in ISO 8601 format
- profile_name: str, the AWS profile name
Returns:
- list of str: the keys of the filtered files
"""
# Parse the dates
start_date = datetime.fromisoformat(start_date.replace("Z", "+00:00"))
end_date = datetime.fromisoformat(end_date.replace("Z", "+00:00"))
# Initialize a session using the specified profile
session = boto3.Session(profile_name=profile_name)
s3_client = session.client("s3")
# List objects in the bucket with the specified prefix
paginator = s3_client.get_paginator("list_objects_v2")
page_iterator = paginator.paginate(Bucket=bucket_name, Prefix=prefix)
# Filter files by date
filtered_files = []
for page in page_iterator:
if "Contents" in page:
for obj in page["Contents"]:
last_modified = obj["LastModified"]
if start_date <= last_modified <= end_date:
filtered_files.append(obj["Key"])
return filtered_files
def download_files(bucket_name, file_keys, profile_name, local_directory):
"""
Download files from an S3 bucket.
Parameters:
- bucket_name: str, the name of the S3 bucket
- file_keys: list of str, the keys of the files to download
- profile_name: str, the AWS profile name
- local_directory: str, the local directory to download files to
"""
# Initialize a session using the specified profile
session = boto3.Session(profile_name=profile_name)
s3_client = session.client("s3")
# Ensure the local directory exists
if not os.path.exists(local_directory):
os.makedirs(local_directory)
# Download each file
for key in file_keys:
file_name = os.path.basename(key) # Get only the file name from the key
local_file_path = os.path.join(local_directory, file_name)
print(f"Downloading {key} to {local_file_path}")
s3_client.download_file(bucket_name, key, local_file_path)
print("Download completed.")
if __name__ == "__main__":
# Define your variables
s3_bucket = "doczyai-use2-u-cn1-s3-textract-processing-001"
# batch_id = 'batch_100524101551'
client_name = "Priority Health"
username = "ADHOC USER"
# These endpoints are in UAT, please change them to DEV if there are access issues with UAT
api_endpoint = (
"https://29gm8cek03.execute-api.us-east-2.amazonaws.com/dev/trigger-pipeline"
)
create_batch_url = (
"https://29gm8cek03.execute-api.us-east-2.amazonaws.com/dev/create-batch"
)
session = boto3.Session(
profile_name="temp_cred"
) # Change the profile name to the one you have in your .aws/credentials file
s3_client = session.client("s3")
# Use current directory as PDF directory
pdf_directory = "C:\\Doczy\\Priority Health\\For_Textract\\For_Textract\\UAT- test East Paris Surgical Center copy"
# Create batch
batch_id, landing_zone = create_batch(s3_bucket, create_batch_url)
print(f"Batch ID: {batch_id}")
print(f"Landing Zone: {landing_zone}")
# batch_id = 'batch_110624213433'
# landing_zone = 'contracts_landing_zone/batch_110624213433/'
if batch_id == "failed_cases":
print("Batch creation failed. Exiting...")
exit()
# Upload files and get contract list
contract_list = upload_files_to_s3(pdf_directory, s3_bucket, batch_id)
# Create JSON object
data = {
"s3_bucket": s3_bucket,
"batch_id": batch_id,
"client_name": client_name,
"username": username,
"contract_list": contract_list,
}
# Make POST request to API
response = requests.post(api_endpoint, json=data)
# Print response
print(response.status_code)
print(response.json())
##################
# Get text files
prefix = f"contract-text-file/{batch_id}/" # if you have a specific prefix (folder) in your bucket
# These dates are to filter the contracts in case there are older contracts in the same batch
start_date = "2024-06-04T00:00:00Z" # ISO 8601 format
end_date = "2024-06-14T23:59:59Z" # ISO 8601 format
profile_name = "temp_cred"
local_directory = "C:\\Doczy\\Priority Health\\For_Textract\\For_Textract\\text otuput" # local directory to save files
# List filtered files
time.sleep(
30
) # Waiting for the text files to be generated, this may take longer and files may not be available after 30 seconds sometimes
filtered_files = list_filtered_files(
s3_bucket, prefix, start_date, end_date, profile_name
)
print(f"Filtered files: {len(filtered_files)}")
# Download files
download_files(s3_bucket, filtered_files, profile_name, local_directory)
+178
View File
@@ -0,0 +1,178 @@
import json
import boto3
import pandas as pd
import numpy as np
from datetime import datetime
import os
import anthropic
import re
from concurrent.futures import ThreadPoolExecutor, as_completed
import config
import utils
# Global configuration variables
S3_REGION = "us-east-2"
BEDROCK_REGION = "us-east-1"
LLM_SELECTED = "Claude Instant"
bedrock_runtime = config.BEDROCK_RUNTIME
def find_tin_numbers(text):
# Regular expression to find sets of 9 digits with an optional dash after the first two digits
pattern = r"\b\d{2}-?\d{7}\b"
# Find all matches in the provided text
matches = re.findall(pattern, text)
return matches
def invoke_llm(context, llm_selected=LLM_SELECTED):
prompt_data = f"""
Human: Use the following pieces of context to provide a concise answer to the questions at the end. You must answer in python list format.
{context}
Question: What is the Group provider's taxpayer identification number stated in the contract? This is a 9 digit long number typically with a hyphen after the first 2 digits. Return only this 9 digit number with a hyphen after the first 2 digits. Do not elaborate or add context.
Assistant: Answer in list format: ["""
if llm_selected == "Claude 3 - Sonnet":
model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0"
body = json.dumps(
{
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 4096,
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": anthropic.HUMAN_PROMPT
+ prompt_data
+ anthropic.AI_PROMPT,
}
],
}
],
"temperature": 0.0,
}
)
elif llm_selected in ["Claude Instant"]:
# Note: Claude 2 support has been removed - using Claude 3 Haiku instead
model_id = config.MODEL_ID_CLAUDE3_HAIKU
body = json.dumps(
{
"prompt": anthropic.HUMAN_PROMPT + prompt_data + anthropic.AI_PROMPT,
"max_tokens_to_sample": 2048,
"temperature": 0.0,
"top_p": 1,
"top_k": 250,
"stop_sequences": [anthropic.HUMAN_PROMPT],
}
)
else:
raise ValueError("Unsupported LLM selected")
try:
response = bedrock_runtime.invoke_model(
body=body,
modelId=model_id,
accept="application/json",
contentType="application/json",
)
response_body = json.loads(response.get("body").read())
if llm_selected == "Claude 3 - Sonnet":
response_text = response_body["content"][0]["text"]
elif llm_selected in ["Claude Instant"]:
response_text = response_body["completion"]
else:
print("Unsupported LLM selected")
except Exception as e:
print(f"Error invoking LLM: {e}")
response_text = "failed"
return response_text.strip()
def main(filename, contract_text):
print(f"Processing contract: {filename}")
try:
df = pd.DataFrame(columns=["Contract Name", "Raw value", "Final value"])
tin_number = find_tin_numbers(contract_text)
final_tin = list(set(tin_number))
if len(final_tin) != 1:
final_tin = find_tin_numbers(
invoke_llm(contract_text, llm_selected=LLM_SELECTED)
)
df["Raw value"] = [tin_number]
df["Final value"] = [final_tin]
df["Contract Name"] = [filename]
df.to_csv(f"tin_output/{filename}.csv", index=False)
except Exception as e:
print(f"Error processing contract: {e}")
# bucket = "centene-national-contracting-files"
# contract_path = "no_tins_text_files"
# s3_client = boto3.Session(aws_access_key_id=config.S3_ACCESS_KEY_ID,
# aws_secret_access_key=config.S3_SECRET_ACCESS_KEY,
# aws_session_token=config.S3_SESSION_TOKEN, region_name="us-east-2").client('s3')
# objects = s3_client.list_objects_v2(Bucket=bucket, Prefix=contract_path)
file_list = []
OUTPUT_DIRECTORY = r"tin_output/no_tins_text_files"
NUM_WORKERS = 10
BATCH_SIZE = 10
def process_file(item):
filename, contract_text = item
if filename.endswith(".txt") and not os.path.exists(
os.path.join(OUTPUT_DIRECTORY, (filename[:-3] + "csv"))
):
main(filename, contract_text)
def process_directory(input_dict):
# for item in input_dict.items():
with ThreadPoolExecutor(max_workers=NUM_WORKERS) as executor:
futures = {executor.submit(process_file, item) for item in input_dict.items()}
for future in as_completed(futures):
future.result() # This will raise an exception if the callable raised one
input_dict = utils.read_input(path="data/cnc/no_tins")
process_directory(input_dict)
individual_path = r"tin_output/no_tins_text_files"
def individual_to_dict(filename):
df = pd.read_csv(os.path.join(individual_path, filename))
df = df.dropna(axis=1, how="all")
column_names = ["Filename", "Answer", "Final Answer"]
new_column_names = column_names[: len(df.columns)] + list(
df.columns[len(column_names) :]
)
df.columns = new_column_names
df_dict = {"Filename": filename}
df_dict["Answer"] = df["Answer"].iloc[0]
df_dict["Final Answer"] = df["Final Answer"].iloc[0]
return df_dict
all_dicts = []
for filename in os.listdir(individual_path):
df_dict = individual_to_dict(filename)
all_dicts.append(df_dict)
final_df = pd.DataFrame(all_dicts)
final_df.to_csv("results.csv", index=False)