singhn9 commited on
Commit
f87bf64
·
verified ·
1 Parent(s): 696a38d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +799 -409
app.py CHANGED
@@ -1,458 +1,848 @@
1
- import os
2
- import io
3
- import json
4
- import random
5
- import tempfile
6
- import smtplib
7
- from email.message import EmailMessage
8
- from datetime import datetime, timedelta, timezone
9
- from fastapi import FastAPI, UploadFile, Form, Request
10
- from fastapi.responses import JSONResponse
11
- from starlette.middleware.cors import CORSMiddleware
12
- from sentence_transformers import SentenceTransformer, util
13
- from PyPDF2 import PdfReader
14
  import gradio as gr
15
- import torch
16
- import pytz
17
- from dropbox_utils import upload_to_dropbox
18
- import asyncio
19
- os.environ["HF_HOME"] = "/app/cache"
20
- os.environ["TRANSFORMERS_CACHE"] = "/app/cache"
21
- os.environ["HF_DATASETS_CACHE"] = "/app/cache"
22
-
23
- smtp_user = os.getenv("SMTP_USER")
24
- smtp_pass = os.getenv("SMTP_PASS")
25
- if not smtp_user or not smtp_pass:
26
- raise EnvironmentError("SMTP credentials are not set in environment variables.")
27
-
28
-
29
- # Setup model cache path
30
- # os.environ["TRANSFORMERS_CACHE"] = os.environ.get("TRANSFORMERS_CACHE", "/app/cache")
31
- # os.environ["HF_HOME"] = os.environ.get("HF_HOME", "/app/cache")
32
-
33
- # === Profile Save/Load ===
34
- PROFILE_DIR = os.path.join(os.getenv("HF_HOME", "/app/cache"), "user_profiles")
35
- os.makedirs(PROFILE_DIR, exist_ok=True)
36
-
37
- def test_writable_dirs():
38
- for path in ["/app/cache", PROFILE_DIR, "/tmp"]:
39
- print(f"🔍 Checking write permission for: {path}")
40
- if os.access(path, os.W_OK):
41
- print("✅ Writable")
 
 
 
 
42
  else:
43
- print("❌ Not writable")
44
-
45
- test_writable_dirs()
46
- # from huggingface_hub import login
 
47
 
48
- # # Load HF token and login
49
- hf_token = os.environ.get("HUGGINGFACE_HUB_TOKEN")
50
- # if hf_token:
51
- # login(token=hf_token, add_to_git_credential=False, write_permission=False)
52
 
53
- # === Load Model with CUDA if available and safe cache ===
54
- device = "cuda" if torch.cuda.is_available() else "cpu"
55
- print(f"🚀 Loading SentenceTransformer on: {device}")
56
-
57
-
58
-
59
-
60
- # === Define Cohorts ===
61
- COHORTS = {
62
- "consulting": "Management consulting, strategy, analytics, client interaction",
63
- "bfsi": "Banking, finance, investment analysis, risk, fintech",
64
- "sales": "Sales, business development, GTM strategy, CRM, channel sales",
65
- "it": "Software development, cloud, AI/ML, backend systems",
66
- "hr": "Human resources, L&D, talent acquisition, HRBP",
67
- "legal": "Contracts, litigation, compliance, intellectual property",
68
- "scm": "Logistics, procurement, inventory, operations, manufacturing",
69
- "bpo": "Customer service, support, inbound/outbound calling, operations"
70
- }
71
-
72
-
73
- # === Helper to extract text from PDF ===
74
- def extract_text(file):
75
- reader = PdfReader(file)
76
- return "\n".join([page.extract_text() or "" for page in reader.pages])
77
-
78
- # === Gradio UI function for resume match ===
79
- def match_resume(resume_pdf, job_description):
80
- text = extract_text(resume_pdf)
81
- resume_emb = model.encode(text, convert_to_tensor=True)
82
- jd_emb = model.encode(job_description, convert_to_tensor=True)
83
- score = util.cos_sim(jd_emb, resume_emb).item() * 100
84
- label = "✅ Strong Match" if score > 70 else "⚠️ Needs Tailoring"
85
- return f"Match Score: {round(score, 2)}%\n\n{label}"
86
-
87
- demo = gr.Interface(
88
- fn=match_resume,
89
- inputs=[
90
- gr.File(label="Upload Resume PDF", file_types=[".pdf"]),
91
- gr.Textbox(label="Paste Job Description", lines=6)
92
- ],
93
- outputs="text",
94
- title="🧠 Resume to JD Matcher",
95
- description="Upload your resume PDF and paste a job description to get a similarity score and feedback!"
96
- )
97
-
98
- # === FastAPI App ===
99
- fastapi_app = FastAPI()
100
- model = None
101
- COHORT_EMBEDDINGS = {}
102
- # === Load model eagerly before app starts ===
103
- print("🕐 Preloading SentenceTransformer model before app declaration...")
104
-
105
- model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2", use_auth_token=hf_token).to(device)
106
- COHORT_EMBEDDINGS = {
107
- k: model.encode(v, convert_to_tensor=True) for k, v in COHORTS.items()
108
  }
109
 
