yashgori20 commited on
Commit
492af8a
·
1 Parent(s): a49ee8d
Files changed (4) hide show
  1. enhancer_agent.py +86 -68
  2. evaluator_agent.py +4 -1
  3. linkedin_scraper.py +72 -41
  4. new_resume.py +124 -75
enhancer_agent.py CHANGED
@@ -19,11 +19,10 @@ def enhancer_agent(profile_data, evaluation_feedback, target_role):
19
  profile_sections = [
20
  "Headline",
21
  "Summary/About",
22
- "Current Position/Experience",
23
  "Education",
24
  "Skills",
25
  "Certifications",
26
-
27
  "Recommendations",
28
  ]
29
 
@@ -83,6 +82,8 @@ Explanation:
83
  temperature=0.3
84
  )
85
  agent_response = chat_completion.choices[0].message.content.strip()
 
 
86
  except Exception as e:
87
  st.error(f"❌ Groq API Error: {str(e)}")
88
  if "organization_restricted" in str(e):
@@ -117,7 +118,8 @@ Explanation:
117
  else:
118
  positions_list = []
119
 
120
- current_position = positions_list[0] if positions_list else '-'
 
121
 
122
  if isinstance(certifications_data, dict):
123
  certifications_list = certifications_data.get('certificationHistory', [])
@@ -129,9 +131,10 @@ Explanation:
129
 
130
  if isinstance(educations_data, dict):
131
  educations_list = educations_data.get('educationHistory', [])
132
- education = educations_list[0] if educations_list else '-'
 
133
  else:
134
- education = '-'
135
 
136
  if isinstance(recommendations_data, dict):
137
  recommendations_count = recommendations_data.get('recommendationsCount', 0)
@@ -143,8 +146,8 @@ Explanation:
143
  parsed_data = {
144
  'Headline': person.get('headline', '-') or '-',
145
  'Summary/About': summary,
146
- 'Current Position/Experience': current_position,
147
- 'Education': education,
148
  'Skills': skills_list,
149
  'Certifications': certifications_list,
150
  'Recommendations': recommendations_count,
@@ -181,58 +184,64 @@ Explanation:
181
  if not value or value == '-' or (isinstance(value, list) and not any(value)):
182
  st.markdown("_This section is currently missing or incomplete._")
183
  else:
184
- if key == "Current Position/Experience" and isinstance(value, dict):
185
- position = value
186
- start_end_date = position.get('startEndDate', {})
187
- start_date = start_end_date.get('start')
188
- end_date = start_end_date.get('end')
189
-
190
- if isinstance(start_date, dict):
191
- start_month = start_date.get('month', '-')
192
- start_year = start_date.get('year', '-')
193
- else:
194
- start_month = '-'
195
- start_year = '-'
196
-
197
- if isinstance(end_date, dict):
198
- end_month = end_date.get('month', '-')
199
- end_year = end_date.get('year', '-')
200
- else:
201
- end_month = 'Present'
202
- end_year = ''
203
-
204
- duration = f"{start_month}/{start_year} - {end_month}/{end_year}".strip()
205
- if duration == "-/- - Present/":
206
- duration = "Present"
207
-
208
- position_details = f"**Title:** {position.get('title', '-')}\n\n" \
209
- f"**Company Name:** {position.get('companyName', '-')}\n\n" \
210
- f"**Duration:** {duration}\n\n" \
211
- f"**Description:**\n{position.get('description', '-')}"
212
- st.markdown(position_details)
213
-
214
- elif key == "Education" and isinstance(value, dict):
215
- education = value
216
- start_end_date = education.get('startEndDate', {})
217
- start_date = start_end_date.get('start')
218
- end_date = start_end_date.get('end')
219
-
220
- if isinstance(start_date, dict):
221
- start_year = start_date.get('year', '-')
222
- else:
223
- start_year = '-'
224
-
225
- if isinstance(end_date, dict):
226
- end_year = end_date.get('year', '-')
227
- else:
228
- end_year = '-'
229
-
230
- duration = f"{start_year} - {end_year}".strip()
231
- education_details = f"**School Name:** {education.get('schoolName', '-')}\n\n" \
232
- f"**Degree Name:** {education.get('degreeName', '-')}\n\n" \
233
- f"**Field of Study:** {education.get('fieldOfStudy', '-')}\n\n" \
234
- f"**Duration:** {duration}"
235
- st.markdown(education_details)
 
 
 
 
 
 
236
 
237
  elif key == "Skills" and isinstance(value, list):
238
  st.markdown("\n".join([f"- {skill}" for skill in value]))
@@ -273,6 +282,8 @@ Enhanced Evaluation Feedback:
273
  temperature=0.3
274
  )
