Soham commited on
Commit
6648464
·
1 Parent(s): e90709b

created folder strucutre

Browse files
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .venv
2
+ .env
3
+ __pycache__/
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # you will also find guides on how best to write your Dockerfile
3
+
4
+ FROM python:3.9
5
+
6
+ WORKDIR /code
7
+
8
+ COPY ./requirements.txt /code/requirements.txt
9
+
10
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
11
+
12
+ COPY . .
13
+
14
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
ai_model/callAiModel.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain.chains import LLMChain
2
+ from langchain.memory import ConversationBufferMemory
3
+ from langchain.prompts import PromptTemplate
4
+ from langchain_huggingface import HuggingFaceEndpoint
5
+
6
+ # HuggingFace Endpoint Initialization
7
+ repo_id = "mistralai/Mixtral-8x7B-Instruct-v0.1"
8
+ llmModel = HuggingFaceEndpoint(
9
+ repo_id=repo_id,
10
+ max_new_tokens=512,
11
+ temperature=0.5,
12
+ huggingfacehub_api_token="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",
13
+ task="text-generation",
14
+ )
15
+
16
+ # Define the prompt template
17
+ prompt = PromptTemplate(
18
+ input_variables=["logs", "query"],
19
+ template=(
20
+ """ You are an expert log analyzer. Analyze the system logs provided below. Return only precise and concise answers to the questions asked, formatted clearly and without unnecessary elaboration.
21
+ Logs: {logs}
22
+ User's Query: Analyze the logs and answer the following question:{query}
23
+ Be concise and direct in your responses."""
24
+ ),
25
+ )
26
+
27
+ # Memory setup
28
+ memory = ConversationBufferMemory(
29
+ input_key="query",
30
+ memory_key="history",
31
+ return_messages=False,
32
+ )
33
+
34
+ # Create the LLM chain with memory
35
+ conversation_chain = LLMChain(llm=llmModel, prompt=prompt, memory=memory)
36
+
37
+
38
+ def generate_ai_response(user_query,logs):
39
+ try:
40
+ # Run the conversation chain
41
+ response = conversation_chain.run({"logs": logs, "query": user_query})
42
+ return response
43
+ except Exception as e:
44
+ print(e)
45
+ return f"An error occurred: {e}"
46
+
47
+
48
+ # generate_ai_response ("who is prime minister of india")
ai_model/callGeminiModel.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import google.generativeai as genai
2
+ from langchain.prompts import PromptTemplate
3
+
4
+ # Configure Gemini
5
+ api_key = "AIzaSyCWj2sHInMricyC2frSg3uwUgsd_QOGkLA"
6
+ genai.configure(api_key=api_key)
7
+ model = genai.GenerativeModel(model_name="gemini-1.5-flash-latest")
8
+
9
+ # Define the Prompt Template
10
+ log_prompt = PromptTemplate(
11
+ input_variables=["logs", "query"],
12
+ template=(
13
+ """You are an expert log analyzer. Analyze the system logs provided below.
14
+ Return only precise and concise answers to the questions asked, formatted clearly.
15
+
16
+ Logs: {logs}
17
+ User's Query: {query}
18
+
19
+ Be concise and direct in your responses."""
20
+ ),
21
+ )
22
+
23
+
24
+ def generate_ai_response(user_query, logs):
25
+ formatted_prompt = log_prompt.format(logs=logs, query=user_query)
26
+ response = model.generate_content([formatted_prompt])
27
+ return response.text
config/database.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pymongo
2
+ import gridfs
3
+
4
+ # database connection
5
+ client = pymongo.MongoClient(
6
+ "mongodb+srv://soham2000:soham2000@cluster0.lbu4i.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0",
7
+ ssl=True,
8
+ )
9
+ db = client["nodetest"]
10
+ user_collection = db["users"]
11
+ chat_collection = db["chats"]
12
+ log_collection = db["logs"]
13
+ fs = gridfs.GridFS(db) # Initialize GridFS bucket
main.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from routes.route import router
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ app = FastAPI()
5
+
6
+ app.add_middleware(
7
+ CORSMiddleware,
8
+ allow_origins=["*"], # Allow requests from any origin
9
+ allow_credentials=True,
10
+ allow_methods=["GET", "POST", "PUT", "DELETE"],
11
+ allow_headers=["Authorization", "Content-Type"],
12
+ )
13
+
14
+ app.include_router(router)
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.8
2
+ uvicorn==0.34.0
3
+ pydantic==2.10.6
4
+ pymongo==4.11
5
+ passlib[bcrypt]
6
+ python-jose[cryptography]
7
+ python-multipart==0.0.20
8
+ langchain==0.3.17
9
+ langchain-huggingface==0.1.2
10
+ google-generativeai==0.8.4
11
+ huggingface-hub==0.28.1
routes/route.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from fastapi import APIRouter, Body, HTTPException, Depends
3
+ from ai_model.callGeminiModel import generate_ai_response
4
+ from config.database import user_collection, chat_collection, log_collection
5
+ from utils.auth import create_access_token, hash_password, verify_password, verify_token
6
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
7
+ from schema.model import (
8
+ ChatData,
9
+ DateRangeModel,
10
+ QueryModel,
11
+ UserIdModel,
12
+ UserRegister,
13
+ UserLogin,
14
+ )
15
+ from datetime import datetime, timedelta, timezone
16
+ from cryptography.fernet import Fernet
17
+
18
+ security_scheme = HTTPBearer()
19
+ router = APIRouter()
20
+ activation_tokens = {}
21
+ encryption_key = b"kFRvFTtpl6WDr0eceOteS5IAnv3ps0YtCavmUwllO0k="
22
+ cipher = Fernet(encryption_key)
23
+
24
+
25
+ # -------------------------------------------User Collections-------------------------------------------
26
+ @router.post("/api/user/userLogin", tags=["user"])
27
+ async def user_login(userLogin: UserLogin):
28
+ collection = user_collection
29
+ user = collection.find_one({"userName": userLogin.userName})
30
+ if not user or not verify_password(userLogin.password, user.get("password", "")):
31
+ raise HTTPException(status_code=401, detail="Incorrect username or password")
32
+ user_data = {
33
+ "email": user.get("email"),
34
+ "username": user.get("userName"),
35
+ "id": str(user.get("_id")),
36
+ "role": user.get("role"),
37
+ }
38
+ token = create_access_token({"sub": userLogin.userName})
39
+ response_data = {
40
+ "access_token": token,
41
+ "token_type": "bearer",
42
+ "user_data": user_data,
43
+ }
44
+ return response_data
45
+
46
+
47
+ # User Registration Api
48
+ @router.post("/api/user/registerUser", tags=["user"])
49
+ async def create_user(user: UserRegister):
50
+ hashed_password = hash_password(user.password)
51
+ user_dict = user.dict()
52
+ user_dict["password"] = hashed_password
53
+ result = user_collection.insert_one(user_dict)
54
+ return {"message": "User created successfully", "userId": str(result.inserted_id)}
55
+
56
+
57
+ # Api to show the registered users list
58
+ @router.get(
59
+ "/api/user/getAllUsers", tags=["user"], dependencies=[Depends(security_scheme)]
60
+ )
61
+ async def get_all_user(token: HTTPAuthorizationCredentials = Depends(security_scheme)):
62
+ verify_token(token.credentials)
63
+ users = user_collection.find({})
64
+ user_list = []
65
+ for user in users:
66
+ user["_id"] = str(user["_id"]) # Convert ObjectId to string
67
+ user_list.append(user)
68
+ return user_list
69
+
70
+
71
+ # -------------------------------------------Model Collections-------------------------------------------
72
+
73
+
74
+ # Api to create new chat
75
+ @router.post(
76
+ "/api/model/callModel", tags=["model"], dependencies=[Depends(security_scheme)]
77
+ )
78
+ async def call_ai_model(
79
+ query: QueryModel,
80
+ token: HTTPAuthorizationCredentials = Depends(security_scheme),
81
+ ):
82
+ verify_token(token.credentials)
83
+ answer = generate_ai_response(query.query, query.logs)
84
+ return {"answer": answer}
85
+
86
+
87
+ # Api to show the registered users list
88
+ @router.post(
89
+ "/api/model/getAllChatHistoryByTitleId",
90
+ tags=["model"],
91
+ dependencies=[Depends(security_scheme)],
92
+ )
93
+ async def get_all_chat_history(
94
+ titleUId: str = Body(...),
95
+ userID: str = Body(...),
96
+ token: HTTPAuthorizationCredentials = Depends(security_scheme),
97
+ ):
98
+ verify_token(token.credentials)
99
+ if not titleUId or not userID:
100
+ raise HTTPException(
101
+ status_code=400, detail="Both titleUId and userID are required"
102
+ )
103
+
104
+ # Find matching document from MongoDB
105
+ result = chat_collection.find_one({"titleUId": titleUId, "userID": userID})
106
+ if not result:
107
+ raise HTTPException(status_code=404, detail="No chat history found")
108
+
109
+ # Convert ObjectId and prepare response
110
+ result["_id"] = str(result["_id"])
111
+ result.pop("_id", None) # Remove _id if not needed
112
+
113
+ # Ensure chat format consistency
114
+ result.setdefault("chat", [])
115
+ result["chat"] = [
116
+ {"question": entry.get("question", ""), "answer": entry.get("answer", "")}
117
+ for entry in result.get("chat", [])
118
+ ]
119
+
120
+ return result
121
+
122
+
123
+ @router.post(
124
+ "/api/model/addUpdateChatHistory",
125
+ tags=["model"],
126
+ dependencies=[Depends(security_scheme)],
127
+ )
128
+ async def add_or_update_chat(
129
+ data: ChatData,
130
+ token: HTTPAuthorizationCredentials = Depends(security_scheme),
131
+ ):
132
+ verify_token(token.credentials)
133
+ existing_entry = chat_collection.find_one(
134
+ {"userID": data.userID, "titleUId": data.titleUId}
135
+ )
136
+
137
+ if existing_entry:
138
+ # Extract existing chat questions to avoid duplication
139
+ existing_questions = {
140
+ entry["question"] for entry in existing_entry.get("chat", [])
141
+ }
142
+ new_entries = [
143
+ entry.dict()
144
+ for entry in data.chat
145
+ if entry.question not in existing_questions
146
+ ]
147
+
148
+ if new_entries:
149
+ chat_collection.update_one(
150
+ {"_id": existing_entry["_id"]},
151
+ {"$push": {"chat": {"$each": new_entries}}},
152
+ )
153
+ return {"message": "Chat updated successfully."}
154
+ else:
155
+ return {"message": "No new chat entries to add."}
156
+ else:
157
+ # Insert new document
158
+ new_data = data.dict()
159
+ chat_collection.insert_one(new_data)
160
+ return {"message": "Chat added successfully."}
161
+
162
+
163
+
164
+ @router.post(
165
+ "/api/model/getAllTitleByUserId",
166
+ tags=["model"],
167
+ dependencies=[Depends(security_scheme)],
168
+ )
169
+ async def get_all_title(
170
+ data: UserIdModel,
171
+ token: HTTPAuthorizationCredentials = Depends(security_scheme),
172
+ ):
173
+ verify_token(token.credentials)
174
+ chats = chat_collection.find({"userID": data.userID})
175
+ chatList = []
176
+ for chat in chats:
177
+ chatObj = {
178
+ "titleUId": chat["titleUId"],
179
+ "titleName": chat["titleName"],
180
+ } # Convert ObjectId to string
181
+ chatList.append(chatObj)
182
+ chatList.reverse()
183
+ return chatList
184
+
185
+
186
+
187
+ @router.post(
188
+ "/api/model/getAllLogsByDateRange",
189
+ tags=["model"],
190
+ dependencies=[Depends(security_scheme)],
191
+ )
192
+ async def get_logs_by_daterange(
193
+ data: DateRangeModel,
194
+ token: HTTPAuthorizationCredentials = Depends(security_scheme),
195
+ ):
196
+ verify_token(token.credentials)
197
+ current_date = datetime.now(timezone.utc) # Ensure timezone awareness
198
+
199
+ # Determine start_date based on duration format
200
+ if " - " in data.duration: # Handle custom date range
201
+ try:
202
+ start_date_str, end_date_str = map(str.strip, data.duration.split(" - "))
203
+ start_date = datetime.fromisoformat(start_date_str)
204
+ end_date = datetime.fromisoformat(end_date_str)
205
+ except ValueError:
206
+ raise ValueError("Invalid date format. Use ISO 8601 format: YYYY-MM-DDTHH:MM:SS.ssssss+00:00 - YYYY-MM-DDTHH:MM:SS.ssssss+00:00")
207
+ else: # Handle predefined durations
208
+ if data.duration == "1 Day":
209
+ start_date = current_date - timedelta(days=1)
210
+ elif data.duration == "Current Week":
211
+ start_date = current_date - timedelta(weeks=1)
212
+ elif data.duration == "Last 2 Weeks":
213
+ start_date = current_date - timedelta(weeks=2)
214
+ else:
215
+ raise ValueError("Invalid duration selected")
216
+ end_date = current_date
217
+
218
+ # Ensure timestamps are in UTC
219
+ start_date = start_date.astimezone(timezone.utc)
220
+ end_date = end_date.astimezone(timezone.utc)
221
+
222
+ duration_str = f"{start_date.isoformat()} - {end_date.isoformat()}"
223
+
224
+ logs_list = []
225
+ for encrypted_record in log_collection.find():
226
+ encrypted_data = encrypted_record["encrypted_data"]
227
+ decrypted_data = cipher.decrypt(encrypted_data).decode()
228
+ log = json.loads(decrypted_data)
229
+ log_timestamp = datetime.fromisoformat(log["Timestamp"]).astimezone(timezone.utc)
230
+
231
+ if start_date <= log_timestamp <= end_date:
232
+ logs_list.append(log)
233
+
234
+ return {"duration": duration_str, "logList": logs_list}
235
+
236
+
237
+ @router.delete(
238
+ "/api/model/deleteChatHistoryByTitleId",
239
+ tags=["model"],
240
+ dependencies=[Depends(security_scheme)],
241
+ )
242
+ async def delete_chat_history(
243
+ titleUId: str = Body(...),
244
+ userID: str = Body(...),
245
+ token: HTTPAuthorizationCredentials = Depends(security_scheme),
246
+ ):
247
+ verify_token(token.credentials)
248
+ if not titleUId or not userID:
249
+ raise HTTPException(
250
+ status_code=400, detail="Both titleUId and userID are required"
251
+ )
252
+
253
+ # Attempt to delete the document
254
+ delete_result = chat_collection.delete_one({"titleUId": titleUId, "userID": userID})
255
+
256
+ if delete_result.deleted_count == 0:
257
+ raise HTTPException(status_code=404, detail="No chat history found to delete")
258
+
259
+ return {"message": "Chat history deleted successfully"}
schema/model.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import List
3
+
4
+
5
+ # user registration parameters
6
+ class UserRegister(BaseModel):
7
+ name: str
8
+ userName: str
9
+ email: str
10
+ password: str
11
+
12
+
13
+ # user login parameters
14
+ class UserLogin(BaseModel):
15
+ userName: str
16
+ password: str
17
+
18
+
19
+ class ChatEntry(BaseModel):
20
+ question: str
21
+ answer: str
22
+
23
+
24
+ class ChatData(BaseModel):
25
+ userID: str
26
+ titleName: str
27
+ titleUId: str
28
+ dateRange: str
29
+ chat: List[ChatEntry]
30
+
31
+
32
+ class UserIdModel(BaseModel):
33
+ userID: str
34
+
35
+
36
+ class QueryModel(BaseModel):
37
+ query: str
38
+ logs: str
39
+
40
+
41
+ class DateRangeModel(BaseModel):
42
+ duration: str
utils/auth.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import HTTPException, status
2
+ from fastapi.security import HTTPBearer
3
+ from datetime import datetime, timedelta
4
+ from jose import JWTError, jwt
5
+ import bcrypt
6
+
7
+ security = HTTPBearer()
8
+
9
+ # Secret key for JWT token
10
+ SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
11
+ ALGORITHM = "HS256"
12
+ ACCESS_TOKEN_EXPIRE_MINUTES = 180
13
+
14
+ # Function to generate JWT token
15
+ def create_access_token(data: dict):
16
+ to_encode = data.copy()
17
+ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
18
+ to_encode.update({"exp": expire})
19
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
20
+ return encoded_jwt
21
+
22
+ # Function to hash a password
23
+ def hash_password(password: str) -> str:
24
+ hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
25
+ return hashed_password
26
+
27
+ # Function to verify a password
28
+ def verify_password(plain_password: str, hashed_password: bytes) -> bool:
29
+ return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password)
30
+
31
+
32
+ def verify_token(token: str):
33
+ try:
34
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
35
+ return payload
36
+ except JWTError:
37
+ raise HTTPException(
38
+ status_code=status.HTTP_401_UNAUTHORIZED,
39
+ detail="Invalid or expired token"
40
+ )
utils/download_mongodb_logs.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pymongo import MongoClient
2
+ from cryptography.fernet import Fernet
3
+ import json
4
+ from datetime import datetime, timedelta
5
+
6
+ # MongoDB connection string
7
+ connection_string = "mongodb+srv://soham2000:soham2000@cluster0.lbu4i.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0"
8
+
9
+ # Encryption key (generate this once and store securely)
10
+ encryption_key = b"kFRvFTtpl6WDr0eceOteS5IAnv3ps0YtCavmUwllO0k="
11
+ cipher = Fernet(encryption_key)
12
+
13
+ client = MongoClient(connection_string)
14
+ db = client["nodetest"]
15
+ collection = db["logs"]
16
+ logs_list = []
17
+ # Calculate the date range based on the selected duration
18
+ def download_logs(duration):
19
+ # Get the current date
20
+ current_date = datetime.now()
21
+
22
+ # Calculate the start and end date based on the selected duration
23
+ if duration == "1 day":
24
+ start_date = current_date - timedelta(days=1)
25
+ end_date = current_date
26
+ elif duration == "1 week":
27
+ start_date = current_date - timedelta(weeks=1)
28
+ end_date = current_date
29
+ elif duration == "2 weeks":
30
+ start_date = current_date - timedelta(weeks=2)
31
+ end_date = current_date
32
+ else:
33
+ raise ValueError("Invalid duration selected")
34
+
35
+ # Decrypt and filter logs based on the calculated date range
36
+ for encrypted_record in collection.find():
37
+ encrypted_data = encrypted_record["encrypted_data"]
38
+ decrypted_data = cipher.decrypt(encrypted_data).decode()
39
+ log = json.loads(decrypted_data)
40
+
41
+ # Parse the Timestamp field into a datetime object
42
+ log_timestamp = datetime.fromisoformat(log["Timestamp"])
43
+
44
+ # Check if the log falls within the specified date range
45
+ if start_date <= log_timestamp <= end_date:
46
+ logs_list.append(log)
47
+
48
+ return logs_list
49
+
50
+
51
+ download_logs("1 week")
utils/email_validator.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from email.mime.text import MIMEText
2
+ from email.mime.multipart import MIMEMultipart
3
+ import smtplib
4
+
5
+ # Function to send activation email with HTML content
6
+ def send_activation_email(email: str, activation_otp: int):
7
+ sender_email = "chandratresoham@gmail.com" # Update with your email address
8
+ sender_password = "wbuc okcv hzzn iwyx" # Update with your email password
9
+ # Update with your website URL
10
+
11
+ # HTML content for the email body
12
+ email_body = f"""
13
+ <html>
14
+ <body>
15
+ <p>
16
+ Hello,<br><br>
17
+ You have successfully registered to the system.<br><br>
18
+ Please enter below otp to verify your accout<br><br>
19
+ <p style="font-size:17px; font-weight:bold">{activation_otp}</p><br><br>
20
+ Thank you!<br>
21
+ </p>
22
+ </body>
23
+ </html>
24
+ """
25
+
26
+ # Create MIMEText object with HTML content
27
+ message = MIMEMultipart("alternative")
28
+ message['From'] = sender_email
29
+ message['To'] = email
30
+ message['Subject'] = "Activate your account"
31
+ message.attach(MIMEText(email_body, 'html'))
32
+
33
+ # Connect to SMTP server and send email
34
+ with smtplib.SMTP('smtp.gmail.com', 587) as server: # Update with your SMTP server details
35
+ server.starttls()
36
+ server.login(sender_email, sender_password)
37
+ server.sendmail(sender_email, email, message.as_string())