110
- print("✅ Model and cohort embeddings loaded.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
- fastapi_app.add_middleware(
113
- CORSMiddleware,
114
- allow_origins=["*"],
115
- allow_methods=["*"],
116
- allow_headers=["*"]
117
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
- @fastapi_app.post("/debug")
120
- async def debug(request: Request):
121
- body = await request.body()
122
- return {"body": body.decode(), "headers": dict(request.headers)}
 
 
123
 
 
 
124
 
 
 
125
 
126
- @fastapi_app.get("/status")
127
- async def status():
128
- return {"model_loaded": model is not None}
129
 
 
 
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
- # === In-memory OTP store ===
133
- OTP_STORE = {}
 
 
 
134
 
135
- from pydantic import BaseModel
 
 
 
 
 
 
 
136
 
137
- class EmailRequest(BaseModel):
138
- email: str
 
 
 
 
 
139
 
140
- @fastapi_app.post("/send-otp")
141
- async def send_otp(request: EmailRequest):
142
- email = request.email
143
- otp = str(random.randint(100000, 999999))
144
- expiry = datetime.now() + timedelta(minutes=10)
145
- OTP_STORE[email] = (otp, expiry)
 
 
 
 
 
 
 
 
146
 
147
- msg = EmailMessage()
148
- msg["Subject"] = "Your ResumePilot Login OTP"
149
- msg["From"] = smtp_user
150
- msg["To"] = email
151
- msg.set_content(f"Your one-time password (OTP) is: {otp}. It will expire in 10 minutes.")
152
 
153
- try:
154
- with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
155
- smtp.login(smtp_user, smtp_pass)
156
- smtp.send_message(msg)
157
- return {"status": "sent"}
158
- except Exception as e:
159
- return JSONResponse({"status": "error", "error": str(e)}, status_code=500)
160
 
161
- from pydantic import BaseModel
162
- from fastapi.responses import JSONResponse
163
- from datetime import datetime
164
- import random
165
 
166
- class OTPVerifyRequest(BaseModel):
167
- email: str
168
- otp: str
169
 
170
- @fastapi_app.post("/verify-otp")
171
- async def verify_otp(request: OTPVerifyRequest):
172
- email = request.email
173
- otp = request.otp
174
 
175
- stored = OTP_STORE.get(email)
176
- if not stored:
177
- return JSONResponse({"error": "No OTP found"}, status_code=400)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
- saved_otp, expiry = stored
180
- if datetime.now() > expiry:
181
- return JSONResponse({"error": "OTP expired"}, status_code=401)
182
 
183
- if otp != saved_otp:
184
- return JSONResponse({"error": "Invalid OTP"}, status_code=401)
185
 
186
- # OTP valid — return a token and email
187
- return {"token": f"token_{random.randint(100000, 999999)}", "email": email}
188
-
189
-
190
- from fastapi import Form
191
- from email.message import EmailMessage
192
- import smtplib
193
- import secrets
194
- import os
195
-
196
- MAGIC_TOKENS = {} # In-memory token storage
197
-
198
- @fastapi_app.post("/send_magic_link")
199
- async def send_magic_link(email: str = Form(...)):
200
- username = email.split("@")[0]
201
- token = secrets.token_urlsafe(16)
202
- MAGIC_TOKENS[username] = token
203
 
204
- link = f"https://tendermatcher.tech/campus/?token={token}&user={username}"
205
 
 
 
 
 
 
 
 
206
 
207
- msg = EmailMessage()
208
- msg["Subject"] = "🔓 Your Magic Login Link"
209
- msg["From"] = os.environ["GMAIL_USER"]
210
- msg["To"] = email
211
- msg.set_content(f"Hi {username},\n\nClick here to log in:\n{link}\n\nCheers,\nResumePilot")
212
 
213
- with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
214
- smtp.login(os.environ["GMAIL_USER"], os.environ["GMAIL_APP_PASSWORD"])
215
- smtp.send_message(msg)
 
 
 
 
 
216
 
217
- return {"status": "sent", "token": token}
 
 
 
 
218
 
219
 
220
- @fastapi_app.post("/verify_magic_token")
221
- async def verify_magic_token(user: str = Form(...), token: str = Form(...)):
222
- stored_token = MAGIC_TOKENS.get(user)
223
- if not stored_token:
224
- return JSONResponse({"error": "No token found for user"}, status_code=400)
225
- if token != stored_token:
226
- return JSONResponse({"error": "Invalid token"}, status_code=401)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
- # ✅ Valid magic link
229
- return {"status": "verified", "user": user}
 
 
 
 
 
 
 
 
 
 
230
 
 
 
 
231
 
 
 
 
 
 
232
 
 
 
 
233
 
234
- # === Resume Matcher endpoint ===
235
- @fastapi_app.post("/predict")
236
- async def predict(file: UploadFile, jd: str = Form(...), email: str = Form(...)):
237
- try:
238
- content = await file.read()
239
- pdf = io.BytesIO(content)
240
- text = extract_text(pdf)
241
- resume_emb = model.encode(text, convert_to_tensor=True)
242
- jd_emb = model.encode(jd, convert_to_tensor=True)
243
- score = util.cos_sim(jd_emb, resume_emb).item() * 100
244
- label = "Strong Match" if score > 70 else "Needs Tailoring"
245
-
246
- with tempfile.TemporaryDirectory() as tmpdir:
247
- upload_to_dropbox(content, f"/spc_cohort_data/{email}/resume.pdf")
248
- jd_data = json.dumps({"score": round(score, 2), "feedback": label}).encode("utf-8")
249
- upload_to_dropbox(jd_data, f"/spc_cohort_data/{email}/jd_match.json")
250
-
251
- return JSONResponse({"score": round(score, 2), "feedback": label, "device": device})
252
- except Exception as e:
253
- return JSONResponse({"error": str(e)}, status_code=500)
254
-
255
- # === Cohort Predictor endpoint ===
256
- @fastapi_app.post("/cohort")
257
- async def cohort_predict(name: str = Form(...), email: str = Form(...), summary: str = Form(...), quiz: str = Form(...)):
258
- try:
259
- combined = f"{name}\n{email}\n{summary}"
260
- user_emb = model.encode(combined, convert_to_tensor=True)
261
- scores = {cohort: util.cos_sim(user_emb, emb).item() for cohort, emb in COHORT_EMBEDDINGS.items()}
262
- predicted = max(scores, key=scores.get)
263
-
264
- with tempfile.TemporaryDirectory() as tmpdir:
265
- quiz_bytes = json.dumps(json.loads(quiz)).encode("utf-8")
266
- upload_to_dropbox(quiz_bytes, f"/spc_cohort_data/{email}/quiz.json")
267
-
268
- cohort_result = {
269
- "predicted_cohort": predicted,
270
- "scores": {k: round(v * 100, 2) for k, v in scores.items()}
271
- }
272
- upload_to_dropbox(json.dumps(cohort_result).encode("utf-8"), f"/spc_cohort_data/{email}/cohort.json")
273
-
274
- return JSONResponse(cohort_result)
275
- except Exception as e:
276
- return JSONResponse({"error": str(e)}, status_code=500)
277
 
 
 
278
 
279
- from dropbox.exceptions import ApiError
280
- import dropbox
 
 
 
 
 
 
 
 
 
281
 
282
- DROPBOX_ACCESS_TOKEN = os.getenv("DROPBOX_REFRESH_TOKEN")
283
- dbx = dropbox.Dropbox(DROPBOX_ACCESS_TOKEN)
284
- @fastapi_app.post("/save_profile")
285
- async def save_profile(request: Request):
286
- form = await request.form()
287
- email = form.get("email")
288
- full_name = form.get("full_name")
289
- job_title = form.get("job_title")
290
 
291
- if not email or not full_name or not job_title:
292
- return JSONResponse({"error": "Missing profile fields"}, status_code=400)
 
 
 
 
293
 
294
- path = get_profile_path(email)
 
 
 
 
 
 
 
295
 
296
- try:
297
- try:
298
- _, res = dbx.files_download(path)
299
- profile_data = json.loads(res.content)
300
- except ApiError:
301
- profile_data = {}
302
-
303
- profile_data.update({
304
- "full_name": full_name,
305
- "job_title": job_title
306
- })
307
-
308
- upload_to_dropbox(
309
- json.dumps(profile_data).encode(),
310
- path)
311
-
312
- return JSONResponse({"success": True})
313
- except Exception as e:
314
- return JSONResponse({"error": str(e)}, status_code=500)
315
-
316
- @fastapi_app.get("/load_profile")
317
- async def load_profile(email: str):
318
- path = get_profile_path(email)
319
 
320
- try:
321
- _, res = dbx.files_download(path)
322
- profile_data = json.loads(res.content)
323
- return JSONResponse({
324
- "full_name": profile_data.get("full_name", ""),
325
- "job_title": profile_data.get("job_title", "")
326
- })
327
- except ApiError:
328
- return JSONResponse({"full_name": "", "job_title": ""})
329
- except Exception as e:
330
- return JSONResponse({"error": str(e)}, status_code=500)
331
-
332
-
333
-
334
-
335
- def get_profile_path(email: str):
336
- return f"/spc_cohort_data/{email}/profile.json"
337
-
338
- @fastapi_app.post("/save_theme")
339
- async def save_theme(request: Request):
340
- form = await request.form()
341
- email = form.get("email")
342
- theme = form.get("theme")
343
-
344
- if not email or not theme:
345
- return JSONResponse({"error": "Missing email or theme"}, status_code=400)
346
-
347
- path = get_profile_path(email)
348
-
349
- try:
350
- try:
351
- _, res = dbx.files_download(path)
352
- profile_data = json.loads(res.content)
353
- except ApiError:
354
- profile_data = {}
355
-
356
- profile_data["theme"] = theme
 
 
 
 
357
 
358
 
359
- upload_to_dropbox(
360
- json.dumps(profile_data).encode(),
361
- path
362
- )
363
-
364
- return JSONResponse({"success": True})
365
- except Exception as e:
366
- return JSONResponse({"error": str(e)}, status_code=500)
367
-
368
- @fastapi_app.get("/load_theme")
369
- async def load_theme(email: str):
370
- path = get_profile_path(email)
371
-
372
- try:
373
- _, res = dbx.files_download(path)
374
- profile_data = json.loads(res.content)
375
- return JSONResponse({"theme": profile_data.get("theme", "light")})
376
- except ApiError:
377
- return JSONResponse({"theme": "light"})
378
- except Exception as e:
379
- return JSONResponse({"error": str(e)}, status_code=500)
380
-
381
-
382
-
383
-
384
- # === Log Endpoint ===
385
- @fastapi_app.post("/log")
386
- async def receive_log(request: Request):
387
- try:
388
- payload = await request.json()
389
- timestamp = datetime.now(timezone.utc).astimezone(pytz.timezone("Asia/Kolkata")).isoformat()
390
- payload["logged_at"] = timestamp
391
-
392
- log_line = json.dumps(payload, ensure_ascii=False) + "\n"
393
- today = datetime.now().strftime("%Y-%m-%d")
394
- log_path = f"/spc_cohort_data/logs/{today}.jsonl"
395
- upload_to_dropbox(log_line.encode("utf-8"), log_path, append=True)
396
-
397
- return {"status": "logged"}
398
- except Exception as e:
399
- return JSONResponse({"error": str(e)}, status_code=500)
400
-
401
- # === List routes endpoint ===
402
- @fastapi_app.get("/routes")
403
- async def list_routes():
404
- return [
405
- {
406
- "path": route.path,
407
- "methods": list(route.methods),
408
- "name": route.name
409
- }
410
- for route in fastapi_app.routes
411
- if hasattr(route, "methods")
412
- ]
413
-
414
- @fastapi_app.get("/")
415
- async def root_redirect():
416
- return JSONResponse({"message": "Visit /ui for the resume matcher and API routes"})
417
-
418
- @fastapi_app.get("/health")
419
- async def health():
420
- return {"status": "ok"}
421
 
422
 
423
- # === Gradio UI for listing routes ===
424
- from fastapi.testclient import TestClient
425
-
426
- client = TestClient(fastapi_app)
427
-
428
- def get_routes_str():
429
- response = client.get("/routes")
430
- if response.status_code == 200:
431
- return json.dumps(response.json(), indent=2)
432
- else:
433
- return f"Error fetching routes: {response.status_code}"
434
-
435
-
436
- routes_demo = gr.Interface(
437
- fn=get_routes_str,
438
- inputs=[],
439
- outputs="textbox",
440
- title="API Routes",
441
- description="List of all API routes exposed by FastAPI backend"
442
- )
443
-
444
- with gr.Blocks() as ui:
445
- with gr.Tabs():
446
- with gr.TabItem("Resume Matcher"):
447
- demo.render()
448
- with gr.TabItem("API Routes"):
449
- routes_demo.render()
450
-
451
-
452
- # Mount the combined UI at /ui
453
- app = fastapi_app
454
-
455
- if __name__ == "__main__":
456
- import os
457
- import uvicorn
458
- ui.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
+ from sentence_transformers import SentenceTransformer
3
+ from sklearn.metrics.pairwise import cosine_similarity
4
+ import pandas as pd
5
+ import numpy as np
6
+ import os
7
+ from datetime import datetime
8
+ import socket
9
+ import nltk
10
+ #nltk.download("all")
11
+
12
+
13
+ # For sentence tokenization
14
+ nltk.download('punkt')
15
+ nltk.download("punkt_tab")
16
+ ###############################
17
+ # LOGGING SETUP
18
+ ###############################
19
+ log_file_path = os.path.expanduser("~/Second_Opinion_Logs.log")
20
+
21
+ if not os.path.exists(log_file_path):
22
+ with open(log_file_path, mode='w') as log_file:
23
+ log_file.write("Timestamp (GMT) - IP: [IP Address] - [Action]\n")
24
+
25
+ def log_action(action, request=None):
26
+ """
27
+ Logs major actions with IP address and UTC timestamp.
28
+ """
29
+ try:
30
+ user_ip = "Unknown IP"
31
+ if request and hasattr(request, 'client'):
32
+ user_ip = request.client.host
33
  else:
34
+ user_ip = socket.gethostbyname(socket.gethostname())
35
+ if user_ip in ("127.0.0.1", "::1"):
36
+ user_ip = "Localhost (127.0.0.1)"
37
+ except Exception:
38
+ user_ip = "Unknown IP"
39
 
40
+ timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
41
+ log_entry = f"{timestamp} (GMT) - IP: {user_ip} - {action}\n"
 
 
42
 
43
+ try:
44
+ with open(log_file_path, 'a') as log_file:
45
+ log_file.write(log_entry)
46
+ print(f"Log entry added: {log_entry.strip()}")
47
+ except Exception as e:
48
+ print(f"Error logging action: {e}")
49
+
50
+ ###############################
51
+ # LOAD MODELS
52
+ ###############################
53
+ models = {
54
+ "all-MiniLM-L6-v2": SentenceTransformer("all-MiniLM-L6-v2"),
55
+ "paraphrase-MiniLM-L6-v2": SentenceTransformer("paraphrase-MiniLM-L6-v2"),
56
+ "multi-qa-MiniLM-L6-cos-v1": SentenceTransformer("multi-qa-MiniLM-L6-cos-v1"),
57
+ "all-mpnet-base-v2": SentenceTransformer("all-mpnet-base-v2"),
58
+ "paraphrase-mpnet-base-v2": SentenceTransformer("paraphrase-mpnet-base-v2"),
59
+ "all-distilroberta-v1": SentenceTransformer("all-distilroberta-v1"),
60
+ "paraphrase-albert-small-v2": SentenceTransformer("paraphrase-albert-small-v2"),
61
+ "multi-qa-distilbert-cos-v1": SentenceTransformer("multi-qa-distilbert-cos-v1"),
62
+ "distiluse-base-multilingual-cased-v2": SentenceTransformer("distiluse-base-multilingual-cased-v2"),
63
+ "all-MiniLM-L12-v2": SentenceTransformer("all-MiniLM-L12-v2"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  }
65
 
66
+ ###############################
67
+ # MAIN SIMILARITY FUNCTION
68
+ ###############################
69
+ def compute_similarity(resume_text, job_descriptions):
70
+ """
71
+ Computes similarity for each model between the resume_text and each job description (split by double line breaks).
72
+ Returns a tuple of (reasoning_html, table_html).
73
+ """
74
+ try:
75
+ if not resume_text.strip() or not job_descriptions.strip():
76
+ return "<b>Error:</b> Resume and job descriptions cannot be empty.", None
77
+
78
+ # Split job descriptions by double line break
79
+ job_list = job_descriptions.split("\n\n")
80
+ if len(job_list) == 0:
81
+ return "<b>Error:</b> Provide at least one job description separated by double line breaks.", None
82
+
83
+ # Dictionary to hold model results
84
+ results = {}
85
+ for model_name, model in models.items():
86
+ # Encode resume and all job descriptions
87
+ documents = [resume_text] + job_list
88
+ embeddings = model.encode(documents)
89
+ resume_embedding = embeddings[0]
90
+ job_embeddings = embeddings[1:]
91
+ similarities = cosine_similarity([resume_embedding], job_embeddings).flatten()
92
+ results[model_name] = similarities
93
+
94
+ # Convert to DataFrame
95
+ df = pd.DataFrame(results, index=[f"Job {i+1}" for i in range(len(job_list))]).T
96
+
97
+ # Compute metrics
98
+ metrics = {
99
+ "Average": df.mean(axis=0),
100
+ "Variance": df.var(axis=0),
101
+ "Median": df.median(axis=0),
102
+ "Standard Deviation": df.std(axis=0),
103
+ "Certainty Score": 1 - (df.var(axis=0) / df.var(axis=0).max()),
104
+ }
105
+ for metric_name, values in metrics.items():
106
+ df.loc[metric_name] = values
107
+
108
+ # Separate model rows from metrics rows
109
+ model_rows = df.iloc[:-5]
110
+ metrics_rows = df.iloc[-5:]
111
+
112
+ # Style the DataFrame
113
+ styled_df = model_rows.style.background_gradient(cmap="Greens", axis=None).to_html()
114
+ styled_df += metrics_rows.to_html(header=False)
115
+
116
+ # Identify best job by highest average similarity
117
+ best_job = metrics["Average"].idxmax()
118
+ reasoning = f"<b>The best job match is {best_job} based on the highest average similarity score.</b>"
119
+
120
+ # Additional description
121
+ description = """
122
+ <p><b>Explanation of the Table:</b></p>
123
+ <ul>
124
+ <li><b>Models:</b> Each row corresponds to a pre-trained model used for computing similarity. Below are details about each model:</li>
125
+ <ul>
126
+ <li><b>all-MiniLM-L6-v2:</b> Trained on NLI and STS datasets. Developed by Hugging Face and Microsoft. (<a href="https://arxiv.org/abs/2012.15832" target="_blank">Research Paper</a>).</li>
127
+ <li><b>paraphrase-MiniLM-L6-v2:</b> Optimized for paraphrase detection on datasets like Quora Questions and MSRPC. (<a href="https://arxiv.org/abs/2012.15832" target="_blank">Research Paper</a>).</li>
128
+ <li><b>multi-qa-MiniLM-L6-cos-v1:</b> Fine-tuned for question-answering tasks using datasets like SQuAD and Natural Questions.</li>
129
+ <li><b>all-mpnet-base-v2:</b> Robust embeddings for high-contextualized tasks. (<a href="https://arxiv.org/abs/2004.09297" target="_blank">Research Paper</a>).</li>
130
+ <li><b>paraphrase-mpnet-base-v2:</b> Reliable for paraphrase tasks, trained on diverse datasets.</li>
131
+ <li><b>all-distilroberta-v1:</b> A lightweight RoBERTa-based model for sentence embeddings. (<a href="https://arxiv.org/abs/1907.11692" target="_blank">Research Paper</a>).</li>
132
+ <li><b>paraphrase-albert-small-v2:</b> Suitable for paraphrasing in resource-constrained environments.</li>
133
+ <li><b>multi-qa-distilbert-cos-v1:</b> Optimized for multilingual question-answering tasks.</li>
134
+ <li><b>distiluse-base-multilingual-cased-v2:</b> Trained on multilingual datasets for cross-lingual embeddings.</li>
135
+ <li><b>all-MiniLM-L12-v2:</b> Deeper MiniLM variant for enhanced contextual understanding.</li>
136
+ </ul>
137
+ <li><b>Metrics:</b>
138
+ <ul>
139
+ <li><b>Average:</b> Mean similarity score for each job description.</li>
140
+ <li><b>Variance:</b> Variability in the similarity scores.</li>
141
+ <li><b>Median:</b> Middle value of the similarity scores.</li>
142
+ <li><b>Standard Deviation:</b> Spread of the similarity scores around the mean.</li>
143
+ <li><b>Certainty Score:</b> Indicates model agreement, with 1 being the highest consensus.</li>
144
+ </ul>
145
+ </li>
146
+ </ul>
147
+ <p>If you liked this application, feel free to send your feedback, suggestions, or adulations to <b>21f1006368@ds.study.iitm.ac.in</b>.</p>
148
+ """
149
+
150
+ return reasoning, styled_df + description
151
 
152
+ except Exception as e:
153
+ return f"<b>Error during computation:</b> {str(e)}", None
154
+
155
+ ###############################
156
+ # APPROACH A EXPLANATION
157
+ ###############################
158
+ def explain_scores_by_sentences(model, resume_text, job_text, top_k=3):
159
+ """
160
+ Given a SentenceTransformer model, a resume, and one job description,
161
+ returns HTML with the top-k (resume_sentence, job_sentence) pairs by similarity.
162
+ """
163
+ from nltk.tokenize import sent_tokenize
164
+
165
+ resume_sents = sent_tokenize(resume_text)
166
+ job_sents = sent_tokenize(job_text)
167
+
168
+ if not resume_sents or not job_sents:
169
+ return "<b>No sentences found in resume or job description.</b>"
170
+
171
+ # Encode each sentence
172
+ resume_embeddings = model.encode(resume_sents)
173
+ job_embeddings = model.encode(job_sents)
174
+
175
+ # Pairwise cosine similarity
176
+ sim_matrix = cosine_similarity(resume_embeddings, job_embeddings)
177
+
178
+ # Flatten and pick top K
179
+ flat_sim = sim_matrix.flatten()
180
+ top_k_indices = np.argsort(flat_sim)[::-1][:top_k]
181
+
182
+ explanation_html = "<h4>Top Similar Sentence Pairs</h4>"
183
+ for rank, idx in enumerate(top_k_indices, start=1):
184
+ row = idx // job_embeddings.shape[0]
185
+ col = idx % job_embeddings.shape[0]
186
+ score = sim_matrix[row, col]
187
+
188
+ resume_sentence = resume_sents[row]
189
+ job_sentence = job_sents[col]
190
+ explanation_html += f"""
191
+ <p><b>#{rank}:</b><br>
192
+ <b>Resume:</b> {resume_sentence}<br>
193
+ <b>Job:</b> {job_sentence}<br>
194
+ <b>Similarity Score:</b> {score:.4f}</p>
195
+ """
196
+
197
+ return explanation_html
198
+
199
+ def explain_model_scores(model_name, resume, jobs, selected_job_idx, top_k=3):
200
+ """
201
+ For a given model_name, resume, and job descriptions, returns a gr.update object
202
+ containing HTML that explains which sentence pairs are most similar, making the
203
+ explanation visible in the Gradio app.
204
+ """
205
+ try:
206
+ model = models[model_name]
207
+ job_list = jobs.split("\n\n")
208
 
209
+ # Check valid job index
210
+ if selected_job_idx < 0 or selected_job_idx >= len(job_list):
211
+ return gr.update(
212
+ value=f"<b>Error:</b> Invalid job index {selected_job_idx}.",
213
+ visible=True
214
+ )
215
 
216
+ resume_text = resume.strip()
217
+ job_text = job_list[int(selected_job_idx)].strip()
218
 
219
+ if not resume_text:
220
+ return gr.update(value="<b>No resume text provided.</b>", visible=True)
221
 
222
+ if not job_text:
223
+ return gr.update(value=f"<b>Job description #{selected_job_idx+1} is empty.</b>", visible=True)
 
224
 
225
+ explanation_html = explain_scores_by_sentences(model, resume_text, job_text, top_k)
226
+ return gr.update(value=explanation_html, visible=True)
227
 
228
+ except Exception as e:
229
+ return gr.update(value=f"<b>Error in explanation:</b> {str(e)}", visible=True)
230
+
231
+ ###############################
232
+ # GRADIO APP
233
+ ###############################
234
+ def process_and_display(resume, jobs, request=None):
235
+ """
236
+ Main callback to compute similarity, logs the user action, and yields the result.
237
+ """
238
+ try:
239
+ user_ip = "Unknown IP"
240
+ if request and hasattr(request, 'client'):
241
+ user_ip = request.client.host
242
+
243
+ # Log the event
244
+ log_action(f"Process and display triggered for IP: {user_ip}")
245
+
246
+ # Show a "processing" message first
247
+ yield gr.update(value="<b>Processing...</b>", visible=True), None, None, gr.update(visible=False)
248
+
249
+ log_action(f"Starting similarity computation for IP: {user_ip}")
250
+ reasoning, table = compute_similarity(resume, jobs)
251
+
252
+ if table:
253
+ log_action(f"Successfully processed and displayed results for IP: {user_ip}")
254
+ yield (
255
+ gr.update(value="", visible=False), # Clear the "processing" message
256
+ reasoning, # Recommendation text
257
+ table, # Table of similarities
258
+ gr.update(value="Papa Please Preach More", visible=True),
259
+ )
260
+ else:
261
+ log_action(f"Error: No results to display for IP: {user_ip}")
262
+ yield (
263
+ gr.update(value="", visible=False),
264
+ reasoning,
265
+ "<p>No results to display.</p>",
266
+ gr.update(visible=False),
267
+ )
268
+ except Exception as e:
269
+ log_action(f"Error during process for IP {user_ip}: {str(e)}")
270
+ raise e
271
+
272
+ def show_details(table):
273
+ """
274
+ Callback to reveal the full table upon user request.
275
+ """
276
+ return gr.update(value=table, visible=True)
277
+
278
+ ###############################
279
+ # BUILD THE GRADIO INTERFACE
280
+ ###############################
281
+ with gr.Blocks() as app:
282
+ gr.Markdown("# Second Opinion with Naval v1.1 – “Midnight Tears”")
283
+ gr.Markdown(
284
+ "This application matches a resume to job descriptions using SentenceTransformer models, "
285
+ "provides similarity scores, and can explain which sentences contributed to each model's score."
286
+ )
287
+
288
+ with gr.Row():
289
+ resume_input = gr.Textbox(label="Paste Resume", lines=5, placeholder="Paste your resume here...")
290
+ job_input = gr.Textbox(label="Paste Job Descriptions", lines=5,
291
+ placeholder="Paste one or more job descriptions here (double line break to separate).")
292
+
293
+ with gr.Row():
294
+ match_button = gr.Button("Match My Resume to Jobs")
295
+ processing_output = gr.HTML(value="", visible=False)
296
+
297
+ with gr.Row():
298
+ recommendation_output = gr.HTML(label="Recommendation", visible=True)
299
+ with gr.Row():
300
+ table_output = gr.HTML(label="Similarity Table", visible=False)
301
+
302
+ with gr.Row():
303
+ nerd_button = gr.Button("Papa Please Preach More", visible=False)
304
+
305
+ # "Explain" output component: hidden initially
306
+ explanation_output = gr.HTML(label="Model Explanation", visible=False)
307
+
308
+ # Main match button -> calls process_and_display
309
+ match_button.click(
310
+ process_and_display,
311
+ inputs=[resume_input, job_input],
312
+ outputs=[processing_output, recommendation_output, table_output, nerd_button]
313
+ )
314
+
315
+ # Button to show the full table
316
+ nerd_button.click(
317
+ show_details,
318
+ inputs=[table_output],
319
+ outputs=[table_output],
320
+ )
321
+
322
+ # Input for user to pick which job to explain
323
+ with gr.Row():
324
+ job_index_to_explain = gr.Number(label="Job Index (0-based)", value=0)
325
+
326
+ # Buttons to explain each model's sentence-level similarity
327
+ with gr.Row():
328
+ for m_name in models.keys():
329
+ btn = gr.Button(f"Explain {m_name}")
330
+ # Each button calls explain_model_scores for that model
331
+ btn.click(
332
+ fn=explain_model_scores,
333
+ inputs=[gr.State(m_name), resume_input, job_input, job_index_to_explain],
334
+ outputs=[explanation_output],
335
+ )
336
+
337
+ # Optional: custom CSS
338
+ app.css = """
339
+ /* Make the entire background a gradient */
340
+ body {
341
+ background: linear-gradient(120deg, #E0C3FC 0%, #8EC5FC 100%);
342
+ margin: 0;
343
+ padding: 0;
344
+ font-family: 'Open Sans', sans-serif;
345
+ min-height: 100vh; /* ensure full coverage of viewport */
346
+ }
347
 
348
+ /* Let the gradient show through behind the .gradio-container */
349
+ .gradio-container {
350
+ background-color: transparent !important;
351
+ color: #333333;
352
+ }
353
 
354
+ /* Your original style for centered recommendation text */
355
+ #centered-recommendation {
356
+ text-align: center;
357
+ font-size: 1.2em;
358
+ margin-top: 20px;
359
+ margin-bottom: 20px;
360
+ color: #2c3e50; /* a nice dark teal */
361
+ }
362
 
363
+ /* Example button styling to match the gradient vibe */
364
+ button.primary, button.secondary {
365
+ background-color: #3498db !important;
366
+ border: 1px solid #2980b9 !important;
367
+ color: #fff !important;
368
+ border-radius: 4px !important;
369
+ }
370
 
371
+ /* Optional: style textboxes or other inputs for a cleaner look */
372
+ textarea, input[type='text'], input[type='number'] {
373
+ background-color: #FFFFFF;
374
+ color: #333;
375
+ border-radius: 6px !important;
376
+ border: 1px solid #ccc !important;
377
+ padding: 8px !important;
378
+ }
379
+ /* (Optional) If you have an h1 or h2, you can style them too */
380
+ h1, h2, h3 {
381
+ color: #2c3e50;
382
+ }
383
+ """
384
+ app.launch(share=True)
385
 
 
 
 
 
 
386
 
 
 
 
 
 
 
 
387
 
 
 
 
 
388
 
 
 
 
389
 
 
 
 
 
390
 
391
+ import os
392
+ # import io
393
+ # import json
394
+ # import random
395
+ # import tempfile
396
+ # import smtplib
397
+ # from email.message import EmailMessage
398
+ # from datetime import datetime, timedelta, timezone
399
+ # from fastapi import FastAPI, UploadFile, Form, Request
400
+ # from fastapi.responses import JSONResponse
401
+ # from starlette.middleware.cors import CORSMiddleware
402
+ # from sentence_transformers import SentenceTransformer, util
403
+ # from PyPDF2 import PdfReader
404
+ # import gradio as gr
405
+ # import torch
406
+ # import pytz
407
+ # from dropbox_utils import upload_to_dropbox
408
+ # import asyncio
409
+ # os.environ["HF_HOME"] = "/app/cache"
410
+ # os.environ["TRANSFORMERS_CACHE"] = "/app/cache"
411
+ # os.environ["HF_DATASETS_CACHE"] = "/app/cache"
412
+
413
+ # smtp_user = os.getenv("SMTP_USER")
414
+ # smtp_pass = os.getenv("SMTP_PASS")
415
+ # if not smtp_user or not smtp_pass:
416
+ # raise EnvironmentError("SMTP credentials are not set in environment variables.")
417
+
418
+
419
+ # # Setup model cache path
420
+ # # os.environ["TRANSFORMERS_CACHE"] = os.environ.get("TRANSFORMERS_CACHE", "/app/cache")
421
+ # # os.environ["HF_HOME"] = os.environ.get("HF_HOME", "/app/cache")
422
+
423
+ # # === Profile Save/Load ===
424
+ # PROFILE_DIR = os.path.join(os.getenv("HF_HOME", "/app/cache"), "user_profiles")
425
+ # os.makedirs(PROFILE_DIR, exist_ok=True)
426
+
427
+ # def test_writable_dirs():
428
+ # for path in ["/app/cache", PROFILE_DIR, "/tmp"]:
429
+ # print(f"🔍 Checking write permission for: {path}")
430
+ # if os.access(path, os.W_OK):
431
+ # print("✅ Writable")
432
+ # else:
433
+ # print("❌ Not writable")
434
+
435
+ # test_writable_dirs()
436
+ # # from huggingface_hub import login
437
+
438
+ # # # Load HF token and login
439
+ # hf_token = os.environ.get("HUGGINGFACE_HUB_TOKEN")
440
+ # # if hf_token:
441
+ # # login(token=hf_token, add_to_git_credential=False, write_permission=False)
442
+
443
+ # # === Load Model with CUDA if available and safe cache ===
444
+ # device = "cuda" if torch.cuda.is_available() else "cpu"
445
+ # print(f"🚀 Loading SentenceTransformer on: {device}")
446
+
447
+
448
+
449
+
450
+ # # === Define Cohorts ===
451
+ # COHORTS = {
452
+ # "consulting": "Management consulting, strategy, analytics, client interaction",
453
+ # "bfsi": "Banking, finance, investment analysis, risk, fintech",
454
+ # "sales": "Sales, business development, GTM strategy, CRM, channel sales",
455
+ # "it": "Software development, cloud, AI/ML, backend systems",
456
+ # "hr": "Human resources, L&D, talent acquisition, HRBP",
457
+ # "legal": "Contracts, litigation, compliance, intellectual property",
458
+ # "scm": "Logistics, procurement, inventory, operations, manufacturing",
459
+ # "bpo": "Customer service, support, inbound/outbound calling, operations"
460
+ # }
461
+
462
+
463
+ # # === Helper to extract text from PDF ===
464
+ # def extract_text(file):
465
+ # reader = PdfReader(file)
466
+ # return "\n".join([page.extract_text() or "" for page in reader.pages])
467
+
468
+ # # === Gradio UI function for resume match ===
469
+ # def match_resume(resume_pdf, job_description):
470
+ # text = extract_text(resume_pdf)
471
+ # resume_emb = model.encode(text, convert_to_tensor=True)
472
+ # jd_emb = model.encode(job_description, convert_to_tensor=True)
473
+ # score = util.cos_sim(jd_emb, resume_emb).item() * 100
474
+ # label = "✅ Strong Match" if score > 70 else "⚠️ Needs Tailoring"
475
+ # return f"Match Score: {round(score, 2)}%\n\n{label}"
476
+
477
+ # demo = gr.Interface(
478
+ # fn=match_resume,
479
+ # inputs=[
480
+ # gr.File(label="Upload Resume PDF", file_types=[".pdf"]),
481
+ # gr.Textbox(label="Paste Job Description", lines=6)
482
+ # ],
483
+ # outputs="text",
484
+ # title="🧠 Resume to JD Matcher",
485
+ # description="Upload your resume PDF and paste a job description to get a similarity score and feedback!"
486
+ # )
487
+
488
+ # # === FastAPI App ===
489
+ # fastapi_app = FastAPI()
490
+ # model = None
491
+ # COHORT_EMBEDDINGS = {}
492
+ # # === Load model eagerly before app starts ===
493
+ # print("🕐 Preloading SentenceTransformer model before app declaration...")
494
+
495
+ # model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2", use_auth_token=hf_token).to(device)
496
+ # COHORT_EMBEDDINGS = {
497
+ # k: model.encode(v, convert_to_tensor=True) for k, v in COHORTS.items()
498
+ # }
499
+
500
+ # print("✅ Model and cohort embeddings loaded.")
501
+
502
+ # fastapi_app.add_middleware(
503
+ # CORSMiddleware,
504
+ # allow_origins=["*"],
505
+ # allow_methods=["*"],
506
+ # allow_headers=["*"]
507
+ # )
508
+
509
+ # @fastapi_app.post("/debug")
510
+ # async def debug(request: Request):
511
+ # body = await request.body()
512
+ # return {"body": body.decode(), "headers": dict(request.headers)}
513
+
514
+
515
+
516
+ # @fastapi_app.get("/status")
517
+ # async def status():
518
+ # return {"model_loaded": model is not None}
519
+
520
+
521
+
522
+ # # === In-memory OTP store ===
523
+ # OTP_STORE = {}
524
+
525
+ # from pydantic import BaseModel
526
+
527
+ # class EmailRequest(BaseModel):
528
+ # email: str
529
+
530
+ # @fastapi_app.post("/send-otp")
531
+ # async def send_otp(request: EmailRequest):
532
+ # email = request.email
533
+ # otp = str(random.randint(100000, 999999))
534
+ # expiry = datetime.now() + timedelta(minutes=10)
535
+ # OTP_STORE[email] = (otp, expiry)
536
+
537
+ # msg = EmailMessage()
538
+ # msg["Subject"] = "Your ResumePilot Login OTP"
539
+ # msg["From"] = smtp_user
540
+ # msg["To"] = email
541
+ # msg.set_content(f"Your one-time password (OTP) is: {otp}. It will expire in 10 minutes.")
542
+
543
+ # try:
544
+ # with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
545
+ # smtp.login(smtp_user, smtp_pass)
546
+ # smtp.send_message(msg)
547
+ # return {"status": "sent"}
548
+ # except Exception as e:
549
+ # return JSONResponse({"status": "error", "error": str(e)}, status_code=500)
550
+
551
+ # from pydantic import BaseModel
552
+ # from fastapi.responses import JSONResponse
553
+ # from datetime import datetime
554
+ # import random
555
+
556
+ # class OTPVerifyRequest(BaseModel):
557
+ # email: str
558
+ # otp: str
559
+
560
+ # @fastapi_app.post("/verify-otp")
561
+ # async def verify_otp(request: OTPVerifyRequest):
562
+ # email = request.email
563
+ # otp = request.otp
564
+
565
+ # stored = OTP_STORE.get(email)
566
+ # if not stored:
567
+ # return JSONResponse({"error": "No OTP found"}, status_code=400)
568
 
569
+ # saved_otp, expiry = stored
570
+ # if datetime.now() > expiry:
571
+ # return JSONResponse({"error": "OTP expired"}, status_code=401)
572
 
573
+ # if otp != saved_otp:
574
+ # return JSONResponse({"error": "Invalid OTP"}, status_code=401)
575
 
576
+ # # OTP valid — return a token and email
577
+ # return {"token": f"token_{random.randint(100000, 999999)}", "email": email}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
 
 
579
 
580
+ # from fastapi import Form
581
+ # from email.message import EmailMessage
582
+ # import smtplib
583
+ # import secrets
584
+ # import os
585
+
586
+ # MAGIC_TOKENS = {} # In-memory token storage
587
 
588
+ # @fastapi_app.post("/send_magic_link")
589
+ # async def send_magic_link(email: str = Form(...)):
590
+ # username = email.split("@")[0]
591
+ # token = secrets.token_urlsafe(16)
592
+ # MAGIC_TOKENS[username] = token
593
 
594
+ # link = f"https://tendermatcher.tech/campus/?token={token}&user={username}"
595
+
596
+
597
+ # msg = EmailMessage()
598
+ # msg["Subject"] = "🔓 Your Magic Login Link"
599
+ # msg["From"] = os.environ["GMAIL_USER"]
600
+ # msg["To"] = email
601
+ # msg.set_content(f"Hi {username},\n\nClick here to log in:\n{link}\n\nCheers,\nResumePilot")
602
 
603
+ # with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
604
+ # smtp.login(os.environ["GMAIL_USER"], os.environ["GMAIL_APP_PASSWORD"])
605
+ # smtp.send_message(msg)
606
+
607
+ # return {"status": "sent", "token": token}
608
 
609
 
610
+ # @fastapi_app.post("/verify_magic_token")
611
+ # async def verify_magic_token(user: str = Form(...), token: str = Form(...)):
612
+ # stored_token = MAGIC_TOKENS.get(user)
613
+ # if not stored_token:
614
+ # return JSONResponse({"error": "No token found for user"}, status_code=400)
615
+ # if token != stored_token:
616
+ # return JSONResponse({"error": "Invalid token"}, status_code=401)
617
+
618
+ # # ✅ Valid magic link
619
+ # return {"status": "verified", "user": user}
620
+
621
+
622
+
623
+
624
+ # # === Resume Matcher endpoint ===
625
+ # @fastapi_app.post("/predict")
626
+ # async def predict(file: UploadFile, jd: str = Form(...), email: str = Form(...)):
627
+ # try:
628
+ # content = await file.read()
629
+ # pdf = io.BytesIO(content)
630
+ # text = extract_text(pdf)
631
+ # resume_emb = model.encode(text, convert_to_tensor=True)
632
+ # jd_emb = model.encode(jd, convert_to_tensor=True)
633
+ # score = util.cos_sim(jd_emb, resume_emb).item() * 100
634
+ # label = "Strong Match" if score > 70 else "Needs Tailoring"
635
+
636
+ # with tempfile.TemporaryDirectory() as tmpdir:
637
+ # upload_to_dropbox(content, f"/spc_cohort_data/{email}/resume.pdf")
638
+ # jd_data = json.dumps({"score": round(score, 2), "feedback": label}).encode("utf-8")
639
+ # upload_to_dropbox(jd_data, f"/spc_cohort_data/{email}/jd_match.json")
640
 
641
+ # return JSONResponse({"score": round(score, 2), "feedback": label, "device": device})
642
+ # except Exception as e:
643
+ # return JSONResponse({"error": str(e)}, status_code=500)
644
+
645
+ # # === Cohort Predictor endpoint ===
646
+ # @fastapi_app.post("/cohort")
647
+ # async def cohort_predict(name: str = Form(...), email: str = Form(...), summary: str = Form(...), quiz: str = Form(...)):
648
+ # try:
649
+ # combined = f"{name}\n{email}\n{summary}"
650
+ # user_emb = model.encode(combined, convert_to_tensor=True)
651
+ # scores = {cohort: util.cos_sim(user_emb, emb).item() for cohort, emb in COHORT_EMBEDDINGS.items()}
652
+ # predicted = max(scores, key=scores.get)
653
 
654
+ # with tempfile.TemporaryDirectory() as tmpdir:
655
+ # quiz_bytes = json.dumps(json.loads(quiz)).encode("utf-8")
656
+ # upload_to_dropbox(quiz_bytes, f"/spc_cohort_data/{email}/quiz.json")
657
 
658
+ # cohort_result = {
659
+ # "predicted_cohort": predicted,
660
+ # "scores": {k: round(v * 100, 2) for k, v in scores.items()}
661
+ # }
662
+ # upload_to_dropbox(json.dumps(cohort_result).encode("utf-8"), f"/spc_cohort_data/{email}/cohort.json")
663
 
664
+ # return JSONResponse(cohort_result)
665
+ # except Exception as e:
666
+ # return JSONResponse({"error": str(e)}, status_code=500)
667
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
668
 
669
+ # from dropbox.exceptions import ApiError
670
+ # import dropbox
671
 
672
+ # DROPBOX_ACCESS_TOKEN = os.getenv("DROPBOX_REFRESH_TOKEN")
673
+ # dbx = dropbox.Dropbox(DROPBOX_ACCESS_TOKEN)
674
+ # @fastapi_app.post("/save_profile")
675
+ # async def save_profile(request: Request):
676
+ # form = await request.form()
677
+ # email = form.get("email")
678
+ # full_name = form.get("full_name")
679
+ # job_title = form.get("job_title")
680
+
681
+ # if not email or not full_name or not job_title:
682
+ # return JSONResponse({"error": "Missing profile fields"}, status_code=400)
683
 
684
+ # path = get_profile_path(email)
 
 
 
 
 
 
 
685
 
686
+ # try:
687
+ # try:
688
+ # _, res = dbx.files_download(path)
689
+ # profile_data = json.loads(res.content)
690
+ # except ApiError:
691
+ # profile_data = {}
692
 
693
+ # profile_data.update({
694
+ # "full_name": full_name,
695
+ # "job_title": job_title
696
+ # })
697
+
698
+ # upload_to_dropbox(
699
+ # json.dumps(profile_data).encode(),
700
+ # path)
701
 
702
+ # return JSONResponse({"success": True})
703
+ # except Exception as e:
704
+ # return JSONResponse({"error": str(e)}, status_code=500)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
705
 
706
+ # @fastapi_app.get("/load_profile")
707
+ # async def load_profile(email: str):
708
+ # path = get_profile_path(email)
709
+
710
+ # try:
711
+ # _, res = dbx.files_download(path)
712
+ # profile_data = json.loads(res.content)
713
+ # return JSONResponse({
714
+ # "full_name": profile_data.get("full_name", ""),
715
+ # "job_title": profile_data.get("job_title", "")
716
+ # })
717
+ # except ApiError:
718
+ # return JSONResponse({"full_name": "", "job_title": ""})
719
+ # except Exception as e:
720
+ # return JSONResponse({"error": str(e)}, status_code=500)
721
+
722
+
723
+
724
+
725
+ # def get_profile_path(email: str):
726
+ # return f"/spc_cohort_data/{email}/profile.json"
727
+
728
+ # @fastapi_app.post("/save_theme")
729
+ # async def save_theme(request: Request):
730
+ # form = await request.form()
731
+ # email = form.get("email")
732
+ # theme = form.get("theme")
733
+
734
+ # if not email or not theme:
735
+ # return JSONResponse({"error": "Missing email or theme"}, status_code=400)
736
+
737
+ # path = get_profile_path(email)
738
+
739
+ # try:
740
+ # try:
741
+ # _, res = dbx.files_download(path)
742
+ # profile_data = json.loads(res.content)
743
+ # except ApiError:
744
+ # profile_data = {}
745
+
746
+ # profile_data["theme"] = theme
747
 
748
 
749
+ # upload_to_dropbox(
750
+ # json.dumps(profile_data).encode(),
751
+ # path
752
+ # )
753
+
754
+ # return JSONResponse({"success": True})
755
+ # except Exception as e:
756
+ # return JSONResponse({"error": str(e)}, status_code=500)
757
+
758
+ # @fastapi_app.get("/load_theme")
759
+ # async def load_theme(email: str):
760
+ # path = get_profile_path(email)
761
+
762
+ # try:
763
+ # _, res = dbx.files_download(path)
764
+ # profile_data = json.loads(res.content)
765
+ # return JSONResponse({"theme": profile_data.get("theme", "light")})
766
+ # except ApiError:
767
+ # return JSONResponse({"theme": "light"})
768
+ # except Exception as e:
769
+ # return JSONResponse({"error": str(e)}, status_code=500)
770
+
771
+
772
+
773
+
774
+ # # === Log Endpoint ===
775
+ # @fastapi_app.post("/log")
776
+ # async def receive_log(request: Request):
777
+ # try:
778
+ # payload = await request.json()
779
+ # timestamp = datetime.now(timezone.utc).astimezone(pytz.timezone("Asia/Kolkata")).isoformat()
780
+ # payload["logged_at"] = timestamp
781
+
782
+ # log_line = json.dumps(payload, ensure_ascii=False) + "\n"
783
+ # today = datetime.now().strftime("%Y-%m-%d")
784
+ # log_path = f"/spc_cohort_data/logs/{today}.jsonl"
785
+ # upload_to_dropbox(log_line.encode("utf-8"), log_path, append=True)
786
+
787
+ # return {"status": "logged"}
788
+ # except Exception as e:
789
+ # return JSONResponse({"error": str(e)}, status_code=500)
790
+
791
+ # # === List routes endpoint ===
792
+ # @fastapi_app.get("/routes")
793
+ # async def list_routes():
794
+ # return [
795
+ # {
796
+ # "path": route.path,
797
+ # "methods": list(route.methods),
798
+ # "name": route.name
799
+ # }
800
+ # for route in fastapi_app.routes
801
+ # if hasattr(route, "methods")
802
+ # ]
803
+
804
+ # @fastapi_app.get("/")
805
+ # async def root_redirect():
806
+ # return JSONResponse({"message": "Visit /ui for the resume matcher and API routes"})
807
+
808
+ # @fastapi_app.get("/health")
809
+ # async def health():
810
+ # return {"status": "ok"}
811
 
812
 
813
+ # # === Gradio UI for listing routes ===
814
+ # from fastapi.testclient import TestClient
815
+
816
+ # client = TestClient(fastapi_app)
817
+
818
+ # def get_routes_str():
819
+ # response = client.get("/routes")
820
+ # if response.status_code == 200:
821
+ # return json.dumps(response.json(), indent=2)
822
+ # else:
823
+ # return f"Error fetching routes: {response.status_code}"
824
+
825
+
826
+ # routes_demo = gr.Interface(
827
+ # fn=get_routes_str,
828
+ # inputs=[],
829
+ # outputs="textbox",
830
+ # title="API Routes",
831
+ # description="List of all API routes exposed by FastAPI backend"
832
+ # )
833
+
834
+ # with gr.Blocks() as ui:
835
+ # with gr.Tabs():
836
+ # with gr.TabItem("Resume Matcher"):
837
+ # demo.render()
838
+ # with gr.TabItem("API Routes"):
839
+ # routes_demo.render()
840
+
841
+
842
+ # # Mount the combined UI at /ui
843
+ # app = fastapi_app
844
+
845
+ # if __name__ == "__main__":
846
+ # import os
847
+ # import uvicorn
848
+ # ui.launch(server_name="0.0.0.0", server_port=7860)