Vivek0912 commited on
Commit
f404c1e
·
1 Parent(s): 66aef2b

added some new endpoint

Browse files
backend/app/api/endpoints.py CHANGED
@@ -1,4 +1,5 @@
1
 
 
2
  from fastapi import APIRouter, HTTPException, UploadFile, File, Depends
3
  from typing import List, Optional
4
  from pydantic import BaseModel
@@ -14,12 +15,20 @@ from app.services.email_reader import parse_email_bytes, read_emails_from_direct
14
  from app.services.duplicate_checker import check_duplicate
15
  from app.models.email_model import EmailData
16
  from app.models.request_type_model import RequestTypeModel
17
- from app.services.gemeni_classification import analyze_intent, classify_email_gemeni, extract_text_from_attachment, get_primary_intent
18
  from app.services.retrieve_email_process import process_single_email
19
  from config import settings
20
 
21
  router = APIRouter()
22
 
 
 
 
 
 
 
 
 
 
23
 
24
  @router.post("/process-emails-upload/", response_model=List[EmailData])
25
  async def process_email_files(files: List[UploadFile] = File(...)):
@@ -37,6 +46,7 @@ async def process_email_files(files: List[UploadFile] = File(...)):
37
  sub_request_type=email_result["sub_request_type"],
38
  confidence_score=email_result["confidence_score"],
39
  duplicate_flag=email_result["duplicate_flag"],
 
40
  )
41
  results.append(email_resp)
42
  except Exception as e:
@@ -80,3 +90,54 @@ async def process_email_directory():
80
  except Exception as e:
81
  print(f"Error processing {file_path}: {e}")
82
  return results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
 
2
+ from pathlib import Path
3
  from fastapi import APIRouter, HTTPException, UploadFile, File, Depends
4
  from typing import List, Optional
5
  from pydantic import BaseModel
 
15
  from app.services.duplicate_checker import check_duplicate
16
  from app.models.email_model import EmailData
17
  from app.models.request_type_model import RequestTypeModel
 
18
  from app.services.retrieve_email_process import process_single_email
19
  from config import settings
20
 
21
  router = APIRouter()
22
 
23
+ # Define the directory where attachments will be saved
24
+ SAVE_DIR = Path("data/attachments")
25
+ SAVE_FILE_PATH = SAVE_DIR / settings.settings.ALLOWED_PRIORITY_RULES_FILENAME
26
+
27
+ if not SAVE_DIR.exists():
28
+ SAVE_DIR.mkdir(parents=True)
29
+ print(f"Directory created: {SAVE_DIR}")
30
+ else:
31
+ print(f"Directory already exists: {SAVE_DIR}")
32
 
33
  @router.post("/process-emails-upload/", response_model=List[EmailData])
34
  async def process_email_files(files: List[UploadFile] = File(...)):
 
46
  sub_request_type=email_result["sub_request_type"],
47
  confidence_score=email_result["confidence_score"],
48
  duplicate_flag=email_result["duplicate_flag"],
49
+ all_extracted_numbers=email_result["extracted_numbers_list"]
50
  )
51
  results.append(email_resp)
52
  except Exception as e:
 
90
  except Exception as e:
91
  print(f"Error processing {file_path}: {e}")
92
  return results
93
+
94
+
95
+ @router.post("/upload-priority-rules/",
96
+ description="""
97
+ Upload a JSON file containing the priority rules for request type identification and numerical extraction.
98
+ **Sample JSON file structure:**
99
+
100
+ ```json
101
+ {
102
+ "priority_rules": {
103
+ "is_prioritization_extraction": true,
104
+ "request_type_identification": {
105
+ "order": ["email_content", "document_content"],
106
+ "fallback": "document_content"
107
+ },
108
+ "numerical_field_extraction": {
109
+ "preferred_source": ["attachments"],
110
+ "fallback": "email_body"
111
+ },
112
+ "banking_numeric_keys": [
113
+ "loan_amount",
114
+ "balance",
115
+ "interest_rate"
116
+ .......
117
+ ]
118
+ }
119
+ }"
120
+ """
121
+ )
122
+ async def upload_rules(file: UploadFile = File(...)):
123
+
124
+ # Validate filename
125
+ if file.filename != settings.settings.ALLOWED_PRIORITY_RULES_FILENAME:
126
+ raise HTTPException(
127
+ status_code=400,
128
+ detail=f"Invalid file name. Please use the appropriate file name: {settings.settings.ALLOWED_PRIORITY_RULES_FILENAME}"
129
+ )
130
+
131
+ # Check if file is a JSON
132
+ if file.content_type != "application/json":
133
+ raise HTTPException(status_code=400, detail="Only JSON files are allowed.")
134
+
135
+ # Save file to the specified location
136
+ try:
137
+ file_content = await file.read()
138
+ with open(SAVE_FILE_PATH, "wb") as f:
139
+ f.write(file_content)
140
+ except Exception as e:
141
+ raise HTTPException(status_code=500, detail=f"Error saving file: {e}")
142
+
143
+ return {"message": "Rules JSON uploaded successfully."}
backend/app/models/email_model.py CHANGED
@@ -1,5 +1,5 @@
1
  from pydantic import BaseModel
