Spaces:
Runtime error
Runtime error
Commit ·
b20f676
1
Parent(s): 0897779
Fix circular import and NameError issues
Browse files
api.py
CHANGED
|
@@ -1,26 +1,33 @@
|
|
| 1 |
from fastapi import FastAPI, HTTPException
|
| 2 |
from pydantic import BaseModel, Field
|
| 3 |
-
from typing import List, Dict, Tuple, Any, Optional
|
| 4 |
-
import
|
|
|
|
| 5 |
|
| 6 |
-
#
|
| 7 |
try:
|
| 8 |
-
from
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
except ImportError as e:
|
| 11 |
-
print(f"
|
| 12 |
-
#
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
| 17 |
|
| 18 |
-
|
| 19 |
-
# >>>>> THIS LINE IS CRUCIAL <<<<<
|
| 20 |
-
app = FastAPI()
|
| 21 |
-
# >>>>> MUST BE NAMED 'app' <<<<<
|
| 22 |
|
| 23 |
-
# --- Pydantic Models ---
|
| 24 |
class EmailInput(BaseModel):
|
| 25 |
email_body: str = Field(..., example="Hello, my name is Jane Doe and my email is jane.doe@example.com. I have a billing question.")
|
| 26 |
|
|
@@ -29,64 +36,56 @@ class MaskedEntity(BaseModel):
|
|
| 29 |
classification: str = Field(..., example="full_name")
|
| 30 |
entity: str = Field(..., example="Jane Doe")
|
| 31 |
|
| 32 |
-
class
|
| 33 |
input_email_body: str
|
| 34 |
list_of_masked_entities: List[MaskedEntity]
|
| 35 |
masked_email: str
|
| 36 |
category_of_the_email: str
|
| 37 |
|
| 38 |
-
# --- Load
|
| 39 |
-
#
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
|
| 47 |
-
|
|
|
|
| 48 |
async def classify_email(email_input: EmailInput):
|
| 49 |
"""
|
| 50 |
-
|
| 51 |
-
and returns the results in the specified JSON format.
|
| 52 |
"""
|
| 53 |
-
if not model_pipeline:
|
| 54 |
-
raise HTTPException(status_code=503, detail="Model pipeline not available. Please train/load it first.")
|
| 55 |
-
|
| 56 |
-
input_email = email_input.email_body
|
| 57 |
-
|
| 58 |
try:
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
# Ensure entities match the Pydantic model format
|
| 63 |
-
validated_entities = [MaskedEntity(**entity) for entity in entities]
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
|
|
|
| 67 |
|
| 68 |
-
#
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
list_of_masked_entities=validated_entities,
|
| 72 |
-
masked_email=masked_email_body,
|
| 73 |
-
category_of_the_email=predicted_class
|
| 74 |
-
)
|
| 75 |
-
return response
|
| 76 |
|
|
|
|
|
|
|
|
|
|
| 77 |
except Exception as e:
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
#
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
-
# ---
|
| 84 |
-
# You'll typically run this using uvicorn from the terminal:
|
| 85 |
-
# uvicorn api:app --reload
|
| 86 |
-
# The block below is useful if you want to run `python api.py` directly sometimes,
|
| 87 |
-
# but `uvicorn` command is standard for development.
|
| 88 |
if __name__ == "__main__":
|
| 89 |
import uvicorn
|
| 90 |
print("Starting Uvicorn server directly (for debugging)...")
|
| 91 |
-
# Use host="0.0.0.0" to make it accessible within Codespace network
|
| 92 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
|
|
| 1 |
from fastapi import FastAPI, HTTPException
|
| 2 |
from pydantic import BaseModel, Field
|
| 3 |
+
from typing import List, Dict, Tuple, Any, Optional, Union
|
| 4 |
+
import sys # Add sys import
|
| 5 |
+
import os # Add os import
|
| 6 |
|
| 7 |
+
# --- Define/Import Pipeline Type FIRST ---
|
| 8 |
try:
|
| 9 |
+
from sklearn.pipeline import Pipeline
|
| 10 |
+
except ImportError:
|
| 11 |
+
Pipeline = object # type: ignore
|
| 12 |
+
|
| 13 |
+
# Add the project root to the Python path to allow imports
|
| 14 |
+
# sys.path.append(os.path.dirname(os.path.abspath(__file__))) # This might not be needed depending on execution context
|
| 15 |
+
|
| 16 |
+
# Import the processing function from utils
|
| 17 |
+
try:
|
| 18 |
+
# Assuming utils.py is in the same directory or PYTHONPATH is set correctly
|
| 19 |
+
from utils import process_email_request, load_spacy_model, load_model_pipeline # Import necessary functions
|
| 20 |
except ImportError as e:
|
| 21 |
+
print(f"ERROR in api.py: Could not import from utils. Details: {e}")
|
| 22 |
+
# Define dummy function if import fails to allow FastAPI to start
|
| 23 |
+
def process_email_request(email_body: str):
|
| 24 |
+
return {"error": f"Failed to import processing function: {e}"}
|
| 25 |
+
# Define dummy loaders
|
| 26 |
+
def load_spacy_model(): print("Dummy spacy loader called"); return None
|
| 27 |
+
def load_model_pipeline(): print("Dummy pipeline loader called"); return None
|
| 28 |
|
| 29 |
+
app = FastAPI(title="Email PII Classifier API", version="1.0.0")
|
|
|
|
|
|
|
|
|
|
| 30 |
|
|
|
|
| 31 |
class EmailInput(BaseModel):
|
| 32 |
email_body: str = Field(..., example="Hello, my name is Jane Doe and my email is jane.doe@example.com. I have a billing question.")
|
| 33 |
|
|
|
|
| 36 |
classification: str = Field(..., example="full_name")
|
| 37 |
entity: str = Field(..., example="Jane Doe")
|
| 38 |
|
| 39 |
+
class EmailResponse(BaseModel):
|
| 40 |
input_email_body: str
|
| 41 |
list_of_masked_entities: List[MaskedEntity]
|
| 42 |
masked_email: str
|
| 43 |
category_of_the_email: str
|
| 44 |
|
| 45 |
+
# --- Load models on startup ---
|
| 46 |
+
# It's often better to load models once when the app starts
|
| 47 |
+
@app.on_event("startup")
|
| 48 |
+
async def startup_event():
|
| 49 |
+
print("FastAPI startup: Loading models...")
|
| 50 |
+
load_spacy_model() # Load spaCy model
|
| 51 |
+
load_model_pipeline() # Load classification pipeline
|
| 52 |
+
print("FastAPI startup: Model loading complete.")
|
| 53 |
|
| 54 |
+
# --- API Endpoint ---
|
| 55 |
+
@app.post("/classify_email/", response_model=Union[EmailResponse, Dict[str, str]]) # Allow dict for error response
|
| 56 |
async def classify_email(email_input: EmailInput):
|
| 57 |
"""
|
| 58 |
+
Receives email body, performs PII masking and classification.
|
|
|
|
| 59 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
try:
|
| 61 |
+
print("Received request for /classify_email/") # Log request
|
| 62 |
+
result = process_email_request(email_input.email_body)
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
+
if "error" in result:
|
| 65 |
+
# Return a 500 error if processing failed internally
|
| 66 |
+
raise HTTPException(status_code=500, detail=result["error"])
|
| 67 |
|
| 68 |
+
# Validate response structure before returning (optional but good practice)
|
| 69 |
+
# This assumes process_email_request returns the correct structure on success
|
| 70 |
+
return EmailResponse(**result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
+
except HTTPException as http_exc:
|
| 73 |
+
# Re-raise HTTP exceptions (like the 500 error above)
|
| 74 |
+
raise http_exc
|
| 75 |
except Exception as e:
|
| 76 |
+
print(f"Unexpected error in /classify_email endpoint: {e}")
|
| 77 |
+
# import traceback # Uncomment for detailed debugging
|
| 78 |
+
# print(traceback.format_exc()) # Uncomment for detailed debugging
|
| 79 |
+
# Return a generic 500 error for other unexpected issues
|
| 80 |
+
raise HTTPException(status_code=500, detail=f"An internal server error occurred: {str(e)}")
|
| 81 |
+
|
| 82 |
+
# --- Root Endpoint ---
|
| 83 |
+
@app.get("/")
|
| 84 |
+
async def read_root():
|
| 85 |
+
return {"message": "Welcome to the Email PII Classifier API. Use the /docs endpoint for details."}
|
| 86 |
|
| 87 |
+
# --- Optional: Add uvicorn runner for local testing ---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
if __name__ == "__main__":
|
| 89 |
import uvicorn
|
| 90 |
print("Starting Uvicorn server directly (for debugging)...")
|
|
|
|
| 91 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
models.py
CHANGED
|
@@ -9,7 +9,6 @@ from pathlib import Path
|
|
| 9 |
import re
|
| 10 |
from fastapi import FastAPI, HTTPException
|
| 11 |
from pydantic import BaseModel
|
| 12 |
-
from utils import clean_text_for_classification
|
| 13 |
import spacy
|
| 14 |
import pickle
|
| 15 |
|
|
@@ -55,6 +54,15 @@ def load_model_pipeline() -> Optional[Pipeline]:
|
|
| 55 |
print("Please train and save the model pipeline first.")
|
| 56 |
return model_pipeline
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
# --- Mask PII Function ---
|
| 59 |
def mask_pii(text: str, nlp_model: spacy.language.Language) -> Tuple[str, List[Dict[str, Any]]]:
|
| 60 |
"""
|
|
@@ -90,22 +98,21 @@ def mask_pii(text: str, nlp_model: spacy.language.Language) -> Tuple[str, List[D
|
|
| 90 |
def predict_category(text: str, pipeline: Pipeline) -> str:
|
| 91 |
"""
|
| 92 |
Predicts the category of the text using the loaded classification pipeline.
|
| 93 |
-
|
| 94 |
-
Args:
|
| 95 |
-
text: The input text (potentially masked).
|
| 96 |
-
pipeline: The loaded scikit-learn compatible pipeline object.
|
| 97 |
-
|
| 98 |
-
Returns:
|
| 99 |
-
The predicted category name as a string.
|
| 100 |
"""
|
| 101 |
-
print(f"Executing predict_category for text: '{text[:50]}...'")
|
| 102 |
try:
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
category = str(prediction[0]) if prediction else "Prediction failed"
|
| 105 |
except Exception as e:
|
| 106 |
print(f"Error during prediction: {e}")
|
| 107 |
category = "Prediction Error"
|
| 108 |
-
print(f"predict_category result: {category}")
|
| 109 |
return category
|
| 110 |
|
| 111 |
# --- Training Function ---
|
|
|
|
| 9 |
import re
|
| 10 |
from fastapi import FastAPI, HTTPException
|
| 11 |
from pydantic import BaseModel
|
|
|
|
| 12 |
import spacy
|
| 13 |
import pickle
|
| 14 |
|
|
|
|
| 54 |
print("Please train and save the model pipeline first.")
|
| 55 |
return model_pipeline
|
| 56 |
|
| 57 |
+
# --- Text Cleaning Function ---
|
| 58 |
+
def clean_text_for_classification(text: str) -> str:
|
| 59 |
+
"""Basic text cleaning."""
|
| 60 |
+
text = text.lower()
|
| 61 |
+
text = re.sub(r'<.*?>', '', text) # Remove HTML tags
|
| 62 |
+
text = re.sub(r'[^a-z\s]', '', text) # Remove non-alpha and non-whitespace
|
| 63 |
+
text = re.sub(r'\s+', ' ', text).strip() # Normalize whitespace
|
| 64 |
+
return text
|
| 65 |
+
|
| 66 |
# --- Mask PII Function ---
|
| 67 |
def mask_pii(text: str, nlp_model: spacy.language.Language) -> Tuple[str, List[Dict[str, Any]]]:
|
| 68 |
"""
|
|
|
|
| 98 |
def predict_category(text: str, pipeline: Pipeline) -> str:
|
| 99 |
"""
|
| 100 |
Predicts the category of the text using the loaded classification pipeline.
|
| 101 |
+
Applies cleaning before prediction.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
"""
|
| 103 |
+
print(f"Executing predict_category for text: '{text[:50]}...'")
|
| 104 |
try:
|
| 105 |
+
# Clean the text first using the function now in this file
|
| 106 |
+
cleaned_text = clean_text_for_classification(text)
|
| 107 |
+
print(f"Cleaned text for prediction: '{cleaned_text[:50]}...'")
|
| 108 |
+
|
| 109 |
+
# Assuming the pipeline has a .predict() method
|
| 110 |
+
prediction = pipeline.predict([cleaned_text]) # Predict on cleaned text
|
| 111 |
category = str(prediction[0]) if prediction else "Prediction failed"
|
| 112 |
except Exception as e:
|
| 113 |
print(f"Error during prediction: {e}")
|
| 114 |
category = "Prediction Error"
|
| 115 |
+
print(f"predict_category result: {category}")
|
| 116 |
return category
|
| 117 |
|
| 118 |
# --- Training Function ---
|
utils.py
CHANGED
|
@@ -1,27 +1,31 @@
|
|
| 1 |
import re
|
| 2 |
import spacy
|
| 3 |
-
from typing import List, Dict, Tuple
|
| 4 |
import pickle
|
| 5 |
from pathlib import Path
|
| 6 |
-
import os
|
| 7 |
|
| 8 |
-
#
|
| 9 |
try:
|
| 10 |
-
from
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
except ImportError as e:
|
| 13 |
-
print(f"ERROR in utils.py: Could not import
|
| 14 |
-
# Define dummy functions if import fails
|
| 15 |
-
def mask_pii(text, nlp_model): return "Masking failed", []
|
| 16 |
def predict_category(text, pipeline): return "Classification failed"
|
| 17 |
|
| 18 |
# --- Model Loading ---
|
| 19 |
MODEL_DIR = Path("saved_models")
|
| 20 |
MODEL_PATH = MODEL_DIR / "email_classifier_pipeline.pkl"
|
| 21 |
-
NLP_MODEL = None
|
| 22 |
-
MODEL_PIPELINE = None
|
| 23 |
|
| 24 |
-
def load_spacy_model():
|
| 25 |
"""Loads the spaCy model."""
|
| 26 |
global NLP_MODEL
|
| 27 |
if NLP_MODEL is None:
|
|
@@ -41,7 +45,7 @@ def load_spacy_model():
|
|
| 41 |
NLP_MODEL = None # Ensure it remains None if loading fails
|
| 42 |
return NLP_MODEL
|
| 43 |
|
| 44 |
-
def load_model_pipeline() -> Pipeline
|
| 45 |
"""Loads the classification pipeline from the .pkl file."""
|
| 46 |
global MODEL_PIPELINE
|
| 47 |
if MODEL_PIPELINE is None:
|
|
@@ -71,14 +75,14 @@ REGEX_PATTERNS = {
|
|
| 71 |
"dob": r'\b(\d{1,2}[-/]\d{1,2}[-/]\d{2,4}|\d{4}[-/]\d{1,2}[-/]\d{1,2})\b' # Basic DOB patterns
|
| 72 |
}
|
| 73 |
|
| 74 |
-
# --- PII Masking Function ---
|
| 75 |
-
|
| 76 |
-
def mask_pii(text: str) -> Tuple[str, List[Dict]]:
|
| 77 |
"""
|
| 78 |
Detects and masks PII in the input text using spaCy NER and Regex.
|
| 79 |
|
| 80 |
Args:
|
| 81 |
text: The input email body string.
|
|
|
|
| 82 |
|
| 83 |
Returns:
|
| 84 |
A tuple containing:
|
|
@@ -139,8 +143,7 @@ def mask_pii(text: str) -> Tuple[str, List[Dict]]:
|
|
| 139 |
|
| 140 |
return masked_text, list_of_masked_entities
|
| 141 |
|
| 142 |
-
# --- Main Processing Function
|
| 143 |
-
|
| 144 |
def process_email_request(email_body: str) -> dict:
|
| 145 |
"""
|
| 146 |
Processes the input email body for PII masking and classification.
|
|
@@ -188,14 +191,3 @@ def process_email_request(email_body: str) -> dict:
|
|
| 188 |
"error": f"An error occurred during processing: {str(e)}",
|
| 189 |
"input_email_body": email_body
|
| 190 |
}
|
| 191 |
-
|
| 192 |
-
# --- Other Utility Functions (Add as needed) ---
|
| 193 |
-
# E.g., text cleaning for classification model input
|
| 194 |
-
def clean_text_for_classification(text: str) -> str:
|
| 195 |
-
"""Basic text cleaning."""
|
| 196 |
-
text = text.lower()
|
| 197 |
-
text = re.sub(r'<.*?>', '', text) # Remove HTML tags
|
| 198 |
-
text = re.sub(r'[^a-z\s]', '', text) # Remove punctuation/numbers (adjust if needed)
|
| 199 |
-
text = re.sub(r'\s+', ' ', text).strip() # Normalize whitespace
|
| 200 |
-
# Add stopword removal if necessary (import nltk stopwords)
|
| 201 |
-
return text
|
|
|
|
| 1 |
import re
|
| 2 |
import spacy
|
| 3 |
+
from typing import List, Dict, Tuple, Optional, Union
|
| 4 |
import pickle
|
| 5 |
from pathlib import Path
|
| 6 |
+
import os
|
| 7 |
|
| 8 |
+
# --- Define/Import Pipeline Type FIRST ---
|
| 9 |
try:
|
| 10 |
+
from sklearn.pipeline import Pipeline
|
| 11 |
+
except ImportError:
|
| 12 |
+
Pipeline = object # type: ignore
|
| 13 |
+
|
| 14 |
+
# --- Import from models.py ---
|
| 15 |
+
try:
|
| 16 |
+
from models import predict_category # Keep this import
|
| 17 |
+
print("Successfully imported predict_category from models.py")
|
| 18 |
except ImportError as e:
|
| 19 |
+
print(f"ERROR in utils.py: Could not import predict_category from models.py. Details: {e}")
|
|
|
|
|
|
|
| 20 |
def predict_category(text, pipeline): return "Classification failed"
|
| 21 |
|
| 22 |
# --- Model Loading ---
|
| 23 |
MODEL_DIR = Path("saved_models")
|
| 24 |
MODEL_PATH = MODEL_DIR / "email_classifier_pipeline.pkl"
|
| 25 |
+
NLP_MODEL: Optional[spacy.language.Language] = None
|
| 26 |
+
MODEL_PIPELINE: Optional[Pipeline] = None # Now Pipeline is defined
|
| 27 |
|
| 28 |
+
def load_spacy_model() -> Optional[spacy.language.Language]:
|
| 29 |
"""Loads the spaCy model."""
|
| 30 |
global NLP_MODEL
|
| 31 |
if NLP_MODEL is None:
|
|
|
|
| 45 |
NLP_MODEL = None # Ensure it remains None if loading fails
|
| 46 |
return NLP_MODEL
|
| 47 |
|
| 48 |
+
def load_model_pipeline() -> Optional[Pipeline]: # Now Pipeline is defined
|
| 49 |
"""Loads the classification pipeline from the .pkl file."""
|
| 50 |
global MODEL_PIPELINE
|
| 51 |
if MODEL_PIPELINE is None:
|
|
|
|
| 75 |
"dob": r'\b(\d{1,2}[-/]\d{1,2}[-/]\d{2,4}|\d{4}[-/]\d{1,2}[-/]\d{1,2})\b' # Basic DOB patterns
|
| 76 |
}
|
| 77 |
|
| 78 |
+
# --- PII Masking Function (Defined within utils.py) ---
|
| 79 |
+
def mask_pii(text: str, nlp: spacy.language.Language) -> Tuple[str, List[Dict]]:
|
|
|
|
| 80 |
"""
|
| 81 |
Detects and masks PII in the input text using spaCy NER and Regex.
|
| 82 |
|
| 83 |
Args:
|
| 84 |
text: The input email body string.
|
| 85 |
+
nlp: The spaCy language model.
|
| 86 |
|
| 87 |
Returns:
|
| 88 |
A tuple containing:
|
|
|
|
| 143 |
|
| 144 |
return masked_text, list_of_masked_entities
|
| 145 |
|
| 146 |
+
# --- Main Processing Function (Defined within utils.py) ---
|
|
|
|
| 147 |
def process_email_request(email_body: str) -> dict:
|
| 148 |
"""
|
| 149 |
Processes the input email body for PII masking and classification.
|
|
|
|
| 191 |
"error": f"An error occurred during processing: {str(e)}",
|
| 192 |
"input_email_body": email_body
|
| 193 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|