ak0601 commited on
Commit
c98daca
·
verified ·
1 Parent(s): d31a44a

Update app_hug.py

Browse files
Files changed (1) hide show
  1. app_hug.py +489 -487
app_hug.py CHANGED
@@ -1,487 +1,489 @@
1
- from fastapi import FastAPI, HTTPException, BackgroundTasks
2
- from pydantic import BaseModel, EmailStr, Field
3
- from typing import Optional, Tuple
4
- from enum import Enum
5
- import os
6
- import base64
7
- import pickle
8
- import pandas as pd
9
- from dotenv import load_dotenv
10
- from langchain_openai import ChatOpenAI
11
- from langchain.schema import HumanMessage, SystemMessage
12
- from email.mime.text import MIMEText
13
- from google.auth.transport.requests import Request
14
- from google.oauth2.credentials import Credentials
15
- from google_auth_oauthlib.flow import InstalledAppFlow
16
- from googleapiclient.discovery import build
17
- from googleapiclient.errors import HttpError
18
- from datetime import datetime
19
- import json
20
-
21
- # Load environment variables (not needed on Hugging Face, but harmless)
22
- load_dotenv()
23
-
24
- # ------------------------------------------
25
- # Helper: Write GOOGLE_CREDENTIALS_JSON to file if needed
26
- # ------------------------------------------
27
- def ensure_credentials_file():
28
- credentials_env = os.getenv("GOOGLE_CREDENTIALS_JSON")
29
- credentials_path = "credentials_SYNAPSE.json"
30
- if not os.path.exists(credentials_path):
31
- if not credentials_env:
32
- raise Exception("GOOGLE_CREDENTIALS_JSON not found in environment variables.")
33
- try:
34
- parsed_json = json.loads(credentials_env)
35
- except json.JSONDecodeError:
36
- raise Exception("Invalid JSON in GOOGLE_CREDENTIALS_JSON")
37
- with open(credentials_path, "w") as f:
38
- json.dump(parsed_json, f, indent=2)
39
- return credentials_path
40
-
41
- # ------------------------------------------
42
- # FastAPI app
43
- # ------------------------------------------
44
- app = FastAPI(title="Recruitment Message Generator API", version="1.0.0")
45
-
46
- SCOPES = ["https://www.googleapis.com/auth/gmail.send"]
47
- openai_api_key = os.getenv("OPENAI_API_KEY")
48
-
49
- # ------------------------------------------
50
- # Enums and Models
51
- # ------------------------------------------
52
- class MessageType(str, Enum):
53
- OUTREACH = "outreach"
54
- INTRODUCTORY = "introductory"
55
- FOLLOWUP = "followup"
56
-
57
- class GenerateMessageRequest(BaseModel):
58
- job_evaluation: str
59
- sender_email: EmailStr
60
- recruiter_email: Optional[EmailStr] = None
61
- recipient_email: EmailStr
62
- candidate_name: str
63
- current_role: str
64
- current_company: str
65
- company_name: str
66
- role: str
67
- recruiter_name: str
68
- organisation: str
69
- message_type: MessageType
70
- send_email: bool = False
71
-
72
- class FeedbackRequest(BaseModel):
73
- message: str
74
- feedback: str
75
-
76
- class AuthenticateRequest(BaseModel):
77
- email: EmailStr
78
-
79
- class AuthenticateResponse(BaseModel):
80
- success: bool
81
- message: str
82
- error: Optional[str] = None
83
-
84
- class MessageResponse(BaseModel):
85
- success: bool
86
- message: str
87
- email_sent: bool = False
88
- email_subject: Optional[str] = None
89
- error: Optional[str] = None
90
-
91
- # ------------------------------------------
92
- # Gmail Helper Functions
93
- # ------------------------------------------
94
- def get_token_file_path(email: str) -> str:
95
- tokens_dir = "gmail_tokens"
96
- if not os.path.exists(tokens_dir):
97
- os.makedirs(tokens_dir)
98
- safe_email = email.replace("@", "_at_").replace(".", "_dot_")
99
- return os.path.join(tokens_dir, f"token_{safe_email}.pickle")
100
-
101
- def check_user_token_exists(email: str) -> bool:
102
- token_file = get_token_file_path(email)
103
- return os.path.exists(token_file)
104
-
105
- def load_user_credentials(email: str):
106
- token_file = get_token_file_path(email)
107
- if os.path.exists(token_file):
108
- try:
109
- with open(token_file, 'rb') as token:
110
- creds = pickle.load(token)
111
- return creds
112
- except Exception:
113
- if os.path.exists(token_file):
114
- os.remove(token_file)
115
- return None
116
-
117
- def save_user_credentials(email: str, creds):
118
- token_file = get_token_file_path(email)
119
- with open(token_file, 'wb') as token:
120
- pickle.dump(creds, token)
121
-
122
- def create_new_credentials(email: str):
123
- credentials_path = ensure_credentials_file()
124
- flow = InstalledAppFlow.from_client_secrets_file(
125
- credentials_path, SCOPES
126
- )
127
- creds = flow.run_local_server(port=0)
128
- save_user_credentials(email, creds)
129
- return creds
130
-
131
- def authenticate_gmail(email: str, create_if_missing: bool = False):
132
- creds = load_user_credentials(email)
133
- if creds:
134
- if creds.expired and creds.refresh_token:
135
- try:
136
- creds.refresh(Request())
137
- save_user_credentials(email, creds)
138
- except Exception:
139
- if create_if_missing:
140
- try:
141
- creds = create_new_credentials(email)
142
- except:
143
- return None
144
- else:
145
- return None
146
- elif not creds.valid:
147
- creds = None
148
- if not creds:
149
- if create_if_missing:
150
- try:
151
- creds = create_new_credentials(email)
152
- except:
153
- return None
154
- else:
155
- return None
156
- try:
157
- service = build("gmail", "v1", credentials=creds)
158
- return service
159
- except Exception:
160
- return None
161
-
162
- def create_email_message(sender: str, to: str, subject: str, message_text: str, reply_to: Optional[str] = None):
163
- message = MIMEText(message_text)
164
- message["to"] = to
165
- message["from"] = sender
166
- message["subject"] = subject
167
- if reply_to:
168
- message["reply-to"] = reply_to
169
- raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
170
- return {"raw": raw_message}
171
-
172
- def send_gmail_message(service, user_id: str, message: dict):
173
- try:
174
- result = service.users().messages().send(userId=user_id, body=message).execute()
175
- return result is not None
176
- except HttpError:
177
- return False
178
-
179
- # ------------------------------------------
180
- # LLM (OpenAI) Message Generation Helpers
181
- # ------------------------------------------
182
- def refine_message_with_feedback(
183
- original_message: str,
184
- feedback: str,
185
- ) -> Tuple[str, str]:
186
- api_key = os.getenv("OPENAI_API_KEY")
187
- llm = ChatOpenAI(
188
- model="gpt-4o-mini",
189
- temperature=0.7,
190
- max_tokens=600,
191
- openai_api_key=api_key
192
- )
193
- prompt = f"""
194
- Please refine the following recruitment message based on the provided feedback:
195
-
196
- ORIGINAL MESSAGE:
197
- {original_message}
198
-
199
- FEEDBACK:
200
- {feedback}
201
-
202
- Please provide your response in the following format:
203
- SUBJECT: [Your subject line here]
204
-
205
- BODY:
206
- [Your refined email body content here]
207
- Keep the same tone and intent as the original message, but incorporate the feedback to improve it.
208
- """
209
- try:
210
- messages = [
211
- SystemMessage(content="You are a professional recruitment message writer. Refine the given message based on feedback while maintaining professionalism and the original intent."),
212
- HumanMessage(content=prompt)
213
- ]
214
- response = llm.invoke(messages)
215
- content = response.content.strip()
216
- subject_line = ""
217
- body_content = ""
218
- lines = content.split('\n')
219
- body_found = False
220
- body_lines = []
221
- for line in lines:
222
- if line.strip().startswith('SUBJECT:'):
223
- subject_line = line.replace('SUBJECT:', '').strip()
224
- elif line.strip().startswith('BODY:'):
225
- body_found = True
226
- elif body_found and line.strip():
227
- body_lines.append(line)
228
- body_content = '\n'.join(body_lines).strip()
229
- if not subject_line:
230
- subject_line = "Recruitment Opportunity - Updated"
231
- if not body_content:
232
- body_content = content
233
- return subject_line, body_content
234
- except Exception as e:
235
- raise HTTPException(status_code=500, detail=f"Error refining message: {str(e)}")
236
-
237
- def generate_recruitment_message_with_subject(
238
- msg_type: str,
239
- company: str,
240
- role_title: str,
241
- recruiter: str,
242
- org: str,
243
- candidate: str,
244
- current_pos: str,
245
- evaluation: str,
246
- feedback: Optional[str] = None
247
- ) -> Tuple[str, str]:
248
- api_key = os.getenv("OPENAI_API_KEY")
249
- llm = ChatOpenAI(
250
- model="gpt-4o-mini",
251
- temperature=0.7,
252
- max_tokens=600,
253
- openai_api_key=api_key
254
- )
255
- base_prompt = f"""
256
- Generate a professional recruitment {msg_type} with the following details:
257
- - Company hiring: {company}
258
- - Role: {role_title}
259
- - Recruiter: {recruiter} from {org}
260
- - Candidate: {candidate}
261
- - Candidate's current position: {current_pos}
262
- - Evaluation: {evaluation}
263
- """
264
- if msg_type == "outreach":
265
- prompt = base_prompt + """
266
- Create an initial outreach message that:
267
- - Introduces the recruiter and organization
268
- - Mentions the specific role and company
269
- - Expresses interest in discussing the opportunity
270
- - Keeps it short and to the point.
271
- """
272
- elif msg_type == "introductory":
273
- prompt = base_prompt + """
274
- Create an introductory message that:
275
- - Thanks the candidate for their initial response
276
- - Provides more details about the role and company
277
- - Explains why this opportunity aligns with their background
278
- - Suggests next steps (like a call or meeting)
279
- - Maintains a warm, professional tone
280
- """
281
- else: # followup
282
- prompt = base_prompt + """
283
- Create a follow-up message that:
284
- - References previous communication
285
- - Reiterates interest in the candidate
286
- - Addresses any potential concerns
287
- - Provides additional compelling reasons to consider the role
288
- - Includes a clear call to action
289
- """
290
- if feedback:
291
- prompt += f"\n\nPlease modify the message based on this feedback: {feedback}"
292
- prompt += """
293
-
294
- Please provide your response in the following format:
295
- SUBJECT: [Your subject line here]
296
-
297
- BODY:
298
- [Your email body content here]
299
- """
300
- try:
301
- messages = [
302
- SystemMessage(content="You are a professional recruitment message writer. Generate both an email subject line and body content. Follow the exact format requested."),
303
- HumanMessage(content=prompt)
304
- ]
305
- response = llm.invoke(messages)
306
- content = response.content.strip()
307
- subject_line = ""
308
- body_content = ""
309
- lines = content.split('\n')
310
- body_found = False
311
- body_lines = []
312
- for line in lines:
313
- if line.strip().startswith('SUBJECT:'):
314
- subject_line = line.replace('SUBJECT:', '').strip()
315
- elif line.strip().startswith('BODY:'):
316
- body_found = True
317
- elif body_found and line.strip():
318
- body_lines.append(line)
319
- body_content = '\n'.join(body_lines).strip()
320
- if not subject_line:
321
- subject_line = f"Opportunity at {company} - {role_title}"
322
- if not body_content:
323
- body_content = content
324
- return subject_line, body_content
325
- except Exception as e:
326
- raise HTTPException(status_code=500, detail=f"Error generating message: {str(e)}")
327
-
328
- # ------------------------------------------
329
- # FastAPI Endpoints
330
- # ------------------------------------------
331
- @app.get("/")
332
- async def root():
333
- return {
334
- "message": "Recruitment Message Generator API",
335
- "version": "1.0.0",
336
- "endpoints": [
337
- "/generate-message",
338
- "/refine-message",
339
- "/authenticate",
340
- "/docs"
341
- ]
342
- }
343
-
344
- @app.post("/generate-message", response_model=MessageResponse)
345
- async def generate_message(request: GenerateMessageRequest, background_tasks: BackgroundTasks):
346
- try:
347
- current_position = f"{request.current_role} at {request.current_company}"
348
- email_subject, generated_message = generate_recruitment_message_with_subject(
349
- msg_type=request.message_type.value.replace('followup', 'follow-up'),
350
- company=request.company_name,
351
- role_title=request.role,
352
- recruiter=request.recruiter_name,
353
- org=request.organisation,
354
- candidate=request.candidate_name,
355
- current_pos=current_position,
356
- evaluation=request.job_evaluation
357
- )
358
- email_sent = False
359
- if request.send_email:
360
- registered_users = []
361
- if os.path.exists("registered_users.csv"):
362
- df = pd.read_csv("registered_users.csv")
363
- registered_users = df['email'].tolist() if 'email' in df.columns else []
364
- if request.sender_email.lower() not in [user.lower() for user in registered_users]:
365
- return MessageResponse(
366
- success=True,
367
- message=generated_message,
368
- email_sent=False,
369
- email_subject=email_subject,
370
- error="Email not sent: Sender email is not registered"
371
- )
372
- service = authenticate_gmail(request.sender_email)
373
- if service:
374
- email_message = create_email_message(
375
- sender=request.sender_email,
376
- to=request.recipient_email,
377
- subject=email_subject,
378
- message_text=generated_message,
379
- reply_to=request.recruiter_email
380
- )
381
- email_sent = send_gmail_message(service, "me", email_message)
382
- if not email_sent:
383
- return MessageResponse(
384
- success=True,
385
- message=generated_message,
386
- email_sent=False,
387
- email_subject=email_subject,
388
- error="Email not sent: Failed to send via Gmail API"
389
- )
390
- else:
391
- return MessageResponse(
392
- success=True,
393
- message=generated_message,
394
- email_sent=False,
395
- email_subject=email_subject,
396
- error="Email not sent: Gmail authentication failed"
397
- )
398
- return MessageResponse(
399
- success=True,
400
- message=generated_message,
401
- email_sent=email_sent,
402
- email_subject=email_subject
403
- )
404
- except Exception as e:
405
- return MessageResponse(
406
- success=False,
407
- message="",
408
- error=str(e)
409
- )
410
-
411
- @app.post("/refine-message", response_model=MessageResponse)
412
- async def refine_message(request: FeedbackRequest):
413
- try:
414
- email_subject, refined_message = refine_message_with_feedback(
415
- original_message=request.message,
416
- feedback=request.feedback
417
- )
418
- return MessageResponse(
419
- success=True,
420
- message=refined_message,
421
- email_sent=False,
422
- email_subject=email_subject
423
- )
424
- except Exception as e:
425
- return MessageResponse(
426
- success=False,
427
- message="",
428
- error=str(e)
429
- )
430
-
431
- @app.post("/authenticate", response_model=AuthenticateResponse)
432
- async def authenticate_user(request: AuthenticateRequest):
433
- try:
434
- if check_user_token_exists(request.email):
435
- service = authenticate_gmail(request.email, create_if_missing=False)
436
- if service:
437
- return AuthenticateResponse(
438
- success=True,
439
- message="User already authenticated and token is valid"
440
- )
441
- else:
442
- service = authenticate_gmail(request.email, create_if_missing=True)
443
- if service:
444
- return AuthenticateResponse(
445
- success=True,
446
- message="Token refreshed successfully"
447
- )
448
- else:
449
- return AuthenticateResponse(
450
- success=False,
451
- message="Failed to refresh token",
452
- error="Could not refresh existing token. Please check credentials.json"
453
- )
454
- else:
455
- try:
456
- creds = create_new_credentials(request.email)
457
- if creds:
458
- return AuthenticateResponse(
459
- success=True,
460
- message="Authentication successful. Token created and saved."
461
- )
462
- else:
463
- return AuthenticateResponse(
464
- success=False,
465
- message="Authentication failed",
466
- error="Failed to create credentials"
467
- )
468
- except Exception as e:
469
- return AuthenticateResponse(
470
- success=False,
471
- message="Authentication failed",
472
- error=str(e)
473
- )
474
- except Exception as e:
475
- return AuthenticateResponse(
476
- success=False,
477
- message="Authentication error",
478
- error=str(e)
479
- )
480
-
481
- @app.get("/health")
482
- async def health_check():
483
- return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}
484
-
485
- if __name__ == "__main__":
486
- import uvicorn
487
- uvicorn.run(app, host="0.0.0.0", port=8000)
 
 
 