2
- from typing import List, Optional
3
 
4
  class EmailData(BaseModel):
5
  sender: str
@@ -8,3 +8,4 @@ class EmailData(BaseModel):
8
  sub_request_type: Optional[str] = None
9
  confidence_score: Optional[float] = None
10
  duplicate_flag: bool = False
 
 
1
  from pydantic import BaseModel
2
+ from typing import Any, Dict, List, Optional
3
 
4
  class EmailData(BaseModel):
5
  sender: str
 
8
  sub_request_type: Optional[str] = None
9
  confidence_score: Optional[float] = None
10
  duplicate_flag: bool = False
11
+ all_extracted_numbers: Optional[List[Dict[str, Any]]] = None
backend/app/services/gemeni_classification.py CHANGED
@@ -1,4 +1,5 @@
1
- from config.settings import Settings
 
2
  import google.generativeai as genai
3
  import json
4
  import email
@@ -6,6 +7,8 @@ import io
6
  import PyPDF2
7
  import docx
8
  import mimetypes
 
 
9
 
10
 
11
  genai.configure(api_key=Settings.GEMENI_API_KEY_TOKEN)
@@ -97,7 +100,6 @@ def analyze_intent(text):
97
  print(f"Gemini API error (Intent): {e}")
98
  return ""
99
 
100
-
101
  def classify_email_gemeni(subject, body):
102
  """Classifies an email based on request type and sub-request type."""
103
  results = []
@@ -119,8 +121,6 @@ def classify_email_gemeni(subject, body):
119
  }}
120
  """
121
 
122
-
123
-
124
  response = model.generate_content(PROMPT)
125
 
126
  if response and hasattr(response, "_result"):
@@ -158,4 +158,59 @@ def get_primary_intent(email_content, attach_content):
158
  return response.text # Extracted primary intent
159
 
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from typing import List, Optional
3
  import google.generativeai as genai
4
  import json
5
  import email
 
7
  import PyPDF2
8
  import docx
9
  import mimetypes
10
+
11
+ from config.settings import Settings
12
 
13
 
14
  genai.configure(api_key=Settings.GEMENI_API_KEY_TOKEN)
 
100
  print(f"Gemini API error (Intent): {e}")
101
  return ""
102
 
 
103
  def classify_email_gemeni(subject, body):
104
  """Classifies an email based on request type and sub-request type."""
105
  results = []
 
121
  }}
122
  """
123
 
 
 
124
  response = model.generate_content(PROMPT)
125
 
126
  if response and hasattr(response, "_result"):
 
158
  return response.text # Extracted primary intent
159
 
160
 
161
+ def extract_key_number_with_llm(context: str, rules: Optional[dict] = None) -> Optional[str]:
162
+ """
163
+ Uses the Gemini LLM to extract the most appropriate numerical value(s)
164
+ from the provided context. The prompt instructs the model to return a
165
+ JSON array of objects if more than one key is found. Each object should
166
+ have the banking key as the key and the corresponding numerical value as its value.
167
+
168
+ :param context: The text context from which key numerical value(s) should be extracted.
169
+ :param rules: Optional rules dictionary. If not provided, it will be loaded.
170
+ :return: The extracted key numerical value(s) as a JSON array (list of dicts), or None if not found.
171
+ """
172
+
173
+ banking_keys = rules.get("priority_rules", {}).get("banking_numeric_keys", [])
174
+ keys_str = ", ".join(banking_keys)
175
+
176
+ # Construct the prompt instructing the model to return a JSON array
177
+ prompt = (
178
+ f"Analyze the following context and extract all relevant numerical values that represent key banking references. "
179
+ f"Consider the following banking numeric keys as potential candidates: {keys_str}. "
180
+ f"If you find more than one, return them in a JSON array. Each element in the array should be an object with "
181
+ f"the banking key as the key and the corresponding number as its value. If only one is found, still return a JSON array "
182
+ f"with a single object. Return only the JSON array with no additional text.\n\n"
183
+ f"Context: {context}"
184
+ )
185
+
186
+ try:
187
+ response = model.generate_content(prompt)
188
+ if response and hasattr(response, "_result"):
189
+ text_response = response._result.candidates[0].content.parts[0].text.strip()
190
+ # Clean the JSON response by removing code fences if they exist.
191
+ cleaned_response = clean_json_response(text_response)
192
+ try:
193
+ # Validate that the cleaned response is valid JSON.
194
+ parsed = json.loads(cleaned_response)
195
+ # Re-serialize the parsed object to get a standardized JSON string.
196
+ return json.dumps(parsed)
197
+ except Exception as json_err:
198
+ print(f"JSON parsing error: {json_err}")
199
+ print("Cleaned Response received:", cleaned_response)
200
+ return None
201
+ except Exception as e:
202
+ print(f"Error during LLM extraction: {e}")
203
+
204
+ return None
205
+
206
 