275
  enhanced_feedback = chat_completion.choices[0].message.content.strip()
 
 
276
  return enhanced_feedback
277
  except Exception as e:
278
  st.error(f"❌ Groq API Error: {str(e)}")
@@ -298,9 +309,12 @@ Enhanced Evaluation Feedback:
298
 
299
  for key in profile_sections:
300
  current_content = parsed_profile.get(key, '-')
301
- if key == "Current Position/Experience" and isinstance(current_content, dict):
302
- position = current_content
303
- current_content_str = f"Title: {position.get('title', '-')}\nCompany Name: {position.get('companyName', '-')}\nDescription: {position.get('description', '-')}"
 
 
 
304
  recommendation, explanation = get_section_recommendation_and_explanation(key, current_content_str)
305
  elif isinstance(current_content, list) and current_content != ['-']:
306
  current_content_str = ", ".join(current_content)
@@ -354,12 +368,14 @@ Enhanced Evaluation Feedback:
354
  profile_context = ""
355
  for key, value in parsed_profile.items():
356
  if value != '-' and value:
357
- if key == "Current Position/Experience" and isinstance(value, dict):
358
- position = value
359
- position_details = f"Title: {position.get('title', '-')}\n" \
360
- f"Company Name: {position.get('companyName', '-')}\n" \
361
- f"Description: {position.get('description', '-')}"
362
- profile_context += f"\n{key}:\n{position_details}"
 
 
363
  elif isinstance(value, list):
364
  profile_context += f"\n{key}:\n{', '.join(value)}"
365
  elif isinstance(value, int):
@@ -388,6 +404,8 @@ User's current profile data:
388
  )
389
 
390
  agent_response = chat_completion.choices[0].message.content.strip()
 
 
391
  st.session_state['conversation_history'].append({'role': 'assistant', 'content': agent_response})
392
 
393
  display_conversation()
 
19
  profile_sections = [
20
  "Headline",
21
  "Summary/About",
22
+ "Experience", # Changed from "Current Position/Experience" to "Experience"
23
  "Education",
24
  "Skills",
25
  "Certifications",
 
26
  "Recommendations",
27
  ]
28
 
 
82
  temperature=0.3
83
  )
84
  agent_response = chat_completion.choices[0].message.content.strip()
85
+ # Clean markdown formatting from GPT-OSS response
86
+ agent_response = agent_response.replace('**', '').replace('*', '')
87
  except Exception as e:
88
  st.error(f"❌ Groq API Error: {str(e)}")
89
  if "organization_restricted" in str(e):
 
118
  else:
119
  positions_list = []
120
 
121
+ # Get ALL positions, not just current one
122
+ all_positions = positions_list if positions_list else ['-']
123
 
124
  if isinstance(certifications_data, dict):
125
  certifications_list = certifications_data.get('certificationHistory', [])
 
131
 
132
  if isinstance(educations_data, dict):
133
  educations_list = educations_data.get('educationHistory', [])
134
+ # Get ALL education entries, not just the first one
135
+ all_education = educations_list if educations_list else ['-']
136
  else:
137
+ all_education = ['-']
138
 
139
  if isinstance(recommendations_data, dict):
140
  recommendations_count = recommendations_data.get('recommendationsCount', 0)
 
