AIEmailAutomationUtility 0.0.33__py3-none-any.whl → 0.0.35__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- AIEmailAutomationUtility/Email_Draft.py +26 -2
- AIEmailAutomationUtility/Email_Read.py +1 -1
- AIEmailAutomationUtility/Process_Category.py +114 -18
- {AIEmailAutomationUtility-0.0.33.dist-info → AIEmailAutomationUtility-0.0.35.dist-info}/METADATA +3 -3
- {AIEmailAutomationUtility-0.0.33.dist-info → AIEmailAutomationUtility-0.0.35.dist-info}/RECORD +8 -8
- {AIEmailAutomationUtility-0.0.33.dist-info → AIEmailAutomationUtility-0.0.35.dist-info}/LICENCE.txt +0 -0
- {AIEmailAutomationUtility-0.0.33.dist-info → AIEmailAutomationUtility-0.0.35.dist-info}/WHEEL +0 -0
- {AIEmailAutomationUtility-0.0.33.dist-info → AIEmailAutomationUtility-0.0.35.dist-info}/top_level.txt +0 -0
@@ -4,10 +4,10 @@ from email.message import Message
|
|
4
4
|
import datetime
|
5
5
|
import time
|
6
6
|
import loggerutility as logger
|
7
|
-
from flask import Flask,request
|
8
7
|
import json
|
9
8
|
from email.mime.multipart import MIMEMultipart
|
10
9
|
from email.mime.text import MIMEText
|
10
|
+
from bs4 import BeautifulSoup
|
11
11
|
|
12
12
|
class Email_Draft:
|
13
13
|
def draft_email(self, email_config, email_details, response_content):
|
@@ -60,6 +60,7 @@ class Email_Draft:
|
|
60
60
|
# Extract response content
|
61
61
|
body = response_content.get('body', '')
|
62
62
|
table_html = response_content.get('table_', '')
|
63
|
+
aligned_table_html = self.align_numeric_cells_right(table_html)
|
63
64
|
signature = response_content.get('signature', '')
|
64
65
|
email_body = email_details['body']
|
65
66
|
|
@@ -72,7 +73,7 @@ class Email_Draft:
|
|
72
73
|
html_content = (
|
73
74
|
"<html><body>"
|
74
75
|
f"<p>{body_html}</p>"
|
75
|
-
f"{
|
76
|
+
f"{aligned_table_html}"
|
76
77
|
f"<b>GST Extra as per applicable.</b>"
|
77
78
|
f"<p>{signature_html}</p>"
|
78
79
|
"<hr>"
|
@@ -174,3 +175,26 @@ class Email_Draft:
|
|
174
175
|
|
175
176
|
except Exception as e:
|
176
177
|
logger.log(f"Error in Draft_Save: {str(e)}")
|
178
|
+
|
179
|
+
def align_numeric_cells_right(self,table_html):
|
180
|
+
soup = BeautifulSoup(table_html, 'html.parser')
|
181
|
+
rows = soup.find_all('tr')
|
182
|
+
|
183
|
+
for row in rows[1:]: # Skip header row
|
184
|
+
cells = row.find_all('td')
|
185
|
+
for cell in cells:
|
186
|
+
text = cell.get_text(strip=True)
|
187
|
+
# Check if the content is numeric (int or float)
|
188
|
+
try:
|
189
|
+
float(text.replace(',', '')) # Handle values like '7,500.00'
|
190
|
+
# If 'text-align: right;' is not present, add it
|
191
|
+
style = cell.get('style', '')
|
192
|
+
if 'text-align: right' not in style:
|
193
|
+
if style:
|
194
|
+
style += '; '
|
195
|
+
style += 'text-align: right;'
|
196
|
+
cell['style'] = style
|
197
|
+
except ValueError:
|
198
|
+
continue # Not numeric
|
199
|
+
|
200
|
+
return str(soup)
|
@@ -16,6 +16,8 @@ import csv
|
|
16
16
|
import os
|
17
17
|
from email import message_from_string
|
18
18
|
import re
|
19
|
+
import weaviate
|
20
|
+
from weaviate.gql.get import HybridFusion
|
19
21
|
|
20
22
|
class Process_Category:
|
21
23
|
|
@@ -168,8 +170,28 @@ class Process_Category:
|
|
168
170
|
elif category == "Quotation":
|
169
171
|
action_taken = f"Mail drafted for products rate"
|
170
172
|
responseMethod, parameters = self.get_JsonArray_values(category, file_JsonArray)
|
171
|
-
logger.log(f"
|
173
|
+
logger.log(f"Parameters are ::: {parameters}")
|
172
174
|
|
175
|
+
|
176
|
+
enterpriseName = parameters["Enterprise_Name"]
|
177
|
+
schema_name = parameters["Schema_Name"].capitalize().replace("-","_")
|
178
|
+
entity_type = parameters["Entity_Type"]
|
179
|
+
server_url = ""
|
180
|
+
|
181
|
+
schemaName_Updated = enterpriseName + "_" + schema_name + "_" + entity_type
|
182
|
+
logger.log(f'\nschemaName_Updated ::: \t{schemaName_Updated}')
|
183
|
+
|
184
|
+
environment_weaviate_server_url = os.getenv('weaviate_server_url')
|
185
|
+
logger.log(f"environment_weaviate_server_url ::: [{environment_weaviate_server_url}]")
|
186
|
+
|
187
|
+
if environment_weaviate_server_url != None and environment_weaviate_server_url != '':
|
188
|
+
server_url = environment_weaviate_server_url
|
189
|
+
logger.log(f"\nProcess_cat class Quotation server_url:::\t{server_url} \t{type(server_url)}","0")
|
190
|
+
else:
|
191
|
+
if 'server_url' in parameters.keys():
|
192
|
+
server_url = parameters['server_url']
|
193
|
+
logger.log(f"\nProcess_cat class Quotation server_url:::\t{server_url} \t{type(server_url)}","0")
|
194
|
+
|
173
195
|
# Step 4: Identify customer from email using AI
|
174
196
|
customer_data = self.identify_customer(email_body, subject, Model_Name, openai_api_key, geminiAI_APIKey, localAIURL, parameters["Customer_Assistant_Id"])
|
175
197
|
logger.log(f"Identified customer is ::: {customer_data}")
|
@@ -190,6 +212,8 @@ class Process_Category:
|
|
190
212
|
products = self.identify_products(email_body, subject, Model_Name, openai_api_key, geminiAI_APIKey, localAIURL, parameters["Product_Assistant_Id"])
|
191
213
|
|
192
214
|
logger.log(f'Identified Products are ::: {products}')
|
215
|
+
products=self.products_item_code_lookup(products, openai_api_key, schemaName_Updated, server_url)
|
216
|
+
logger.log(f'Identified Products after Lookup are ::: {products}')
|
193
217
|
|
194
218
|
product_determination=products
|
195
219
|
|
@@ -198,6 +222,8 @@ class Process_Category:
|
|
198
222
|
item_no = product.get("item_no", "").strip()
|
199
223
|
make = product.get("make", "").strip()
|
200
224
|
rate = None
|
225
|
+
discount = "NA"
|
226
|
+
price_pickup_source = "NA"
|
201
227
|
found_rate = False
|
202
228
|
|
203
229
|
logger.log(f"item no is ::: {item_no}")
|
@@ -233,8 +259,8 @@ class Process_Category:
|
|
233
259
|
else:
|
234
260
|
logger.log(f"Process_Category - Quotation [0] No base price found for item '{item_no}' .")
|
235
261
|
product["rate"] = "NA"
|
236
|
-
product["
|
237
|
-
product["
|
262
|
+
product["price_pickup_source"]="NA"
|
263
|
+
product["discount"]="NA"
|
238
264
|
continue
|
239
265
|
|
240
266
|
# Condition 1: Exact match in special_rate_customer_wise
|
@@ -270,7 +296,7 @@ class Process_Category:
|
|
270
296
|
|
271
297
|
if discount_result:
|
272
298
|
discount_percent = discount_result[0]
|
273
|
-
|
299
|
+
discount= discount_percent
|
274
300
|
rate = raw_price * (1 - int(discount_percent) / 100)
|
275
301
|
rate = round(rate, 2)
|
276
302
|
found_rate = True
|
@@ -295,7 +321,7 @@ class Process_Category:
|
|
295
321
|
|
296
322
|
if past_sales_result:
|
297
323
|
most_common_make, past_discount = past_sales_result
|
298
|
-
|
324
|
+
discount=past_discount
|
299
325
|
if isinstance(raw_price, (int, float)):
|
300
326
|
rate = raw_price * (1 - int(past_discount) / 100)
|
301
327
|
rate = round(rate, 2)
|
@@ -319,7 +345,7 @@ class Process_Category:
|
|
319
345
|
|
320
346
|
if general_discount_result:
|
321
347
|
general_discount_percent = general_discount_result[0]
|
322
|
-
|
348
|
+
discount=general_discount_percent
|
323
349
|
rate = raw_price * (1 - int(general_discount_percent) / 100)
|
324
350
|
rate = round(rate, 2)
|
325
351
|
found_rate = True
|
@@ -331,11 +357,11 @@ class Process_Category:
|
|
331
357
|
rate = raw_price
|
332
358
|
logger.log(f"Process_Category - Quotation [5] No discounts applied. Using base price for item '{item_no}': {rate}")
|
333
359
|
price_pickup_source="PRICE_LIST"
|
334
|
-
|
360
|
+
discount = "NA"
|
335
361
|
|
336
362
|
product["rate"] = rate
|
337
|
-
product["
|
338
|
-
product["
|
363
|
+
product["price_pickup_source"]=price_pickup_source
|
364
|
+
product["discount"]=discount
|
339
365
|
|
340
366
|
db_connection.close()
|
341
367
|
|
@@ -542,7 +568,7 @@ class Process_Category:
|
|
542
568
|
logger.log("Inside identify_products")
|
543
569
|
|
544
570
|
if model_type == "OpenAI":
|
545
|
-
prompt = f"""/* Extract complete item pricing information from the following mixed-format email content. The email may contain a combination of:- descriptive product listings- tabular structured price info- semi-structured lines with HSN, quantity, material, etc. Give me price in INR and information of all items in following format requested_description, item_no, make, description, price,
|
571
|
+
prompt = f"""/* Extract complete item pricing information from the following mixed-format email content. The email may contain a combination of:- descriptive product listings- tabular structured price info- semi-structured lines with HSN, quantity, material, etc. Give me price in INR and information of all items in following format requested_description, item_no, make, description, price, quantity, price_unit, inventory_unit strictly in JSON array format(Always return the result as a JSON **array**, even if there's only one item). If the item is not available in the assistant files return requested description from email data and rest of the columns as NA. Do not include any instruction as the output will be directly in a program. */ /n {email_body}. """
|
546
572
|
emailreplyassistant = EmailReplyAssistant()
|
547
573
|
ai_result = emailreplyassistant.identify_customer_product_reply_assitant(openai_api_key, assistant_id, email_body, subject, prompt)
|
548
574
|
|
@@ -566,6 +592,9 @@ class Process_Category:
|
|
566
592
|
if ai_result["status"] == "Success":
|
567
593
|
logger.log(f"ai_result ::: {ai_result}")
|
568
594
|
product_data = json.loads(ai_result["message"])
|
595
|
+
# If it's a dict, wrap in list
|
596
|
+
if isinstance(product_data, dict):
|
597
|
+
product_data = json.dumps([product_data], indent=4)
|
569
598
|
else:
|
570
599
|
product_data = []
|
571
600
|
return product_data
|
@@ -577,7 +606,7 @@ class Process_Category:
|
|
577
606
|
product_table = "Products:\n"
|
578
607
|
for product in products:
|
579
608
|
rate = product.get("rate", "NA")
|
580
|
-
quantity = product.get("
|
609
|
+
quantity = product.get("quantity", "NA")
|
581
610
|
try:
|
582
611
|
total = float(rate) * float(quantity)
|
583
612
|
except:
|
@@ -586,14 +615,14 @@ class Process_Category:
|
|
586
615
|
f'- Requested Description: {product.get("requested_description", "-")}, '
|
587
616
|
f'Item Code: {product.get("item_no", "-")}, '
|
588
617
|
f'Item Description: {product.get("description", "-")}, '
|
589
|
-
f'
|
618
|
+
f'Make: {product.get("make", "-")}, '
|
619
|
+
f'Inventory Unit: {product.get("inventory_unit", "-")}, '
|
590
620
|
f'Price: {product.get("price", "-")}, '
|
591
|
-
f'
|
592
|
-
f'Discount: {product.get("Discount", "-")}, '
|
621
|
+
f'Discount: {product.get("discount", "-")}, '
|
593
622
|
f'Rate: {rate}, '
|
594
623
|
f'Quantity: {quantity}, '
|
595
624
|
f'Total: {total}, '
|
596
|
-
f'Price Pickup Source: {product.get("
|
625
|
+
f'Price Pickup Source: {product.get("price_pickup_source", "-")}, '
|
597
626
|
f'Availability: ""\n'
|
598
627
|
)
|
599
628
|
|
@@ -606,12 +635,11 @@ class Process_Category:
|
|
606
635
|
|
607
636
|
{product_table}
|
608
637
|
Ensure the table has the following columns in this exact order:
|
609
|
-
Sr. No., Requested Description, Item Code, Item Description,
|
638
|
+
Sr. No., Requested Description, Item Code, Item Description, Make, Inventory Unit, Price, Discount, Rate, Quantity, Total, Price Pickup Source, Availability
|
610
639
|
|
611
640
|
- If any value is missing, use a dash ("-") instead.
|
612
|
-
- "Total" is calculated as Rate × Quantity.
|
613
641
|
- "Availability" should be a blank column.
|
614
|
-
Original Email Subject: {subject}
|
642
|
+
Original Email Subject: {subject}
|
615
643
|
|
616
644
|
Return only the following JSON String format:
|
617
645
|
{{
|
@@ -976,3 +1004,71 @@ class Process_Category:
|
|
976
1004
|
return payload.decode(part.get_content_charset() or 'utf-8')
|
977
1005
|
return "No HTML content found."
|
978
1006
|
|
1007
|
+
def products_item_code_lookup(self, products, openai_api_key, schemaName_Updated, server_url):
|
1008
|
+
try:
|
1009
|
+
logger.log(f'\nproduct_Json : {products}')
|
1010
|
+
logger.log(f'\nopenai_api_key : {openai_api_key}')
|
1011
|
+
logger.log(f'\nschemaName_Updated : {schemaName_Updated}')
|
1012
|
+
logger.log(f'\nserver_url : {server_url}')
|
1013
|
+
alphaValue = 0.54
|
1014
|
+
|
1015
|
+
client = weaviate.Client(server_url,additional_headers={"X-OpenAI-Api-Key": openai_api_key})
|
1016
|
+
logger.log(f'Connection is establish : {client.is_ready()}')
|
1017
|
+
|
1018
|
+
schemaClasslist = [i['class'] for i in client.schema.get()["classes"]]
|
1019
|
+
logger.log(f'schemaClasslist : {schemaClasslist}')
|
1020
|
+
|
1021
|
+
for product in products:
|
1022
|
+
item_no = product.get("item_no", "NA")
|
1023
|
+
item_name = product.get("requested_description", "NA")
|
1024
|
+
if item_no == "NA":
|
1025
|
+
inputQuery = item_name.upper().replace("N/A","").replace("."," ").replace(","," ").replace("-"," ").replace("_"," ")
|
1026
|
+
logger.log(f'inputQuery : {inputQuery}')
|
1027
|
+
|
1028
|
+
if schemaName_Updated in schemaClasslist:
|
1029
|
+
logger.log(f'Inside schemaClasslist')
|
1030
|
+
response = (
|
1031
|
+
client.query
|
1032
|
+
.get(schemaName_Updated, ["description", "answer","phy_attrib_2","phy_attrib_3","phy_attrib_4"])
|
1033
|
+
.with_hybrid(
|
1034
|
+
alpha = alphaValue,
|
1035
|
+
query = inputQuery.strip() ,
|
1036
|
+
fusion_type = HybridFusion.RELATIVE_SCORE
|
1037
|
+
)
|
1038
|
+
.with_additional('score')
|
1039
|
+
.with_limit(10)
|
1040
|
+
.do()
|
1041
|
+
)
|
1042
|
+
logger.log(f"Input ::: {item_name}")
|
1043
|
+
if response != {}:
|
1044
|
+
response_List = response['data']['Get'][schemaName_Updated]
|
1045
|
+
product['description'] = response_List[0]['description']
|
1046
|
+
product['item_no'] = response_List[0]['answer']
|
1047
|
+
# product['cas_no'] = response_List[0]['phy_attrib_1']
|
1048
|
+
product['make'] = response_List[0]['phy_attrib_2']
|
1049
|
+
product["price"] = response_List[0]['phy_attrib_3']
|
1050
|
+
product["inventory_unit"] = response_List[0]['phy_attrib_4']
|
1051
|
+
|
1052
|
+
for index in range(len(response_List)):
|
1053
|
+
description = response_List[index]['description']
|
1054
|
+
description = description.upper().replace("N/A","").replace("."," ").replace(","," ").replace("-"," ").replace("_"," ")
|
1055
|
+
|
1056
|
+
descr_replaced = description.replace(" ", "")
|
1057
|
+
inputQuery_replaced = inputQuery.replace(" ", "")
|
1058
|
+
|
1059
|
+
if descr_replaced == inputQuery_replaced:
|
1060
|
+
logger.log(f"\n Input::: '{inputQuery_replaced}' MATCHEDD with description ::: '{descr_replaced}' \n")
|
1061
|
+
product['description'] = response_List[index]['description']
|
1062
|
+
product['item_no'] = response_List[index]['answer']
|
1063
|
+
# product['cas_no'] = response_List[index]['phy_attrib_1']
|
1064
|
+
product['make'] = response_List[index]['phy_attrib_2']
|
1065
|
+
product["price"] = response_List[index]['phy_attrib_3']
|
1066
|
+
product["inventory_unit"] = response_List[index]['phy_attrib_4']
|
1067
|
+
break
|
1068
|
+
else:
|
1069
|
+
logger.log(f"\n Input '{inputQuery_replaced}' not matched with returned response description '{descr_replaced}'\n ")
|
1070
|
+
|
1071
|
+
return products
|
1072
|
+
|
1073
|
+
except Exception as error:
|
1074
|
+
raise str(error)
|
{AIEmailAutomationUtility-0.0.33.dist-info → AIEmailAutomationUtility-0.0.35.dist-info}/METADATA
RENAMED
@@ -1,7 +1,7 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: AIEmailAutomationUtility
|
3
|
-
Version: 0.0.
|
4
|
-
Summary:
|
3
|
+
Version: 0.0.35
|
4
|
+
Summary: Aligned numeric cells in the Email Draft table and ensured that the Identify Products function returns a JSON array only.
|
5
5
|
Author: Proteus Technology PVT. LTD.
|
6
6
|
Author-email: <apps@baseinformation.com>
|
7
7
|
Keywords: python,first package
|
@@ -13,4 +13,4 @@ Classifier: Operating System :: MacOS :: MacOS X
|
|
13
13
|
Classifier: Operating System :: Microsoft :: Windows
|
14
14
|
License-File: LICENCE.txt
|
15
15
|
|
16
|
-
|
16
|
+
Aligned numeric cells in the Email Draft table and ensured that the Identify Products function returns a JSON array only.
|
{AIEmailAutomationUtility-0.0.33.dist-info → AIEmailAutomationUtility-0.0.35.dist-info}/RECORD
RENAMED
@@ -1,15 +1,15 @@
|
|
1
1
|
AIEmailAutomationUtility/EmailReplyAssistant.py,sha256=R_wJna3-ITsVxQEccryhM93T_Nf_Oxo8DXnS-sDN8VE,6679
|
2
2
|
AIEmailAutomationUtility/Email_Classification.py,sha256=Ar0g4Ff8HOT7xICktd3nP_C_vCyeY-xCpUjVCVRWAyc,9417
|
3
3
|
AIEmailAutomationUtility/Email_DocumentUploader.py,sha256=BWNRt2X-E2HCogBaKDfl7cZZNSkZUeIsVs8iXjFjH88,3218
|
4
|
-
AIEmailAutomationUtility/Email_Draft.py,sha256=
|
5
|
-
AIEmailAutomationUtility/Email_Read.py,sha256=
|
4
|
+
AIEmailAutomationUtility/Email_Draft.py,sha256=TGLqV0yzXLIdBnRcEY3CgmcDJVgiP3LN1vLPXTBj5rY,8831
|
5
|
+
AIEmailAutomationUtility/Email_Read.py,sha256=_JwHTJZmxjuMPTbn90D9YStW7NSJd3t4R_cwUZ1tn20,31470
|
6
6
|
AIEmailAutomationUtility/Email_Upload_Document.py,sha256=3bdkxfDlwoeRp-46KPw2Gs1dqBhEIoA1yE5GCudpdV8,1320
|
7
|
-
AIEmailAutomationUtility/Process_Category.py,sha256=
|
7
|
+
AIEmailAutomationUtility/Process_Category.py,sha256=RfOdZq4GnHlWVprSrKf_PQrx48QuFhav3CUwnEzOlOg,53585
|
8
8
|
AIEmailAutomationUtility/Save_Draft.py,sha256=yzLgFN14I_lXE6qL0I3tKNduvcnWdbsY9i2mKdTtio4,5348
|
9
9
|
AIEmailAutomationUtility/Save_Transaction.py,sha256=Gg1w6hhzHmEFjsuzYvkq-3-EsWReetjLHsYSv5YIGgM,3816
|
10
10
|
AIEmailAutomationUtility/__init__.py,sha256=Jad3IdPRsVMeLqEEh-FbCrc1lE2tzJO2DTG5Hgmxh5g,391
|
11
|
-
AIEmailAutomationUtility-0.0.
|
12
|
-
AIEmailAutomationUtility-0.0.
|
13
|
-
AIEmailAutomationUtility-0.0.
|
14
|
-
AIEmailAutomationUtility-0.0.
|
15
|
-
AIEmailAutomationUtility-0.0.
|
11
|
+
AIEmailAutomationUtility-0.0.35.dist-info/LICENCE.txt,sha256=2qX9IkEUBx0VJp1Vh9O2dsRwE-IpYId0lXDyn7OVsJ8,1073
|
12
|
+
AIEmailAutomationUtility-0.0.35.dist-info/METADATA,sha256=H3FAgTui1ayJB9Q-opXbEKkgkqv1ORrrD6PXVVDnQEI,743
|
13
|
+
AIEmailAutomationUtility-0.0.35.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
14
|
+
AIEmailAutomationUtility-0.0.35.dist-info/top_level.txt,sha256=3jTWrTUblVkaP7mpwY2UBSnrlfot5Ykpfsehyke-Uzw,25
|
15
|
+
AIEmailAutomationUtility-0.0.35.dist-info/RECORD,,
|
{AIEmailAutomationUtility-0.0.33.dist-info → AIEmailAutomationUtility-0.0.35.dist-info}/LICENCE.txt
RENAMED
File without changes
|
{AIEmailAutomationUtility-0.0.33.dist-info → AIEmailAutomationUtility-0.0.35.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|