207
+ def clean_json_response(text: str) -> str:
208
+ """
209
+ Removes markdown code fences and any extra backticks from the response.
210
+ """
211
+ # Remove leading and trailing backticks or code fence markers
212
+ # Remove a leading "```json" if present
213
+ text = re.sub(r"^```json", "", text).strip()
214
+ # Remove trailing "```" if present
215
+ text = re.sub(r"```$", "", text).strip()
216
+ return text
backend/app/services/retrieve_email_process.py CHANGED
@@ -1,41 +1,96 @@
1
 
2
- from typing import Optional
 
 
 
 
3
  from app.services.duplicate_checker import check_duplicate
4
  from app.services.email_reader import parse_email_bytes
5
- from app.services.gemeni_classification import classify_email_gemeni, extract_text_from_attachment
 
6
 
7
 
8
  async def process_single_email(file_content: bytes, filename: str) -> Optional[dict]:
9
  """Processes a single email content."""
 
 
 
 
 
 
 
10
  email_data = parse_email_bytes(file_content, filename)
11
  if email_data:
12
- attachment_text = ""
13
- for attachment in email_data["attachments"]:
14
- attachment_text += extract_text_from_attachment(attachment["content"], attachment["filename"])
15
 
16
  email_chain_text = email_data["email_chain_text"]
17
  email_body_text = email_data["body"]
18
 
