sid570 commited on
Commit
e2e309f
·
verified ·
1 Parent(s): d507277

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +133 -208
app.py CHANGED
@@ -1,160 +1,75 @@
1
  import os
2
  import json
3
- import re
4
  import requests
5
  import gradio as gr
6
- from pypdf import PdfReader
7
- from docx import Document
8
 
 
9
  JSEARCH_API_KEY = os.getenv("JSEARCH_API_KEY")
10
- HF_TOKEN = os.getenv("HF_TOKEN")
11
 
12
- QWEN_MODEL = "Qwen/Qwen2.5-1.5B-Instruct"
13
 
14
 
15
- def extract_text(file):
16
  if file is None:
17
- return ""
18
 
19
- path = file.name
20
 
21
- if path.lower().endswith(".pdf"):
22
- reader = PdfReader(path)
23
- text = ""
24
- for page in reader.pages:
25
- text += page.extract_text() or ""
26
- return text
27
 
28
- if path.lower().endswith(".docx"):
29
- doc = Document(path)
30
- return "\n".join([p.text for p in doc.paragraphs])
31
 
32
- if path.lower().endswith(".txt"):
33
- with open(path, "r", encoding="utf-8", errors="ignore") as f:
34
- return f.read()
 
 
 
 
 
 
35
 
36
- return ""
37
 
38
-
39
- def call_qwen(prompt):
40
- if not HF_TOKEN:
41
- return None
42
-
43
- api_url = f"https://api-inference.huggingface.co/models/{QWEN_MODEL}"
44
-
45
- headers = {
46
- "Authorization": f"Bearer {HF_TOKEN}",
47
- "Content-Type": "application/json"
48
- }
49
-
50
- payload = {
51
- "inputs": prompt,
52
- "parameters": {
53
- "max_new_tokens": 700,
54
- "temperature": 0.2,
55
- "return_full_text": False
56
- }
57
- }
58
-
59
- response = requests.post(api_url, headers=headers, json=payload, timeout=60)
60
-
61
- if response.status_code != 200:
62
- print("Qwen Error:", response.text)
63
- return None
64
-
65
- data = response.json()
66
-
67
- if isinstance(data, list) and len(data) > 0:
68
- return data[0].get("generated_text", "")
69
-
70
- return str(data)
71
-
72
-
73
- def extract_json(text):
74
- if not text:
75
- return None
76
-
77
- try:
78
- return json.loads(text)
79
- except:
80
- pass
81
-
82
- match = re.search(r"\{[\s\S]*\}", text)
83
-
84
- if match:
85
- try:
86
- return json.loads(match.group(0))
87
- except:
88
- return None
89
-
90
- return None
91
-
92
-
93
- def analyze_cv_with_ai(cv_text):
94
- prompt = f"""
95
- You are an AI career assistant.
96
-
97
- Analyze this CV and extract career information.
98
-
99
- Return valid JSON only. Do not add explanations.
100
-
101
- Use this exact format:
102
- {{
103
- "profile_level": "Entry-level",
104
  "technical_skills": [],
105
  "soft_skills": [],
106
- "suitable_roles": [],
107
- "job_search_keywords": []
108
- }}
109
-
110
- CV:
111
- {cv_text[:6000]}
 
 
 
 
 
112
  """
113
 
114
- result = call_qwen(prompt)
115
- print("RAW QWEN RESPONSE:", result)
116
-
117
- data = extract_json(result)
118
-
119
- if not data:
120
- data = {}
121
-
122
- technical_skills = data.get("technical_skills", [])
123
- suitable_roles = data.get("suitable_roles", [])
124
- job_search_keywords = data.get("job_search_keywords", [])
125
-
126
- fallback_keywords = []
127
-
128
- fallback_text = cv_text.lower()
129
-
130
- if "technical support" in fallback_text or "it support" in fallback_text:
131
- fallback_keywords.append("IT Support")
132
-
133
- if "linux" in fallback_text or "system administration" in fallback_text:
134
- fallback_keywords.append("System Administrator")
135
-
136
- if "sql" in fallback_text or "mysql" in fallback_text:
137
- fallback_keywords.append("Database")
138
-
139
- if "java" in fallback_text or "javascript" in fallback_text:
140
- fallback_keywords.append("Junior Software Developer")
141
-
142
- if "machine learning" in fallback_text or "nlp" in fallback_text:
143
- fallback_keywords.append("Machine Learning Intern")
144
-
145
- if not job_search_keywords:
146
- job_search_keywords = suitable_roles or fallback_keywords
147
 