146
  parsed_data = {
147
  'Headline': person.get('headline', '-') or '-',
148
  'Summary/About': summary,
149
+ 'Experience': all_positions, # Changed from current_position to all_positions
150
+ 'Education': all_education, # Changed from single education to all_education
151
  'Skills': skills_list,
152
  'Certifications': certifications_list,
153
  'Recommendations': recommendations_count,
 
184
  if not value or value == '-' or (isinstance(value, list) and not any(value)):
185
  st.markdown("_This section is currently missing or incomplete._")
186
  else:
187
+ if key == "Experience" and isinstance(value, list) and value != ['-']:
188
+ # Display all experience entries
189
+ for i, position in enumerate(value):
190
+ st.markdown(f"### Experience {i+1}")
191
+ start_end_date = position.get('startEndDate', {})
192
+ start_date = start_end_date.get('start')
193
+ end_date = start_end_date.get('end')
194
+
195
+ if isinstance(start_date, dict):
196
+ start_month = start_date.get('month', '-')
197
+ start_year = start_date.get('year', '-')
198
+ else:
199
+ start_month = '-'
200
+ start_year = '-'
201
+
202
+ if isinstance(end_date, dict):
203
+ end_month = end_date.get('month', '-')
204
+ end_year = end_date.get('year', '-')
205
+ else:
206
+ end_month = 'Present'
207
+ end_year = ''
208
+
209
+ duration = f"{start_month}/{start_year} - {end_month}/{end_year}".strip()
210
+ if duration == "-/- - Present/":
211
+ duration = "Present"
212
+
213
+ position_details = f"**Title:** {position.get('title', '-')}\n\n" \
214
+ f"**Company Name:** {position.get('companyName', '-')}\n\n" \
215
+ f"**Duration:** {duration}\n\n" \
216
+ f"**Description:**\n{position.get('description', '-')}"
217
+ st.markdown(position_details)
218
+ st.markdown("---") # Separator between experiences
219
+
220
+ elif key == "Education" and isinstance(value, list) and value != ['-']:
221
+ # Display all education entries
222
+ for i, education in enumerate(value):
223
+ st.markdown(f"### Education {i+1}")
224
+ start_end_date = education.get('startEndDate', {})
225
+ start_date = start_end_date.get('start')
226
+ end_date = start_end_date.get('end')
227
+
228
+ if isinstance(start_date, dict):
229
+ start_year = start_date.get('year', '-')
230
+ else:
231
+ start_year = '-'
232
+
233
+ if isinstance(end_date, dict):
234
+ end_year = end_date.get('year', '-')
235
+ else:
236
+ end_year = '-'
237
+
238
+ duration = f"{start_year} - {end_year}".strip()
239
+ education_details = f"**School Name:** {education.get('schoolName', '-')}\n\n" \
240
+ f"**Degree Name:** {education.get('degreeName', '-')}\n\n" \
241
+ f"**Field of Study:** {education.get('fieldOfStudy', '-')}\n\n" \
242
+ f"**Duration:** {duration}"
243
+ st.markdown(education_details)
244
+ st.markdown("---") # Separator between education entries
245
 
246
  elif key == "Skills" and isinstance(value, list):
247
  st.markdown("\n".join([f"- {skill}" for skill in value]))
 
282
  temperature=0.3
283
  )
284
  enhanced_feedback = chat_completion.choices[0].message.content.strip()
285
+ # Clean markdown formatting from GPT-OSS response
286
+ enhanced_feedback = enhanced_feedback.replace('**', '').replace('*', '')
287
  return enhanced_feedback
288
  except Exception as e:
289
  st.error(f"❌ Groq API Error: {str(e)}")
 
309
 
310
  for key in profile_sections:
311
  current_content = parsed_profile.get(key, '-')
312
+ if key == "Experience" and isinstance(current_content, list) and current_content != ['-']:
313
+ # Handle multiple experience entries
314
+ experience_strs = []
315
+ for position in current_content:
316
+ experience_strs.append(f"Title: {position.get('title', '-')}\nCompany Name: {position.get('companyName', '-')}\nDescription: {position.get('description', '-')}")
317
+ current_content_str = "\n\n".join(experience_strs)
318
  recommendation, explanation = get_section_recommendation_and_explanation(key, current_content_str)
319
  elif isinstance(current_content, list) and current_content != ['-']:
320
  current_content_str = ", ".join(current_content)
 
368
  profile_context = ""
369
  for key, value in parsed_profile.items():
370
  if value != '-' and value:
371
+ if key == "Experience" and isinstance(value, list) and value != ['-']:
372
+ experience_details = []
373
+ for position in value:
374
+ position_detail = f"Title: {position.get('title', '-')}\n" \
375
+ f"Company Name: {position.get('companyName', '-')}\n" \
376
+ f"Description: {position.get('description', '-')}"
377
+ experience_details.append(position_detail)
378
+ profile_context += f"\n{key}:\n" + "\n\n".join(experience_details)
379
  elif isinstance(value, list):
380
  profile_context += f"\n{key}:\n{', '.join(value)}"
381
  elif isinstance(value, int):
 
404
  )
405
 
