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
+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()