siddharth786's picture
Add process_email_request function to utils and update Gradio flagging
b2c11bb
Raw History Blame
8.7 kB
import re
import spacy
from typing import List, Dict, Tuple
import pickle
from pathlib import Path
import os # Import os to handle potential path issues
# Assuming mask_pii and predict_category are defined in models.py
try:
from models import mask_pii, predict_category, Pipeline # Import Pipeline if needed for type hinting
except ImportError:
print("ERROR in utils.py: Could not import functions/classes from models.py")
# Define dummy functions if import fails, so the rest of the file can load
def mask_pii(text, nlp_model): return "Masking failed", []
def predict_category(text, pipeline): return "Classification failed"
Pipeline = object # Dummy class
# --- Model Loading ---
MODEL_DIR = Path("saved_models")
MODEL_PATH = MODEL_DIR / "email_classifier_pipeline.pkl"
NLP_MODEL = None
MODEL_PIPELINE = None
def load_spacy_model():
"""Loads the spaCy model."""
global NLP_MODEL
if NLP_MODEL is None:
try:
NLP_MODEL = spacy.load("en_core_web_sm")
print("spaCy model 'en_core_web_sm' loaded successfully.")
except OSError:
print("Error loading spaCy model 'en_core_web_sm'. Make sure it's downloaded.")
# Attempt to download if not found (might fail in restricted envs)
try:
print("Attempting to download spaCy model...")
spacy.cli.download("en_core_web_sm")
NLP_MODEL = spacy.load("en_core_web_sm")
print("spaCy model 'en_core_web_sm' downloaded and loaded successfully.")
except Exception as download_e:
print(f"Failed to download or load spaCy model: {download_e}")
NLP_MODEL = None # Ensure it remains None if loading fails
return NLP_MODEL
def load_model_pipeline() -> Pipeline | None:
"""Loads the classification pipeline from the .pkl file."""
global MODEL_PIPELINE
if MODEL_PIPELINE is None:
if not MODEL_PATH.exists():
print(f"Model pipeline not found at {MODEL_PATH}. Please train and save the model pipeline first.")
return None
try:
with open(MODEL_PATH, "rb") as f:
MODEL_PIPELINE = pickle.load(f)
print("Model pipeline loaded successfully.")
except Exception as e:
print(f"Error loading model pipeline from {MODEL_PATH}: {e}")
MODEL_PIPELINE = None # Ensure it remains None if loading fails
return MODEL_PIPELINE
# --- PII Detection Regex Patterns ---
# Define regex patterns for PII entities not easily caught by NER
# (Refine these patterns carefully for accuracy)
REGEX_PATTERNS = {
"email": r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
"phone_number": r'(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b',
"credit_debit_no": r'\b(?:\d[ -]*?){13,16}\b', # Basic pattern, needs refinement
"cvv_no": r'\b\d{3,4}\b', # Often needs context to differentiate
"expiry_no": r'\b(0[1-9]|1[0-2])\/?([0-9]{4}|[0-9]{2})\b', # MM/YY or MM/YYYY
"aadhar_num": r'\b\d{4}[ -]?\d{4}[ -]?\d{4}\b',
# DOB might be harder with regex alone, consider context or NER patterns
"dob": r'\b(\d{1,2}[-/]\d{1,2}[-/]\d{2,4}|\d{4}[-/]\d{1,2}[-/]\d{1,2})\b' # Basic DOB patterns
}
# --- PII Masking Function ---
def mask_pii(text: str) -> Tuple[str, List[Dict]]:
"""
Detects and masks PII in the input text using spaCy NER and Regex.
Args:
text: The input email body string.
Returns:
A tuple containing:
- masked_email (str): The email body with PII replaced by placeholders.
- list_of_masked_entities (List[Dict]): A list of dictionaries,
each detailing a masked entity (position, classification, original value).
"""
masked_text = text
list_of_masked_entities = []
found_spans = [] # To store (start, end, entity_type, original_value)
# 1. Use spaCy for Named Entity Recognition (PERSON for full_name)
doc = nlp(text)
for ent in doc.ents:
if ent.label_ == "PERSON":
# Simple PERSON check, might need refinement (e.g., filter short names)
if len(ent.text.split()) > 1: # Basic check for multi-word names
found_spans.append((ent.start_char, ent.end_char, "full_name", ent.text))
# 2. Use Regex for other PII types
for entity_type, pattern in REGEX_PATTERNS.items():
for match in re.finditer(pattern, text):
# Basic check for overlap with already found spans (can be improved)
is_overlapping = any(
max(found[0], match.start()) < min(found[1], match.end())
for found in found_spans
)
if not is_overlapping:
# Add basic context checks if needed (e.g., for CVV)
# if entity_type == "cvv_no" and not is_likely_cvv(text, match): continue
found_spans.append((match.start(), match.end(), entity_type, match.group(0)))
# 3. Sort spans by start position to handle masking correctly
found_spans.sort(key=lambda x: x[0])
# 4. Perform masking and create the entity list
offset = 0 # Keep track of index changes due to replacements
for start, end, entity_type, original_value in found_spans:
adjusted_start = start + offset
adjusted_end = end + offset
placeholder = f"[{entity_type}]"
# Replace the PII with the placeholder in the masked_text
masked_text = masked_text[:adjusted_start] + placeholder + masked_text[adjusted_end:]
# Update the offset for subsequent replacements
offset += len(placeholder) - (end - start)
# Add details to the list_of_masked_entities
list_of_masked_entities.append({
"position": [start, end], # Use ORIGINAL indices
"classification": entity_type,
"entity": original_value
})
# Sort the final list by original start position for consistency
list_of_masked_entities.sort(key=lambda x: x["position"][0])
return masked_text, list_of_masked_entities
# --- Main Processing Function for Gradio ---
def process_email_request(email_body: str) -> dict:
"""
Processes the input email body for PII masking and classification.
Loads models on first call if not already loaded.
"""
print("Processing email request...") # Add log
nlp = load_spacy_model()
pipeline = load_model_pipeline()
if nlp is None:
return {"error": "spaCy model not loaded.", "input_email_body": email_body}
if pipeline is None:
return {"error": "Classification pipeline not loaded.", "input_email_body": email_body}
try:
# 1. Mask PII using the loaded spaCy model
# Ensure mask_pii expects the nlp model as an argument if needed
masked_email_body, entities = mask_pii(email_body, nlp) # Pass nlp model
print(f"PII Masking complete. Found {len(entities)} entities.") # Add log
# Convert entities to the required dict format if necessary
# Assuming mask_pii already returns entities as list of dicts
# with 'position', 'classification', 'entity' keys.
# 2. Classify the masked email using the loaded pipeline
predicted_class = predict_category(masked_email_body, pipeline)
print(f"Classification complete. Predicted class: {predicted_class}") # Add log
# 3. Construct the response dictionary
response = {
"input_email_body": email_body,
"list_of_masked_entities": entities, # Ensure this matches expected format
"masked_email": masked_email_body,
"category_of_the_email": predicted_class
}
print("Response constructed successfully.") # Add log
return response
except Exception as e:
print(f"Error during email processing: {e}") # Log the specific error
# Consider logging the full traceback for debugging
# import traceback
# print(traceback.format_exc())
return {
"error": f"An error occurred during processing: {str(e)}",
"input_email_body": email_body
}
# --- Other Utility Functions (Add as needed) ---
# E.g., text cleaning for classification model input
def clean_text_for_classification(text: str) -> str:
"""Basic text cleaning."""
text = text.lower()
text = re.sub(r'<.*?>', '', text) # Remove HTML tags
text = re.sub(r'[^a-z\s]', '', text) # Remove punctuation/numbers (adjust if needed)
text = re.sub(r'\s+', ' ', text).strip() # Normalize whitespace
# Add stopword removal if necessary (import nltk stopwords)
return text