406
  agent_response = chat_completion.choices[0].message.content.strip()
407
+ # Clean markdown formatting from GPT-OSS response
408
+ agent_response = agent_response.replace('**', '').replace('*', '')
409
  st.session_state['conversation_history'].append({'role': 'assistant', 'content': agent_response})
410
 
411
  display_conversation()
evaluator_agent.py CHANGED
@@ -40,7 +40,10 @@ def evaluate_linkedin_profile(profile_data):
40
  max_tokens=30000,
41
  temperature=0.5
42
  )
43
- return completion.choices[0].message.content
 
 
 
44
  except Exception as e:
45
  st.error(f"❌ Groq API Error: {str(e)}")
46
  if "organization_restricted" in str(e):
 
40
  max_tokens=30000,
41
  temperature=0.5
42
  )
43
+ # Clean markdown formatting from GPT-OSS response
44
+ content = completion.choices[0].message.content
45
+ content = content.replace('**', '').replace('*', '') # Remove markdown formatting
46
+ return content
47
  except Exception as e:
48
  st.error(f"❌ Groq API Error: {str(e)}")
49
  if "organization_restricted" in str(e):
linkedin_scraper.py CHANGED
@@ -15,8 +15,18 @@ class LinkedInScraper:
15
  """
16
 
17
  def __init__(self):
18
- self.api_url = "https://api-f1db6c.stack.tryrelevance.com/latest/studios/11116e42-9be9-4837-8753-c46a80458318/trigger_webhook"
19
- self.project_id = "f56ec267-8285-4bef-b8ab-4dce36204e5d"
 
 
 
 
 
 
 
 
 
 
20
  self.headers = {
21
  "Content-Type": "application/json"
22
  }
@@ -34,39 +44,22 @@ class LinkedInScraper:
34
  except:
35
  return False
36
 
37
- def scrape_profile(self, linkedin_url: str) -> Dict[str, Any]:
38
  """
39
- Scrape a LinkedIn profile using the API
40
-
41
- Args:
42
- linkedin_url (str): LinkedIn profile URL
43
-
44
- Returns:
45
- Dict containing profile data or error information
46
  """
47
-
48
- # Validate URL
49
- if not self.is_valid_linkedin_url(linkedin_url):
50
- return {
51
- "success": False,
52
- "error": "Invalid LinkedIn URL format",
53
- "url": linkedin_url
54
- }
55
-
56
  try:
57
- # Prepare request
58
  payload = {"url": linkedin_url}
59
- full_url = f"{self.api_url}?project={self.project_id}"
60
 
61
- print(f"[SCRAPING] LinkedIn profile: {linkedin_url}")
62
  start_time = time.time()
63
 
64
- # Make API request
65
  response = requests.post(
66
  full_url,
67
  headers=self.headers,
68
  data=json.dumps(payload),
69
- timeout=60 # 60 second timeout
70
  )
71
 
72
  end_time = time.time()
@@ -75,11 +68,15 @@ class LinkedInScraper:
75
  if response.status_code == 200:
76
  data = response.json()
77
 
78
- # Check if data was successfully scraped
 
79
  if 'linkedin_full_data' in data:
80
  profile_data = data['linkedin_full_data']
81
-
82
- print(f"[SUCCESS] Scraped profile in {duration}s")
 
 
 
83
  print(f" Name: {profile_data.get('full_name', 'N/A')}")
84
  print(f" Headline: {profile_data.get('headline', 'N/A')}")
85
  print(f" Location: {profile_data.get('location', 'N/A')}")
@@ -88,19 +85,20 @@ class LinkedInScraper:
88
  "success": True,
89
  "data": profile_data,
90
  "scrape_time": duration,
91
- "url": linkedin_url
 
92
  }
93
  else:
94
  return {
95
  "success": False,
96
- "error": "No profile data returned from API",
97
  "raw_response": data,
98
  "url": linkedin_url
99
  }
100
  else:
101
  return {
102
  "success": False,
103
- "error": f"API request failed with status {response.status_code}",
104
  "response_text": response.text,
105
  "url": linkedin_url
106
  }
@@ -108,27 +106,60 @@ class LinkedInScraper:
108
  except requests.exceptions.Timeout:
109
  return {
110
  "success": False,
111
- "error": "Request timed out after 60 seconds",
112
- "url": linkedin_url
113
- }
114
- except requests.exceptions.RequestException as e:
115
- return {
116
- "success": False,
117
- "error": f"Request failed: {str(e)}",
118
  "url": linkedin_url
119
  }
120
- except json.JSONDecodeError as e:
121
  return {
122
  "success": False,
123
- "error": f"Failed to parse API response: {str(e)}",
124
  "url": linkedin_url
125
  }
126
- except Exception as e:
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  return {
128
  "success": False,
129
- "error": f"Unexpected error: {str(e)}",
130
  "url": linkedin_url
131
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
  def extract_key_info(self, profile_data: Dict[str, Any]) -> Dict[str, Any]:
134
  """
 