1
+ from fastapi import FastAPI, HTTPException, BackgroundTasks
2
+ from pydantic import BaseModel, EmailStr, Field
3
+ from typing import Optional, Tuple
4
+ from enum import Enum
5
+ import os
6
+ import base64
7
+ import pickle
8
+ import pandas as pd
9
+ from dotenv import load_dotenv
10
+ from langchain_openai import ChatOpenAI
11
+ from langchain.schema import HumanMessage, SystemMessage
12
+ from email.mime.text import MIMEText
13
+ from google.auth.transport.requests import Request
14
+ from google.oauth2.credentials import Credentials
15
+ from google_auth_oauthlib.flow import InstalledAppFlow
16
+ from googleapiclient.discovery import build
17
+ from googleapiclient.errors import HttpError
18
+ from datetime import datetime
19
+ import json
20
+
21
+ # Load environment variables (not needed on Hugging Face, but harmless)
22
+ load_dotenv()
23
+
24
+ # ------------------------------------------
25
+ # Helper: Write GOOGLE_CREDENTIALS_JSON to file if needed
26
+ # ------------------------------------------
27
+ def ensure_credentials_file():
28
+ credentials_env = os.getenv("GOOGLE_CREDENTIALS_JSON")
29
+ credentials_path = "credentials_SYNAPSE.json"
30
+ if not os.path.exists(credentials_path):
31
+ if not credentials_env:
32
+ raise Exception("GOOGLE_CREDENTIALS_JSON not found in environment variables.")
33
+ try:
34
+ parsed_json = json.loads(credentials_env)
35
+ except json.JSONDecodeError:
36
+ raise Exception("Invalid JSON in GOOGLE_CREDENTIALS_JSON")
37
+ with open(credentials_path, "w") as f:
38
+ json.dump(parsed_json, f, indent=2)
39
+ return credentials_path
40
+
41
+ # ------------------------------------------
42
+ # FastAPI app
43
+ # ------------------------------------------
44
+ app = FastAPI(title="Recruitment Message Generator API", version="1.0.0")
45
+
46
+ SCOPES = ["https://www.googleapis.com/auth/gmail.send"]
47
+ openai_api_key = os.getenv("OPENAI_API_KEY")
48
+
49
+ # ------------------------------------------
50
+ # Enums and Models
51
+ # ------------------------------------------
52
+ class MessageType(str, Enum):
53
+ OUTREACH = "outreach"
54
+ INTRODUCTORY = "introductory"
55
+ FOLLOWUP = "followup"
56
+
57
+ class GenerateMessageRequest(BaseModel):
58
+ job_evaluation: str
59
+ sender_email: EmailStr
60
+ reply_to_email: Optional[EmailStr] = Field(None, description="Recruiter's email for reply-to header")
61
+ recipient_email: EmailStr
62
+ candidate_name: str
63
+ current_role: str
64
+ current_company: str
65
+ company_name: str
66
+ role: str
67
+ recruiter_name: str
68
+ organisation: str
69
+ message_type: MessageType
70
+ send_email: bool = False
71
+
72
+ class FeedbackRequest(BaseModel):
73
+ message: str
74
+ feedback: str
75
+
76
+ class AuthenticateRequest(BaseModel):
77
+ email: EmailStr
78
+
79
+ class AuthenticateResponse(BaseModel):
80
+ success: bool
81
+ message: str
82
+ error: Optional[str] = None
83
+
84
+ class MessageResponse(BaseModel):
85
+ success: bool
86
+ message: str
87
+ email_sent: bool = False
88
+ email_subject: Optional[str] = None
89
+ error: Optional[str] = None
90
+
91
+ # ------------------------------------------
92
+ # Gmail Helper Functions
93
+ # ------------------------------------------
94
+ def get_token_file_path(email: str) -> str:
95
+ tokens_dir = "gmail_tokens"
96
+ if not os.path.exists(tokens_dir):
97
+ os.makedirs(tokens_dir)
98
+ safe_email = email.replace("@", "_at_").replace(".", "_dot_")
99
+ return os.path.join(tokens_dir, f"token_{safe_email}.pickle")
100
+
101
+ def check_user_token_exists(email: str) -> bool:
102
+ token_file = get_token_file_path(email)
103
+ return os.path.exists(token_file)
104
+
105
+ def load_user_credentials(email: str):
106
+ token_file = get_token_file_path(email)
107
+ if os.path.exists(token_file):
108
+ try:
109
+ with open(token_file, 'rb') as token:
110
+ creds = pickle.load(token)
111
+ return creds
112
+ except Exception:
113
+ if os.path.exists(token_file):
114
+ os.remove(token_file)
115
+ return None
116
+
117
+ def save_user_credentials(email: str, creds):
118
+ token_file = get_token_file_path(email)
119
+ with open(token_file, 'wb') as token:
120
+ pickle.dump(creds, token)
121
+
122
+ def create_new_credentials(email: str):
123
+ credentials_path = ensure_credentials_file()
124
+ flow = InstalledAppFlow.from_client_secrets_file(
125
+ credentials_path, SCOPES
126
+ )
127
+ creds = flow.run_local_server(port=0)
128
+ save_user_credentials(email, creds)
129
+ return creds
130
+
131
+ def authenticate_gmail(email: str, create_if_missing: bool = False):
132
+ creds = load_user_credentials(email)
133
+ if creds:
134
+ if creds.expired and creds.refresh_token:
135
+ try:
136
+ creds.refresh(Request())
137
+ save_user_credentials(email, creds)
138
+ except Exception:
139
+ if create_if_missing:
140
+ try:
141
+ creds = create_new_credentials(email)
142
+ except:
143
+ return None
144
+ else:
145
+ return None
146
+ elif not creds.valid:
147
+ creds = None
148
+ if not creds:
149
+ if create_if_missing:
150
+ try:
151
+ creds = create_new_credentials(email)
152
+ except:
153
+ return None
154
+ else:
155
+ return None
156
+ try:
157
+ service = build("gmail", "v1", credentials=creds)
158
+ return service
159
+ except Exception:
160
+ return None
161
+
162
+ def create_email_message(sender: str, to: str, subject: str, message_text: str, reply_to: Optional[str] = None):
163
+ message = MIMEText(message_text)
164
+ message["to"] = to
165
+ message["from"] = sender
166
+ message["subject"] = subject
167
+ if reply_to:
168
+ message["reply-to"] = reply_to
169
+ message["Cc"] = reply_to
170
+ raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
171
+ return {"raw": raw_message}
172
+
173
+ def send_gmail_message(service, user_id: str, message: dict):
174
+ try:
175
+ result = service.users().messages().send(userId=user_id, body=message).execute()
176
+ return result is not None
177
+ except HttpError:
178
+ return False
179
+
180
+ # ------------------------------------------
181
+ # LLM (OpenAI) Message Generation Helpers
182
+ # ------------------------------------------
183
+ def refine_message_with_feedback(
184
+ original_message: str,
185
+ feedback: str,
186
+ ) -> Tuple[str, str]:
187
+ api_key = os.getenv("OPENAI_API_KEY")
188
+ llm = ChatOpenAI(
189
+ model="gpt-4o-mini",
190
+ temperature=0.7,
191
+ max_tokens=600,
192
+ openai_api_key=api_key
193
+ )
194
+ prompt = f"""
195
+ Please refine the following recruitment message based on the provided feedback:
196
+
197
+ ORIGINAL MESSAGE:
198
+ {original_message}
199
+
200
+ FEEDBACK:
201
+ {feedback}
202
+
203
+ Please provide your response in the following format:
204
+ SUBJECT: [Your subject line here]
205
+
206
+ BODY:
207
+ [Your refined email body content here]
208
+ Keep the same tone and intent as the original message, but incorporate the feedback to improve it.
209
+ """
210
+ try:
211
+ messages = [
212
+ SystemMessage(content="You are a professional recruitment message writer. Refine the given message based on feedback while maintaining professionalism and the original intent."),
213
+ HumanMessage(content=prompt)
214
+ ]
215
+ response = llm.invoke(messages)
216
+ content = response.content.strip()
217
+ subject_line = ""
218
+ body_content = ""
219
+ lines = content.split('\n')
220
+ body_found = False
221
+ body_lines = []
222
+ for line in lines:
223
+ if line.strip().startswith('SUBJECT:'):
224
+ subject_line = line.replace('SUBJECT:', '').strip()
225
+ elif line.strip().startswith('BODY:'):
226
+ body_found = True
227
+ elif body_found and line.strip():
228
+ body_lines.append(line)
229
+ body_content = '\n'.join(body_lines).strip()
230
+ if not subject_line:
231
+ subject_line = "Recruitment Opportunity - Updated"
232
+ if not body_content:
233
+ body_content = content
234
+ return subject_line, body_content
235
+ except Exception as e:
236
+ raise HTTPException(status_code=500, detail=f"Error refining message: {str(e)}")
237
+
238
+ def generate_recruitment_message_with_subject(
239
+ msg_type: str,
240
+ company: str,
241
+ role_title: str,
242
+ recruiter: str,
243
+ org: str,
244
+ candidate: str,
245
+ current_pos: str,
246
+ evaluation: str,
247
+ feedback: Optional[str] = None
248
+ ) -> Tuple[str, str]:
249
+ api_key = os.getenv("OPENAI_API_KEY")
250
+ llm = ChatOpenAI(
251
+ model="gpt-4o-mini",
252
+ temperature=0.7,
253
+ max_tokens=600,
254
+ openai_api_key=api_key
255
+ )
256
+ base_prompt = f"""
257
+ Generate a professional recruitment {msg_type} with the following details:
258
+ - Company hiring: {company}
259
+ - Role: {role_title}
260
+ - Recruiter: {recruiter} from {org}
261
+ - Candidate: {candidate}
262
+ - Candidate's current position: {current_pos}
263
+ - Evaluation: {evaluation}
264
+ """
265
+ if msg_type == "outreach":
266
+ prompt = base_prompt + """
267
+ Create an initial outreach message that:
268
+ - Introduces the recruiter and organization
269
+ - Mentions the specific role and company
270
+ - Expresses interest in discussing the opportunity
271
+ - Keeps it short and to the point.
272
+ - Do not include any placeholders like [Candidate Name] or [Role Title] in the email.
273
+ """
274
+ elif msg_type == "introductory":
275
+ prompt = base_prompt + """
276
+ Create an introductory message that:
277
+ - Thanks the candidate for their initial response
278
+ - Provides more details about the role and company
279
+ - Explains why this opportunity aligns with their background
280
+ - Suggests next steps (like a call or meeting)
281
+ - Maintains a warm, professional tone
282
+ """
283
+ else: # followup
284
+ prompt = base_prompt + """
285
+ Create a follow-up message that:
286
+ - References previous communication
287
+ - Reiterates interest in the candidate
288
+ - Addresses any potential concerns
289
+ - Provides additional compelling reasons to consider the role
290
+ - Includes a clear call to action
291
+ """
292
+ if feedback:
293
+ prompt += f"\n\nPlease modify the message based on this feedback: {feedback}"
294
+ prompt += """
295
+
296
+ Please provide your response in the following format:
297
+ SUBJECT: [Your subject line here]
298
+
299
+ BODY:
300
+ [Your email body content here]
301
+ """
302
+ try:
303
+ messages = [
304
+ SystemMessage(content="You are a professional recruitment message writer. Generate both an email subject line and body content. Follow the exact format requested."),
305
+ HumanMessage(content=prompt)
306
+ ]
307
+ response = llm.invoke(messages)
308
+ content = response.content.strip()
309
+ subject_line = ""
310
+ body_content = ""
311
+ lines = content.split('\n')
312
+ body_found = False
313
+ body_lines = []
314
+ for line in lines:
315
+ if line.strip().startswith('SUBJECT:'):
316
+ subject_line = line.replace('SUBJECT:', '').strip()
317
+ elif line.strip().startswith('BODY:'):
318
+ body_found = True
319
+ elif body_found and line.strip():
320
+ body_lines.append(line)
321
+ body_content = '\n'.join(body_lines).strip()
322
+ if not subject_line:
323
+ subject_line = f"Opportunity at {company} - {role_title}"
324
+ if not body_content:
325
+ body_content = content
326
+ return subject_line, body_content
327
+ except Exception as e:
328
+ raise HTTPException(status_code=500, detail=f"Error generating message: {str(e)}")
329
+
330
+ # ------------------------------------------
331
+ # FastAPI Endpoints
332
+ # ------------------------------------------
333
+ @app.get("/")
334
+ async def root():
335
+ return {
336
+ "message": "Recruitment Message Generator API",
337
+ "version": "1.0.0",
338
+ "endpoints": [
339
+ "/generate-message",
340
+ "/refine-message",
341
+ "/authenticate",
342
+ "/docs"
343
+ ]
344
+ }
345
+
346
+ @app.post("/generate-message", response_model=MessageResponse)
347
+ async def generate_message(request: GenerateMessageRequest, background_tasks: BackgroundTasks):
348
+ try:
349
+ current_position = f"{request.current_role} at {request.current_company}"
350
+ email_subject, generated_message = generate_recruitment_message_with_subject(
351
+ msg_type=request.message_type.value.replace('followup', 'follow-up'),
352
+ company=request.company_name,
353
+ role_title=request.role,
354
+ recruiter=request.recruiter_name,
355
+ org=request.organisation,
356
+ candidate=request.candidate_name,
357
+ current_pos=current_position,
358
+ evaluation=request.job_evaluation
359
+ )
360
+ email_sent = False
361
+ if request.send_email:
362
+ registered_users = []
363
+ if os.path.exists("registered_users.csv"):
364
+ df = pd.read_csv("registered_users.csv")
365
+ registered_users = df['email'].tolist() if 'email' in df.columns else []
366
+ if request.sender_email.lower() not in [user.lower() for user in registered_users]:
367
+ return MessageResponse(
368
+ success=True,
369
+ message=generated_message,
370
+ email_sent=False,
371
+ email_subject=email_subject,
372
+ error="Email not sent: Sender email is not registered"
373
+ )
374
+ service = authenticate_gmail(request.sender_email)
375
+ if service:
376
+ email_message = create_email_message(
377
+ sender=request.sender_email,
378
+ to=request.recipient_email,
379
+ subject=email_subject,
380
+ message_text=generated_message,
381
+ reply_to=request.reply_to_email
382
+ )
383
+ email_sent = send_gmail_message(service, "me", email_message)
384
+ if not email_sent:
385
+ return MessageResponse(
386
+ success=True,
387
+ message=generated_message,
388
+ email_sent=False,
389
+ email_subject=email_subject,
390
+ error="Email not sent: Failed to send via Gmail API"
391
+ )
392
+ else:
393
+ return MessageResponse(
394
+ success=True,
395
+ message=generated_message,
396
+ email_sent=False,
397
+ email_subject=email_subject,
398
+ error="Email not sent: Gmail authentication failed"
399
+ )
400
+ return MessageResponse(
401
+ success=True,
402
+ message=generated_message,
403
+ email_sent=email_sent,
404
+ email_subject=email_subject
405
+ )
406
+ except Exception as e:
407
+ return MessageResponse(
408
+ success=False,
409
+ message="",
410
+ error=str(e)
411
+ )
412
+
413
+ @app.post("/refine-message", response_model=MessageResponse)
414
+ async def refine_message(request: FeedbackRequest):
415
+ try:
416
+ email_subject, refined_message = refine_message_with_feedback(
417
+ original_message=request.message,
418
+ feedback=request.feedback
419
+ )
420
+ return MessageResponse(
421
+ success=True,
422
+ message=refined_message,
423
+ email_sent=False,
424
+ email_subject=email_subject
425
+ )
426
+ except Exception as e:
427
+ return MessageResponse(
428
+ success=False,
429
+ message="",
430
+ error=str(e)
431
+ )
432
+
433
+ @app.post("/authenticate", response_model=AuthenticateResponse)
434
+ async def authenticate_user(request: AuthenticateRequest):
435
+ try:
436
+ if check_user_token_exists(request.email):
437
+ service = authenticate_gmail(request.email, create_if_missing=False)
438
+ if service:
439
+ return AuthenticateResponse(
440
+ success=True,
441
+ message="User already authenticated and token is valid"
442
+ )
443
+ else:
444
+ service = authenticate_gmail(request.email, create_if_missing=True)
445
+ if service:
446
+ return AuthenticateResponse(
447
+ success=True,
448
+ message="Token refreshed successfully"
449
+ )
450
+ else:
451
+ return AuthenticateResponse(
452
+ success=False,
453
+ message="Failed to refresh token",
454
+ error="Could not refresh existing token. Please check credentials.json"
455
+ )
456
+ else:
457
+ try:
458
+ creds = create_new_credentials(request.email)
459
+ if creds:
460
+ return AuthenticateResponse(
461
+ success=True,
462
+ message="Authentication successful. Token created and saved."
463
+ )
464
+ else:
465
+ return AuthenticateResponse(
466
+ success=False,
467
+ message="Authentication failed",
468
+ error="Failed to create credentials"
469
+ )
470
+ except Exception as e:
471
+ return AuthenticateResponse(
472
+ success=False,
473
+ message="Authentication failed",
474
+ error=str(e)
475
+ )
476
+ except Exception as e:
477
+ return AuthenticateResponse(
478
+ success=False,
479
+ message="Authentication error",
480
+ error=str(e)
481
+ )
482
+
483
+ @app.get("/health")
484
+ async def health_check():
485
+ return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}
486
+
487
+ if __name__ == "__main__":
488
+ import uvicorn
489
+ uvicorn.run(app, host="0.0.0.0", port=8000)