siddharth786 commited on
Commit
b20f676
·
1 Parent(s): 0897779

Fix circular import and NameError issues

Browse files
Files changed (3) hide show
  1. api.py +57 -58
  2. models.py +18 -11
  3. utils.py +20 -28
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 os # To potentially load environment variables if needed
 
5
 
6
- # Make sure these imports work and don't cause errors themselves
7
  try:
8
- from utils import mask_pii
9
- from models import load_model_pipeline, predict_category, Pipeline # Ensure Pipeline is imported if type hint used
 
 
 
 
 
 
 
 
 
10
  except ImportError as e:
11
- print(f"Error importing from utils or models in api.py: {e}")
12
- # Optionally raise the error to make it obvious during startup
13
- # raise e
14
- except Exception as e:
15
- print(f"Unexpected error during imports in api.py: {e}")
16
- # raise e
 
17
 
18
- # --- FastAPI App ---
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 ClassificationOutput(BaseModel):
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 Model at Startup ---
39
- # Ensure load_model_pipeline() exists and works
40
- model_pipeline: Optional[Pipeline] = load_model_pipeline()
41
-
42
- # --- API Endpoints ---
43
- @app.get("/")
44
- async def read_root():
45
- return {"message": "Email Classification API is running. Use the /classify/ endpoint."}
46
 
47
- @app.post("/classify/", response_model=ClassificationOutput)
 
48
  async def classify_email(email_input: EmailInput):
49
  """
50
- Accepts an email body, masks PII, classifies the email,
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
- # 1. Mask PII
60
- masked_email_body, entities = mask_pii(input_email)
61
-
62
- # Ensure entities match the Pydantic model format
63
- validated_entities = [MaskedEntity(**entity) for entity in entities]
64
 
65
- # 2. Classify the masked email
66
- predicted_class = predict_category(masked_email_body, model_pipeline)
 
67
 
68
- # 3. Construct the response
69
- response = ClassificationOutput(
70
- input_email_body=input_email,
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
- # Log the exception for debugging
79
- print(f"Error processing request: {e}")
80
- # Optionally include more detail in the error response during development
81
- raise HTTPException(status_code=500, detail=f"Internal Server Error: {str(e)}")
 
 
 
 
 
 
82
 
83
- # --- Running the API (for local development) ---
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]}...'") # Add log
102
  try:
103
- prediction = pipeline.predict([text])
 
 
 
 
 
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}") # Add log
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 # Import os to handle potential path issues
7
 
8
- # Assuming mask_pii and predict_category are defined in models.py
9
  try:
10
- from models import mask_pii, predict_category
11
- print("Successfully imported functions from models.py") # Add this for confirmation
 
 
 
 
 
 
12
  except ImportError as e:
13
- print(f"ERROR in utils.py: Could not import functions/classes from models.py. Details: {e}")
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 | None:
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 for Gradio ---
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
  }