15
  """
16
 
17
  def __init__(self):
18
+ # Primary API (original)
19
+ self.primary_api = {
20
+ "url": "https://api-f1db6c.stack.tryrelevance.com/latest/studios/11116e42-9be9-4837-8753-c46a80458318/trigger_webhook",
21
+ "project_id": "f56ec267-8285-4bef-b8ab-4dce36204e5d"
22
+ }
23
+
24
+ # Fallback API (new account)
25
+ self.fallback_api = {
26
+ "url": "https://api-f1db6c.stack.tryrelevance.com/latest/studios/a1a00cf9-4102-4d76-99e5-8ce9b922b51c/trigger_webhook",
27
+ "project_id": "e5f9ef92-aa24-4626-a145-3fb746186504"
28
+ }
29
+
30
  self.headers = {
31
  "Content-Type": "application/json"
32
  }
 
44
  except:
45
  return False
46
 
47
+ def _try_api(self, api_config: Dict[str, str], linkedin_url: str, api_name: str) -> Dict[str, Any]:
48
  """
49
+ Try scraping with a specific API configuration
 
 
 
 
 
 
50
  """
 
 
 
 
 
 
 
 
 
51
  try:
 
52
  payload = {"url": linkedin_url}
53
+ full_url = f"{api_config['url']}?project={api_config['project_id']}"
54
 
55
+ print(f"[{api_name}] Trying to scrape: {linkedin_url}")
56
  start_time = time.time()
57
 
 
58
  response = requests.post(
59
  full_url,
60
  headers=self.headers,
61
  data=json.dumps(payload),
62
+ timeout=60
63
  )
64
 
65
  end_time = time.time()
 
68
  if response.status_code == 200:
69
  data = response.json()
70
 
71
+ # Handle different response formats from different APIs
72
+ profile_data = None
73
  if 'linkedin_full_data' in data:
74
  profile_data = data['linkedin_full_data']
75
+ elif 'data' in data:
76
+ profile_data = data['data']
77
+
78
+ if profile_data:
79
+ print(f"[{api_name}] SUCCESS in {duration}s")
80
  print(f" Name: {profile_data.get('full_name', 'N/A')}")
81
  print(f" Headline: {profile_data.get('headline', 'N/A')}")
82
  print(f" Location: {profile_data.get('location', 'N/A')}")
 
85
  "success": True,
86
  "data": profile_data,
87
  "scrape_time": duration,
88
+ "url": linkedin_url,
89
+ "api_used": api_name
90
  }
91
  else:
92
  return {
93
  "success": False,
94
+ "error": f"{api_name}: No profile data returned",
95
  "raw_response": data,
96
  "url": linkedin_url
97
  }
98
  else:
99
  return {
100
  "success": False,
101
+ "error": f"{api_name}: API returned status {response.status_code}",
102
  "response_text": response.text,
103
  "url": linkedin_url
104
  }
 
106
  except requests.exceptions.Timeout:
107
  return {
108
  "success": False,
109
+ "error": f"{api_name}: Request timed out after 60 seconds",
 
 
 
 
 
 
110
  "url": linkedin_url
111
  }
112
+ except Exception as e:
113
  return {
114
  "success": False,
115
+ "error": f"{api_name}: {str(e)}",
116
  "url": linkedin_url
117
  }
118
+
119
+ def scrape_profile(self, linkedin_url: str) -> Dict[str, Any]:
120
+ """
121
+ Scrape a LinkedIn profile using primary API with fallback
122
+
123
+ Args:
124
+ linkedin_url (str): LinkedIn profile URL
125
+
126
+ Returns:
127
+ Dict containing profile data or error information
128
+ """
129
+
130
+ # Validate URL
131
+ if not self.is_valid_linkedin_url(linkedin_url):
132
  return {
133
  "success": False,
134
+ "error": "Invalid LinkedIn URL format",
135
  "url": linkedin_url
136
  }
137
+
138
+ print(f"[SCRAPING] LinkedIn profile: {linkedin_url}")
139
+
140
+ # Try primary API first
141
+ result = self._try_api(self.primary_api, linkedin_url, "PRIMARY")
142
+
143
+ if result["success"]:
144
+ return result
145
+
146
+ print(f"[FALLBACK] Primary API failed: {result['error']}")
147
+ print(f"[FALLBACK] Trying secondary API...")
148
+
149
+ # Try fallback API
150
+ result = self._try_api(self.fallback_api, linkedin_url, "FALLBACK")
151
+
152
+ if result["success"]:
153
+ return result
154
+
155
+ # Both APIs failed
156
+ print(f"[FAILED] Both APIs failed!")
157
+ return {
158
+ "success": False,
159
+ "error": "Both primary and fallback APIs failed",
160
+ "primary_error": result.get('error', 'Unknown error'),
161
+ "url": linkedin_url
162
+ }
163
 
164
  def extract_key_info(self, profile_data: Dict[str, Any]) -> Dict[str, Any]:
165
  """