148
- return {
149
- "profile_level": data.get("profile_level", "Entry-level"),
150
- "technical_skills": technical_skills,
151
- "soft_skills": data.get("soft_skills", []),
152
- "suitable_roles": suitable_roles,
153
- "job_search_keywords": job_search_keywords
154
- }
155
 
156
 
157
- def search_jobs(keyword, location):
158
  if not JSEARCH_API_KEY:
159
  return []
160
 
@@ -162,11 +77,11 @@ def search_jobs(keyword, location):
162
  "X-API-Key": JSEARCH_API_KEY
163
  }
164
 
165
- query = f"{keyword} jobs in {location}"
166
 
167
  response = requests.get(
168
  "https://api.openwebninja.com/jsearch/search-v2",
169
- params={"query": query},
170
  headers=headers,
171
  timeout=30
172
  )
@@ -177,26 +92,17 @@ def search_jobs(keyword, location):
177
 
178
  data = response.json()
179
 
180
- if "jobs" in data:
181
- return data["jobs"]
182
-
183
- if "data" in data:
184
- return data["data"]
185
-
186
- return []
187
 
188
 
189
- def rank_job_with_ai(cv_summary, job):
190
- title = job.get("title", "Unknown Job")
191
- company = job.get("company_name", job.get("company", "Unknown Company"))
192
- description = job.get("description", job.get("snippet", ""))
193
-
194
  prompt = f"""
195
  You are an AI job matching assistant.
196
 
197
- Compare the candidate profile with this job.
 
 
198
 
199
- Return JSON only:
200
  {{
201
  "match_score": 0,
202
  "reason": "",
@@ -205,108 +111,127 @@ Return JSON only:
205
  }}
206
 
207
  Candidate profile:
208
- {json.dumps(cv_summary)}
209
-
210
- Job title:
211
- {title}
212
-
213
- Company:
214
- {company}
215
 
216
- Job description:
217
- {description[:3000]}
218
  """
219
 
220
- result = call_qwen(prompt)
221
- data = extract_json(result)
 
 
 
 
 
 
222
 
223
- if not data:
 
 
224
  return {
225
  "match_score": 0,
226
- "reason": "Could not analyze this job.",
227
  "matched_skills": [],
228
  "missing_skills": []
229
  }
230
 
231
- return data
232
-
233
 
234
- def analyze_cv(file, location):
235
- cv_text = extract_text(file)
236
 
237
- if not cv_text.strip():
238
- return "Could not extract text from the CV."
239
 
240
- cv_summary = analyze_cv_with_ai(cv_text)
241
 
242
- technical_skills = cv_summary.get("technical_skills", [])
243
- suitable_roles = cv_summary.get("suitable_roles", [])
244
- keywords = cv_summary.get("job_search_keywords", [])
245
 
246
- output = "# Debug Output\n\n"
247
 
248
- output += "## Extracted CV Text Preview\n\n"
249
- output += "```text\n"
250
- output += cv_text[:1500]
251
- output += "\n```\n\n"
252
 
253
- output += "## AI Parsed CV Summary\n\n"
254
- output += "```json\n"
255
- output += json.dumps(cv_summary, indent=2)
256
- output += "\n```\n\n"
257
-
258
- output += "## Skills Detected\n\n"
259
- output += ", ".join(technical_skills) if technical_skills else "No technical skills detected"
260
  output += "\n\n"
261
 
262
- output += "## Suitable Roles Detected\n\n"
263
- output += ", ".join(suitable_roles) if suitable_roles else "No suitable roles detected"
264
  output += "\n\n"
265
 
266
- output += "## Keywords Passed to JSearch\n\n"
267
-
268
- search_terms = keywords or suitable_roles or technical_skills
269
-
270
- if not search_terms:
271
- output += "No keywords available for JSearch.\n\n"
272
- return output
273
 
274
- output += ", ".join(search_terms)
 
275
  output += "\n\n---\n\n"
276
 
277
  all_jobs = []
278
 
279
- for keyword in search_terms[:5]:
280
- output += f"Searching JSearch for: `{keyword} jobs in {location}`\n\n"
281
- jobs = search_jobs(keyword, location)
282
- output += f"Jobs found: {len(jobs)}\n\n"
283
  all_jobs.extend(jobs[:5])
284
 
285
  if not all_jobs:
286
- output += "No real jobs found from JSearch."
287
- return output
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
- output += "## Raw Jobs Found\n\n"
 
 
290
 
291
- for job in all_jobs[:10]:
292
  title = job.get("title", "Unknown Job")