19
- # 1. Separate Classification:
20
- document_result = classify_email_gemeni(email_data["subject"], attachment_text) if attachment_text else ("Unknown", "Unknown", "0")
21
- email_chain_result = classify_email_gemeni(email_data["subject"], email_chain_text) if email_chain_text else ("Unknown", "Unknown", "0")
22
- primary_email_result = classify_email_gemeni(email_data["subject"], email_body_text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- # 2. Confidence Score Comparison:
25
- document_confidence = float(document_result[2])
26
- email_chain_confidence = float(email_chain_result[2])
27
- primary_email_confidence = float(primary_email_result[2])
 
28
 
29
- best_result = primary_email_result # Default to email body
30
- if document_confidence > primary_email_confidence and document_confidence > email_chain_confidence:
31
- best_result = document_result
32
- elif email_chain_confidence > primary_email_confidence and email_chain_confidence > document_confidence:
33
- best_result = email_chain_result
34
 
35
- # 3. Refined Classification:
36
- request_type = best_result[0]
37
- sub_request_type = best_result[1]
38
- confidence_score = best_result[2]
 
 
 
 
 
 
39
 
40
  duplicate_flag, duplicate_reason = check_duplicate(email_data["body"])
41
  email_obj = {
@@ -45,8 +100,56 @@ async def process_single_email(file_content: bytes, filename: str) -> Optional[d
45
  "sub_request_type": sub_request_type,
46
  "confidence_score": confidence_score,
47
  "duplicate_flag": duplicate_flag,
 
48
  }
49
  return email_obj
50
  else:
51
  print(f"Parsing failed for file: {filename}")
52
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
 
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+ import re
6
+ from typing import List, Optional
7
  from app.services.duplicate_checker import check_duplicate
8
  from app.services.email_reader import parse_email_bytes
9
+ from app.services.gemeni_classification import classify_email_gemeni, extract_key_number_with_llm, extract_text_from_attachment
10
+ from config import settings
11
 
12
 
13
  async def process_single_email(file_content: bytes, filename: str) -> Optional[dict]:
14
  """Processes a single email content."""
15
+ # Load customizable priority rules
16
+ rules = load_priority_rules() # Expected to return a dict with "priority_rules" key
17
+ priority_config = rules.get("priority_rules", {})
18
+ use_priority = priority_config.get("is_prioritization_extraction", False)
19
+ all_extracted_numbers = [] # This will hold the combined results from all attachments
20
+
21
+ # Parse the email into its components
22
  email_data = parse_email_bytes(file_content, filename)
23
  if email_data:
24
+ # attachment_text = ""
25
+ # for attachment in email_data["attachments"]:
26
+ # attachment_text += extract_text_from_attachment(attachment["content"], attachment["filename"])
27
 
28
  email_chain_text = email_data["email_chain_text"]
29
  email_body_text = email_data["body"]
30
 
31
+ # Process attachments: extract text and numerical fields
32
+ attachment_text = ""
33
+ extracted_numbers = []
34
+ for attachment in email_data.get("attachments", []):
35
+ text = extract_text_from_attachment(attachment["content"], attachment["filename"])
36
+ attachment_text += text + "\n"
37
+ # extracted_numbers.extend(extract_key_number_with_llm(text,rules))
38
+ extracted_numbers_json = extract_key_number_with_llm(text, rules)
39
+ if extracted_numbers_json:
40
+ try:
41
+ parsed_result = json.loads(extracted_numbers_json) # Convert JSON string to Python object
42
+ # Check if the parsed result is a list; if so, merge it into our overall list.
43
+ if isinstance(parsed_result, list):
44
+ all_extracted_numbers.extend(parsed_result)
45
+ else:
46
+ all_extracted_numbers.append(parsed_result)
47
+ except Exception as e:
48
+ print("Error parsing JSON result from LLM:", e)
49
+
50
+ # Choose classification logic based on priority configuration
51
+ if use_priority:
52
+ # -- Priority Based Extraction Logic --
53
+ identification_order = priority_config.get("request_type_identification", {}).get("order", [])
54
+ classification_source = ""
55
+ if "email_content" in identification_order and email_body_text.strip():
56
+ classification_source = email_body_text
57
+ elif "document_content" in identification_order and attachment_text.strip():
58
+ classification_source = attachment_text
59
+ else:
60
+ classification_source = email_body_text # default fallback
61
+
62
+ primary_result = classify_email_gemeni(email_data["subject"], classification_source)
63
+
64
+ # special condition to check if priority is email content and email has multi thread then we
65
+ # have to compare confidence score with primary confidence score
66
+ if "email_content" in identification_order:
67
+ email_chain_result = classify_email_gemeni(email_data["subject"], email_chain_text) if email_chain_text else ("Unknown", "Unknown", "0")
68
+ email_chain_confidence = float(email_chain_result[2])
69
+ primary_email_confidence = float(primary_result[2])
70
+ if email_chain_confidence > primary_email_confidence and email_chain_confidence:
71
+ primary_result = email_chain_result
72
 
73
+ else:
74
+ # 1. Separate Classification:
75
+ document_result = classify_email_gemeni(email_data["subject"], attachment_text) if attachment_text else ("Unknown", "Unknown", "0")
76
+ email_chain_result = classify_email_gemeni(email_data["subject"], email_chain_text) if email_chain_text else ("Unknown", "Unknown", "0")
77
+ primary_email_result = classify_email_gemeni(email_data["subject"], email_body_text)
78
 
79
+ # 2. Confidence Score Comparison:
80
+ document_confidence = float(document_result[2])
81
+ email_chain_confidence = float(email_chain_result[2])
82
+ primary_email_confidence = float(primary_email_result[2])
 
83
 
84
+ primary_result = primary_email_result # Default to email body
85
+ if document_confidence > primary_email_confidence and document_confidence > email_chain_confidence:
86
+ primary_result = document_result
87
+ elif email_chain_confidence > primary_email_confidence and email_chain_confidence > document_confidence:
88
+ primary_result = email_chain_result
89
+
90
+ # Extract classification results
91
+ request_type = primary_result[0]
92
+ sub_request_type = primary_result[1]
93
+ confidence_score = primary_result[2]
94
 
95
  duplicate_flag, duplicate_reason = check_duplicate(email_data["body"])
96
  email_obj = {
 
100
  "sub_request_type": sub_request_type,
101
  "confidence_score": confidence_score,
102
  "duplicate_flag": duplicate_flag,
103
+ "extracted_numbers_list": all_extracted_numbers
104
  }
105
  return email_obj
106
  else:
107
  print(f"Parsing failed for file: {filename}")
108
+ return None
109
+
110
+
111
+ def load_priority_rules() -> dict:
112
+ """
113
+ Loads the priority rules from a JSON file.
114
+ The JSON file should be located at 'config/rules.json'.
115
+ If the file does not exist, a default rules dictionary is returned.
116
+
117
+ Expected JSON structure example:
118
+ {
119
+ "priority_rules": {
120
+ "is_prioritization_extraction": true,
121
+ "request_type_identification": {
122
+ "order": ["email_content", "document_content"],
123
+ "fallback": "document_content"
124
+ },
125
+ "numerical_field_extraction": {
126
+ "preferred_source": ["attachments"],
127
+ "fallback": "email_body"
128
+ }
129
+ }
130
+ }
131
+ """
132
+ # Get the rules directory and filename from environment variables
133
+ RULES_FILENAME = settings.settings.ALLOWED_PRIORITY_RULES_FILENAME
134
+ RULES_DIR = Path("data/attachments")
135
+ # Build the full file path using pathlib
136
+ RULES_FILE_PATH = Path(RULES_DIR) / RULES_FILENAME
137
+
138
+ if os.path.exists(RULES_FILE_PATH):
139
+ with open(RULES_FILE_PATH, "r") as file:
140
+ return json.load(file)
141
+
142
+ # Return default rules if file doesn't exist
143
+ return {
144
+ "priority_rules": {
145
+ "is_prioritization_extraction": False,
146
+ "request_type_identification": {
147
+ "order": ["email_content", "document_content"],
148
+ "fallback": "document_content"
149
+ },
150
+ "numerical_field_extraction": {
151
+ "preferred_source": ["attachments"],
152
+ "fallback": "email_body"
153
+ }
154
+ }
155
+ }
backend/config/settings.py CHANGED
@@ -9,15 +9,14 @@ class Settings:
9
  ENV = os.getenv("ENV") # Default to "local" if ENV is not set
10
  # API Token
11
  HUGGINGFACE_API_TOKEN = os.getenv("HUGGINGFACE_API_TOKEN") or os.environ.get("HUGGINGFACE_API_TOKEN")
12
-
13
- GEMENI_API_KEY_TOKEN = os.getenv("GEMENI_API_KEY") or os.environ.get("GEMENI_API_KEY")
14
 
 
15
 
16
  # Model Path
17
  MODEL_NAME = os.getenv("MODEL_NAME") or os.environ.get("MODEL_NAME") # Default if not set
18
  OCR_LANGUAGE = os.getenv("OCR_LANGUAGE", "eng")
19
  directory_path = os.getenv("EMAIL_DIRECTORY_PATH") or os.environ.get("EMAIL_DIRECTORY_PATH") # Make configurable
20
-
21
  # Debugging info
22
  print(f"Running in {ENV} mode with model path: {MODEL_NAME}")
23
  print(f"Running in {ENV} mode with TOKEN: {HUGGINGFACE_API_TOKEN}")
 
9
  ENV = os.getenv("ENV") # Default to "local" if ENV is not set
10
  # API Token
11
  HUGGINGFACE_API_TOKEN = os.getenv("HUGGINGFACE_API_TOKEN") or os.environ.get("HUGGINGFACE_API_TOKEN")
 
 
12
 
13
+ GEMENI_API_KEY_TOKEN = os.getenv("GEMENI_API_KEY") or os.environ.get("GEMENI_API_KEY")
14
 
15
  # Model Path
16
  MODEL_NAME = os.getenv("MODEL_NAME") or os.environ.get("MODEL_NAME") # Default if not set
17
  OCR_LANGUAGE = os.getenv("OCR_LANGUAGE", "eng")
18
  directory_path = os.getenv("EMAIL_DIRECTORY_PATH") or os.environ.get("EMAIL_DIRECTORY_PATH") # Make configurable
19
+ ALLOWED_PRIORITY_RULES_FILENAME=os.getenv("ALLOWED_PRIORITY_RULES_FILENAME") or os.environ.get("ALLOWED_PRIORITY_RULES_FILENAME") # Make configurable
20
  # Debugging info
21
  print(f"Running in {ENV} mode with model path: {MODEL_NAME}")
22
  print(f"Running in {ENV} mode with TOKEN: {HUGGINGFACE_API_TOKEN}")