new_resume.py CHANGED
@@ -13,22 +13,52 @@ def generate_latex_resume(personal_info, education_list, experience_list, skills
13
  """Generate clean LaTeX code for resume"""
14
 
15
  def clean_text(text):
16
- """Clean text for LaTeX - simple and effective"""
17
  if not text:
18
  return ""
19
- # Handle common problematic characters
20
- text = str(text)
21
- text = text.replace('&', ' and ')
22
- text = text.replace('%', '\\%')
 
 
 
23
  text = text.replace('$', '\\$')
 
 
24
  text = text.replace('#', '\\#')
 
25
  text = text.replace('_', '\\_')
26
- text = text.replace('{', '\\{')
27
- text = text.replace('}', '\\}')
28
- # Remove problematic Unicode
29
- text = text.replace('\u202f', ' ')
30
- text = text.replace('\u2013', '-')
31
- text = text.replace('\u2014', '-')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  return text
33
 
34
  # Get clean data
@@ -328,88 +358,98 @@ def parse_uploaded_resume(uploaded_file):
328
  return None, f"Error parsing file: {str(e)}"
329
 
330
  def structure_resume_content(raw_content):
331
- """Use AI to structure raw resume content into organized sections"""
332
  try:
333
  client = Groq(api_key=Config.GROQ_API_KEY)
334
 
335
- prompt = f"""
336
- You are an AI assistant that extracts and structures resume information from raw text.
337
- Parse the following resume content and extract information into a structured format.
338
 
339
- Raw Resume Content:
340
  {raw_content}
341
 
342
- Please extract and return the information in this exact JSON format:
343
  {{
344
- "personal_info": {{
345
- "full_name": "extracted name",
346
- "email": "extracted email",
347
- "phone": "extracted phone",
348
- "address": "extracted address",
349
- "summary": "extracted professional summary or objective"
350
- }},
351
- "experience": [
352
- {{
353
- "job_title": "position title",
354
- "company": "company name",
355
- "location": "work location",
356
- "start_date": "start date",
357
- "end_date": "end date or Present",
358
- "responsibilities": ["responsibility 1", "responsibility 2", "responsibility 3"]
359
- }}
360
- ],
361
- "education": [
362
- {{
363
- "degree": "degree name",
364
- "university": "university name",
365
- "location": "university location",
366
- "graduation_date": "graduation date",
367
- "gpa": "GPA if mentioned"
368
- }}
369
- ],
370
- "skills": ["skill1", "skill2", "skill3"],
371
- "certifications": ["cert1", "cert2"],
372
- "projects": [
373
- {{
374
- "project_name": "project name",
375
- "description": "project description"
376
- }}
377
- ],
378
- "languages": ["language1", "language2"],
379
- "hobbies": ["hobby1", "hobby2"]
380
  }}
381
 
382
- Important:
383
- - If any section is not found, use empty arrays [] or empty strings ""
384
- - Extract actual content, don't make up information
385
- - For responsibilities, extract bullet points or key achievements
386
- - Keep the exact JSON format
387
- """
388
 
389
  response = client.chat.completions.create(
390
- messages=[{"role": "user", "content": prompt}],
 
 
 
391
  model="openai/gpt-oss-120b",
392
- temperature=0.1
 
393
  )
394
 
395
- # Parse the JSON response
396
- import json
397
- response_content = response.choices[0].message.content.strip()
398
 
399
- # Try to extract JSON from the response (in case there's extra text)
400
- json_start = response_content.find('{')
401
- json_end = response_content.rfind('}') + 1
402
 
403
- if json_start != -1 and json_end != -1:
404
- json_content = response_content[json_start:json_end]
405
- structured_data = json.loads(json_content)
406
- return structured_data
407
- else:
408
- raise ValueError("No valid JSON found in response")
409
 
 
 
 
 
410
  except Exception as e:
411
- st.error(f"Error structuring content: {str(e)}")
412
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
 
414
  def enhance_resume_with_ai(structured_data, target_role):
415
  """Use AI to enhance the structured resume data for the target role"""
@@ -451,6 +491,9 @@ Enhancement guidelines:
451
 
452
  response_content = response.choices[0].message.content.strip()
453
 
 
 
 
454
  # Extract enhanced data and summary
455
  if "ENHANCED_DATA:" in response_content and "ENHANCEMENT_SUMMARY:" in response_content:
456
  data_part = response_content.split("ENHANCED_DATA:")[1].split("ENHANCEMENT_SUMMARY:")[0].strip()
@@ -556,6 +599,12 @@ def display_editable_resume_data(structured_data):
556
  st.markdown(f"**Project #{i+1}**")
557
  proj_data = existing_projects[i] if i < len(existing_projects) else {}
558
 
 
 
 
 
 
 
559
  project = {
560
  'project_name': st.text_input(f"Project Name #{i+1}", value=proj_data.get('project_name', ''), key=f"edit_proj_name_{i}"),
561
  'description': st.text_area(f"Project Description #{i+1}", value=proj_data.get('description', ''), key=f"edit_proj_desc_{i}")
 
13
  """Generate clean LaTeX code for resume"""
14
 
15
  def clean_text(text):
16
+ """Clean text for LaTeX - PRODUCTION READY VERSION"""
17
  if not text:
18
  return ""
19
+
20
+ text = str(text).strip()
21
+
22
+ # Handle special LaTeX characters in EXACT correct order
23
+ text = text.replace('\\', '\\textbackslash{}') # MUST be first!
24
+ text = text.replace('{', '\\{')
25
+ text = text.replace('}', '\\}')
26
  text = text.replace('$', '\\$')
27
+ text = text.replace('&', '\\&')
28
+ text = text.replace('%', '\\%')
29
  text = text.replace('#', '\\#')
30
+ text = text.replace('^', '\\textasciicircum{}')
31
  text = text.replace('_', '\\_')
32
+ text = text.replace('~', '\\textasciitilde{}')
33
+
34
+ # Handle quotes properly for LaTeX
35
+ text = text.replace('"', "''")
36
+ text = text.replace('`', "'") # Prevent backtick issues
37
+
38
+ # Fix common Unicode characters that break LaTeX
39
+ unicode_fixes = {
40
+ '\u202f': ' ', # Narrow no-break space
41
+ '\u2013': '--', # En dash
42
+ '\u2014': '---', # Em dash
43
+ '\u2019': "'", # Right single quotation mark
44
+ '\u201c': '``', # Left double quotation mark
45
+ '\u201d': "''", # Right double quotation mark
46
+ '\u2026': '...', # Horizontal ellipsis
47
+ '\u00a0': ' ', # Non-breaking space
48
+ '\u2010': '-', # Hyphen
49
+ '\u2011': '-', # Non-breaking hyphen
50
+ '\u2012': '-', # Figure dash
51
+ }
52
+
53
+ for unicode_char, replacement in unicode_fixes.items():
54
+ text = text.replace(unicode_char, replacement)
55
+
56
+ # Convert to ASCII to remove any remaining problematic characters
57
+ text = text.encode('ascii', 'ignore').decode('ascii')
58
+
59
+ # Final cleanup - remove multiple spaces and trim
60
+ text = ' '.join(text.split())
61
+
62
  return text
63
 
64
  # Get clean data
 
358
  return None, f"Error parsing file: {str(e)}"
359
 
360
  def structure_resume_content(raw_content):
361
+ """Use AI to structure raw resume content into organized sections - IMPROVED VERSION"""
362
  try:
363
  client = Groq(api_key=Config.GROQ_API_KEY)
364
 
365
+ prompt = f"""Extract information from this resume and return ONLY a JSON object:
 
 
366
 
 
367
  {raw_content}
368
 
369
+ Return exactly this structure with real data from the resume:
370
  {{
371
+ "personal_info": {{
372
+ "full_name": "",
373
+ "email": "",
374
+ "phone": "",
375
+ "address": "",
376
+ "summary": ""
377
+ }},
378
+ "experience": [
379
+ {{
380
+ "job_title": "",
381
+ "company": "",
382
+ "start_date": "",
383
+ "end_date": "",
384
+ "responsibilities": []
385
+ }}
386
+ ],
387
+ "education": [
388
+ {{
389
+ "degree": "",
390
+ "institution": "",
391
+ "graduation_year": "",
392
+ "field": ""
393
+ }}
394
+ ],
395
+ "skills": [],
396
+ "projects": [],
397
+ "certifications": []
 
 
 
 
 
 
 
 
 
398
  }}
399
 
400
+ Rules:
401
+ - Return ONLY valid JSON, no markdown or explanations
402
+ - Use empty string "" for missing text fields
403
+ - Use empty array [] for missing list fields
404
+ - Extract real data only, don't invent anything"""
 
405
 
406
  response = client.chat.completions.create(
407
+ messages=[
408
+ {"role": "system", "content": "Extract resume data and return only valid JSON. No explanations, no markdown formatting."},
409
+ {"role": "user", "content": prompt}
410
+ ],
411
  model="openai/gpt-oss-120b",
412
+ temperature=0.0,
413
+ max_tokens=1500
414
  )
415
 
416
+ # Clean response
417
+ content = response.choices[0].message.content.strip()
418
+ content = content.replace('```json', '').replace('```', '').strip()
419
 
420
+ # Remove markdown formatting that GPT-OSS sometimes adds
421
+ content = content.replace('**', '') # Remove bold formatting
422
+ content = content.replace('*', '') # Remove italic formatting
423
 
424
+ # Parse JSON
425
+ import json
426
+ data = json.loads(content)
427
+ return data
 
 
428
 
429
+ except json.JSONDecodeError as e:
430
+ st.error(f"JSON parsing error: {str(e)}")
431
+ st.error("AI returned invalid JSON format")
432
+ return create_empty_resume_structure()
433
  except Exception as e:
434
+ st.error(f"Extraction error: {str(e)}")
435
+ return create_empty_resume_structure()
436
+
437
+ def create_empty_resume_structure():
438
+ """Create empty resume structure for fallback"""
439
+ return {
440
+ "personal_info": {
441
+ "full_name": "",
442
+ "email": "",
443
+ "phone": "",
444
+ "address": "",
445
+ "summary": ""
446
+ },
447
+ "experience": [],
448
+ "education": [],
449
+ "skills": [],
450
+ "projects": [],
451
+ "certifications": []
452
+ }
453
 
454
  def enhance_resume_with_ai(structured_data, target_role):
455
  """Use AI to enhance the structured resume data for the target role"""
 
491
 
492
  response_content = response.choices[0].message.content.strip()
493
 
494
+ # Clean markdown formatting from response
495
+ response_content = response_content.replace('**', '').replace('*', '')
496
+
497
  # Extract enhanced data and summary
498
  if "ENHANCED_DATA:" in response_content and "ENHANCEMENT_SUMMARY:" in response_content:
499
  data_part = response_content.split("ENHANCED_DATA:")[1].split("ENHANCEMENT_SUMMARY:")[0].strip()
 
599
  st.markdown(f"**Project #{i+1}**")
600
  proj_data = existing_projects[i] if i < len(existing_projects) else {}
601
 
602
+ # Handle case where project data might be a string instead of dict
603
+ if isinstance(proj_data, str):
604
+ proj_data = {'project_name': proj_data, 'description': ''}
605
+ elif not isinstance(proj_data, dict):
606
+ proj_data = {'project_name': '', 'description': ''}
607
+
608
  project = {
609
  'project_name': st.text_input(f"Project Name #{i+1}", value=proj_data.get('project_name', ''), key=f"edit_proj_name_{i}"),
610
  'description': st.text_area(f"Project Description #{i+1}", value=proj_data.get('description', ''), key=f"edit_proj_desc_{i}")