293
  company = job.get("company_name", job.get("company", "Unknown Company"))
294
  job_location = job.get("location", "Not specified")
295
  url = job.get("url", job.get("apply_link", "#"))
296
 
297
- output += f"### {title}\n"
298
  output += f"**Company:** {company}\n\n"
299
  output += f"**Location:** {job_location}\n\n"
 
 
 
300
  output += f"[Apply Here]({url})\n\n---\n\n"
301
 
302
  return output
303
 
304
 
305
  demo = gr.Interface(
306
- fn=analyze_cv,
307
  inputs=[
308
  gr.File(
309
- label="Upload your CV",
310
  file_types=[".pdf", ".docx", ".txt"]
311
  ),
312
  gr.Textbox(
@@ -315,8 +240,8 @@ demo = gr.Interface(
315
  )
316
  ],
317
  outputs=gr.Markdown(label="Results"),
318
- title="JobFit AI",
319
- description="Upload your CV and get real job matches ranked using AI."
320
  )
321
 
322
 
 
1
  import os
2
  import json
 
3
  import requests
4
  import gradio as gr
5
+ from google import genai
6
+ from google.genai import types
7
 
8
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
9
  JSEARCH_API_KEY = os.getenv("JSEARCH_API_KEY")
 
10
 
11
+ client = genai.Client(api_key=GEMINI_API_KEY)
12
 
13
 
14
+ def extract_profile_with_gemini(file):
15
  if file is None:
16
+ return None, "No file uploaded."
17
 
18
+ uploaded_file = client.files.upload(file=file.name)
19
 
20
+ prompt = """
21
+ You are an AI resume analysis assistant.
 
 
 
 
22
 
23
+ Analyze this resume/CV from any career field.
 
 
24
 
25
+ Extract:
26
+ - candidate field or industry
27
+ - education
28
+ - work experience
29
+ - technical skills
30
+ - soft skills
31
+ - certifications
32
+ - suitable job titles
33
+ - job search queries
34
 
35
+ Return ONLY valid JSON in this exact structure:
36
 
37
+ {
38
+ "candidate_field": "",
39
+ "seniority_level": "",
40
+ "education": [],
41
+ "work_experience": [],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  "technical_skills": [],
43
  "soft_skills": [],
44
+ "certifications": [],
45
+ "suitable_job_titles": [],
46
+ "job_search_queries": []
47
+ }
48
+
49
+ Rules:
50
+ - The resume can be from any field: IT, finance, accounting, HR, marketing, design, engineering, healthcare, admin, hospitality, etc.
51
+ - Do not assume the candidate is in IT.
52
+ - Generate realistic job titles based on the resume.
53
+ - Generate 5 to 8 job_search_queries.
54
+ - Each query should be short and useful for a real job search.
55
  """
56
 
57
+ response = client.models.generate_content(
58
+ model="gemini-2.5-flash",
59
+ contents=[uploaded_file, prompt],
60
+ config=types.GenerateContentConfig(
61
+ temperature=0.2,
62
+ response_mime_type="application/json"
63
+ )
64
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
+ try:
67
+ return json.loads(response.text), response.text
68
+ except:
69
+ return None, response.text
 
 
 
70
 
71
 
72
+ def search_jobs(query, location):
73
  if not JSEARCH_API_KEY:
74
  return []
75
 
 
77
  "X-API-Key": JSEARCH_API_KEY
78
  }
79
 
80
+ full_query = f"{query} jobs in {location}"
81
 
82
  response = requests.get(
83
  "https://api.openwebninja.com/jsearch/search-v2",
84
+ params={"query": full_query},
85
  headers=headers,
86
  timeout=30
87
  )
 
92
 
93
  data = response.json()
94
 
95
+ return data.get("jobs", data.get("data", data.get("results", [])))
 
 
 
 
 
 
96
 
97
 
98
+ def rank_job_with_gemini(profile, job):
 
 
 
 
99
  prompt = f"""
100
  You are an AI job matching assistant.
101
 
102
+ Compare this candidate profile with the job.
103
+
104
+ Return ONLY valid JSON:
105
 
 
106
  {{
107
  "match_score": 0,
108
  "reason": "",
 
111
  }}
112
 
113
  Candidate profile:
114
+ {json.dumps(profile)}
 
 
 
 
 
 
115
 
116
+ Job:
117
+ {json.dumps(job)[:5000]}
118
  """
119
 
120
+ response = client.models.generate_content(
121
+ model="gemini-2.5-flash",
122
+ contents=prompt,
123
+ config=types.GenerateContentConfig(
124
+ temperature=0.2,
125
+ response_mime_type="application/json"
126
+ )
127
+ )
128
 
129
+ try:
130
+ return json.loads(response.text)
131
+ except:
132
  return {
133
  "match_score": 0,
134
+ "reason": "Could not rank this job.",
135
  "matched_skills": [],
136
  "missing_skills": []
137
  }
138
 
 
 
139
 
140
+ def analyze_resume(file, location):
141
+ profile, raw_response = extract_profile_with_gemini(file)
142
 
143
+ if not profile:
144
+ return f"Could not analyze resume.\n\nRaw Gemini response:\n\n```text\n{raw_response}\n```"
145
 
146
+ queries = profile.get("job_search_queries", [])
147
 
148
+ if not queries:
149
+ return "Gemini analyzed the resume but did not generate job search queries."
 
150
 
151
+ output = "# CareerMatch AI Results\n\n"
152
 
153
+ output += "## Resume Analysis\n\n"
154
+ output += f"**Field:** {profile.get('candidate_field', 'Not detected')}\n\n"
155
+ output += f"**Seniority Level:** {profile.get('seniority_level', 'Not detected')}\n\n"
 
156
 
157
+ output += "**Technical Skills:** "
158
+ output += ", ".join(profile.get("technical_skills", [])) or "Not detected"
 
 
 
 
 
159
  output += "\n\n"
160
 
161
+ output += "**Soft Skills:** "
162
+ output += ", ".join(profile.get("soft_skills", [])) or "Not detected"
163
  output += "\n\n"
164
 
165
+ output += "**Suitable Job Titles:** "
166
+ output += ", ".join(profile.get("suitable_job_titles", [])) or "Not detected"
167
+ output += "\n\n"
 
 
 
 
168
 
169
+ output += "**Search Queries Used:** "
170
+ output += ", ".join(queries)
171
  output += "\n\n---\n\n"
172
 
173
  all_jobs = []
174
 
175
+ for query in queries[:5]:
176
+ jobs = search_jobs(query, location)
 
 
177
  all_jobs.extend(jobs[:5])
178
 
179
  if not all_jobs:
180
+ return output + "No jobs found from JSearch."
181
+
182
+ unique_jobs = []
183
+ seen = set()
184
+
185
+ for job in all_jobs:
186
+ title = job.get("title", "")
187
+ company = job.get("company_name", job.get("company", ""))
188
+ key = f"{title}-{company}"
189
+
190
+ if key not in seen:
191
+ seen.add(key)
192
+ unique_jobs.append(job)
193
+
194
+ ranked_jobs = []
195
+
196
+ for job in unique_jobs[:8]:
197
+ match = rank_job_with_gemini(profile, job)
198
+ ranked_jobs.append({
199
+ "job": job,
200
+ "match": match
201
+ })
202
+
203
+ ranked_jobs.sort(
204
+ key=lambda x: int(x["match"].get("match_score", 0)),
205
+ reverse=True
206
+ )
207
+
208
+ output += "## Real Job Matches\n\n"
209
 
210
+ for item in ranked_jobs:
211
+ job = item["job"]
212
+ match = item["match"]
213
 
 
214
  title = job.get("title", "Unknown Job")
215
  company = job.get("company_name", job.get("company", "Unknown Company"))
216
  job_location = job.get("location", "Not specified")
217
  url = job.get("url", job.get("apply_link", "#"))
218
 
219
+ output += f"### {title} — {match.get('match_score', 0)}% Match\n\n"
220
  output += f"**Company:** {company}\n\n"
221
  output += f"**Location:** {job_location}\n\n"
222
+ output += f"**Why it matches:** {match.get('reason', 'Not provided')}\n\n"
223
+ output += f"**Matched Skills:** {', '.join(match.get('matched_skills', [])) or 'Not specified'}\n\n"
224
+ output += f"**Missing Skills:** {', '.join(match.get('missing_skills', [])) or 'None listed'}\n\n"
225
  output += f"[Apply Here]({url})\n\n---\n\n"
226
 
227
  return output
228
 
229
 
230
  demo = gr.Interface(
231
+ fn=analyze_resume,
232
  inputs=[
233
  gr.File(
234
+ label="Upload Resume/CV",
235
  file_types=[".pdf", ".docx", ".txt"]
236
  ),
237
  gr.Textbox(
 
240
  )
241
  ],
242
  outputs=gr.Markdown(label="Results"),
243
+ title="CareerMatch AI",
244
+ description="Upload any resume and get real job matches based on the candidate profile."
245
  )
246
 
247