ARJ3246 commited on
Commit
82a5fd6
Β·
1 Parent(s): 6f19d2d

Add AI Career Advisor application

Browse files
Files changed (5) hide show
  1. .gitignore +65 -0
  2. README.md +22 -8
  3. app.py +823 -0
  4. modal_agent_gemini.py +170 -0
  5. requirements.txt +4 -0
.gitignore ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual Environment
24
+ venv/
25
+ ENV/
26
+ env/
27
+ .venv/
28
+
29
+ # IDE specific files
30
+ .idea/
31
+ .vscode/
32
+ *.swp
33
+ *.swo
34
+
35
+ # Local development settings
36
+ .env
37
+ .env.local
38
+ .env.development.local
39
+ .env.test.local
40
+ .env.production.local
41
+
42
+ # Logs
43
+ *.log
44
+ npm-debug.log*
45
+ yarn-debug.log*
46
+ yarn-error.log*
47
+
48
+ # Temporary files
49
+ *.tmp
50
+ *.bak
51
+ *.swp
52
+ *~
53
+
54
+ # OS specific
55
+ .DS_Store
56
+ .DS_Store?
57
+ ._*
58
+ .Spotlight-V100
59
+ .Trashes
60
+ ehthumbs.db
61
+ Thumbs.db
62
+
63
+ # Project specific
64
+ *.db
65
+ *.sqlite3
README.md CHANGED
@@ -1,14 +1,28 @@
1
  ---
2
- title: Ai Career Advisor
3
- emoji: 🐒
4
- colorFrom: green
5
- colorTo: blue
6
  sdk: gradio
7
- sdk_version: 5.33.0
8
  app_file: app.py
9
  pinned: false
10
- license: mit
11
- short_description: '"An interactive AI Career Advisor powered by Gemini '
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: AI Career Advisor
3
+ emoji: 🎯
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 3.50.2
8
  app_file: app.py
9
  pinned: false
 
 
10
  ---
11
 
12
+ # AI Career Advisor
13
+
14
+ Get personalized career advice, skill-gap analysis, and a learning roadmap from an AI agent powered by Gemini. This application uses Gradio for a user-friendly interface and Modal for serverless AI processing.
15
+
16
+ ## Features
17
+ - Personalized career recommendations based on your profile
18
+ - Skill assessment with visual skill meters
19
+ - Recommended roles with match scores
20
+ - Customized learning path
21
+ - Project portfolio ideas
22
+ - Relevant certification recommendations
23
+
24
+ ## How to use
25
+ 1. Enter your bio or career goals
26
+ 2. Select your primary area of interest
27
+ 3. Optionally upload your resume (PDF or DOCX)
28
+ 4. Get comprehensive career guidance
app.py ADDED
@@ -0,0 +1,823 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import base64
4
+ import time
5
+ import re
6
+
7
+ # --- IMPORTANT ---
8
+ # Paste the web endpoint URL you got from deploying the Modal app here.
9
+ MODAL_WEB_ENDPOINT_URL = "https://aryanjathar0723--career-advisor-agent-gemini-web-endpoint.modal.run"
10
+
11
+ if MODAL_WEB_ENDPOINT_URL.startswith("https://your-org"):
12
+ print("="*80)
13
+ print("!!! WARNING: You have not replaced the placeholder MODAL_WEB_ENDPOINT_URL. !!!")
14
+ print("!!! Please deploy the modal_agent.py script and paste the URL in app.py. !!!")
15
+ print("="*80)
16
+
17
+ def extract_sections(markdown_text):
18
+ """Extract different sections from the markdown response"""
19
+ sections = {
20
+ 'summary': '',
21
+ 'roles': '',
22
+ 'skills': '',
23
+ 'learning': '',
24
+ 'projects': '',
25
+ 'certifications': ''
26
+ }
27
+
28
+ current_section = None
29
+ current_content = []
30
+
31
+ for line in markdown_text.split('\n'):
32
+ if line.startswith('### πŸ’«'):
33
+ current_section = 'summary'
34
+ current_content = []
35
+ elif line.startswith('### 🎯'):
36
+ current_section = 'roles'
37
+ current_content = []
38
+ elif line.startswith('### πŸ“Š'):
39
+ current_section = 'skills'
40
+ current_content = []
41
+ elif line.startswith('### πŸ“š'):
42
+ current_section = 'learning'
43
+ current_content = []
44
+ elif line.startswith('### πŸ’‘'):
45
+ current_section = 'projects'
46
+ current_content = []
47
+ elif line.startswith('### πŸŽ“'):
48
+ current_section = 'certifications'
49
+ current_content = []
50
+ # Skip roadmap section
51
+ elif line.startswith('### πŸ—ΊοΈ'):
52
+ current_section = None
53
+ current_content = []
54
+ elif current_section:
55
+ current_content.append(line)
56
+ sections[current_section] = '\n'.join(current_content)
57
+
58
+ return sections
59
+
60
+ def format_skill_bars(text):
61
+ """Convert skill meter text to HTML progress bars"""
62
+ formatted = []
63
+ in_skill_meter = False
64
+ for line in text.split('\n'):
65
+ if '```skill-meter' in line:
66
+ in_skill_meter = True
67
+ formatted.append('<div class="skill-bars">')
68
+ continue
69
+ elif '```' in line and in_skill_meter:
70
+ in_skill_meter = False
71
+ formatted.append('</div>')
72
+ continue
73
+
74
+ if in_skill_meter and '[' in line and ']' in line:
75
+ try:
76
+ skill_name, rest = line.split('[')
77
+ percentage = re.search(r'(\d+)%', rest)
78
+ if percentage:
79
+ pct = int(percentage.group(1))
80
+ formatted.append(f'''
81
+ <div class="skill-bar">
82
+ <div class="skill-name">{skill_name.strip()}</div>
83
+ <div class="progress-bar">
84
+ <div class="progress" style="width: {pct}%; background-color: #3498db;"></div>
85
+ </div>
86
+ <div class="percentage">{pct}%</div>
87
+ </div>
88
+ ''')
89
+ except:
90
+ formatted.append(line)
91
+ else:
92
+ formatted.append(line)
93
+
94
+ return '\n'.join(formatted)
95
+
96
+ def format_project_cards(text):
97
+ """Convert project-card text to HTML cards"""
98
+ formatted = []
99
+ in_project_card = False
100
+ current_card = {}
101
+
102
+ for line in text.split('\n'):
103
+ if '```project-card' in line:
104
+ in_project_card = True
105
+ current_card = {}
106
+ continue
107
+ elif '```' in line and in_project_card:
108
+ in_project_card = False
109
+ if current_card:
110
+ card_html = f'''
111
+ <div class="project-card">
112
+ <div>{current_card.get('project', 'Project')}</div>
113
+ <div><strong>Difficulty:</strong> {current_card.get('difficulty', '⭐⭐⭐')}</div>
114
+ <div><strong>Duration:</strong> {current_card.get('duration', '2 weeks')}</div>
115
+ <div><strong>Skills:</strong> {current_card.get('skills', 'Various skills')}</div>
116
+ <div><strong>Description:</strong> {current_card.get('description', 'Project description')}</div>
117
+ </div>
118
+ '''
119
+ formatted.append(card_html)
120
+ continue
121
+
122
+ if in_project_card:
123
+ line = line.strip()
124
+ if line.startswith('Project:'):
125
+ current_card['project'] = line[len('Project:'):].strip()
126
+ elif line.startswith('Difficulty:'):
127
+ current_card['difficulty'] = line[len('Difficulty:'):].strip()
128
+ elif line.startswith('Duration:'):
129
+ current_card['duration'] = line[len('Duration:'):].strip()
130
+ elif line.startswith('Skills:'):
131
+ current_card['skills'] = line[len('Skills:'):].strip()
132
+ elif line.startswith('Description:'):
133
+ current_card['description'] = line[len('Description:'):].strip()
134
+ else:
135
+ formatted.append(line)
136
+
137
+ return '\n'.join(formatted)
138
+
139
+ def format_roles(content):
140
+ """Format roles as cards"""
141
+ formatted_content = []
142
+ current_role = []
143
+ in_role = False
144
+
145
+ for line in content.split('\n'):
146
+ if line.strip().startswith('1.') or line.strip().startswith('2.') or line.strip().startswith('3.'):
147
+ if in_role:
148
+ formatted_content.append(format_role_card(current_role))
149
+ in_role = True
150
+ current_role = [line]
151
+ elif in_role and line.strip():
152
+ current_role.append(line)
153
+
154
+ if current_role:
155
+ formatted_content.append(format_role_card(current_role))
156
+
157
+ return '\n'.join(formatted_content)
158
+
159
+ def format_role_card(role_lines):
160
+ """Helper function to format role card"""
161
+ role_title = role_lines[0].strip()
162
+ # Extract role name and match score
163
+ role_name = ""
164
+ match_score = ""
165
+ if "**" in role_title:
166
+ parts = role_title.split("**")
167
+ if len(parts) > 1:
168
+ role_name = parts[1].strip()
169
+ if "(Match Score:" in role_title:
170
+ match_parts = role_title.split("(Match Score:")
171
+ if len(match_parts) > 1:
172
+ match_score = match_parts[1].split(")")[0].strip()
173
+
174
+ card_html = f'''
175
+ <div class="role-card">
176
+ <div class="role-title">{role_name} (Match Score: {match_score})</div>
177
+ '''
178
+
179
+ for line in role_lines[1:]:
180
+ line = line.strip()
181
+ if line.startswith('-'):
182
+ detail = line[1:].strip()
183
+ if "Salary Range:" in detail:
184
+ card_html += f'<div class="role-detail"><strong>Salary Range:</strong> {detail.split("Salary Range:")[1].strip()}</div>'
185
+ elif "Key Requirements:" in detail:
186
+ card_html += f'<div class="role-detail"><strong>Key Requirements:</strong> {detail.split("Key Requirements:")[1].strip()}</div>'
187
+ elif "Why It Fits:" in detail:
188
+ card_html += f'<div class="role-detail"><strong>Why It Fits:</strong> {detail.split("Why It Fits:")[1].strip()}</div>'
189
+ else:
190
+ card_html += f'<div class="role-detail">{detail}</div>'
191
+
192
+ card_html += '</div>'
193
+ return card_html
194
+
195
+ def format_certification_card(cert_lines):
196
+ """Helper function to format certification card"""
197
+ cert_name = cert_lines[0].replace('*', '').strip()
198
+ card_html = f'''
199
+ <div class="cert-card">
200
+ <div class="cert-name">{cert_name}</div>
201
+ '''
202
+
203
+ for line in cert_lines[1:]:
204
+ if line.strip():
205
+ line = line.strip().strip('*').strip('-').strip()
206
+ if "difficulty level:" in line.lower():
207
+ stars = line.split(":", 1)[1].strip() if ":" in line else ""
208
+ card_html += f'<div class="cert-detail">- Difficulty Level: {stars}</div>'
209
+ elif "time commitment:" in line.lower():
210
+ time = line.split(":", 1)[1].strip() if ":" in line else ""
211
+ card_html += f'<div class="cert-detail">- Time Commitment: {time}</div>'
212
+ elif "cost range:" in line.lower():
213
+ cost = line.split(":", 1)[1].strip() if ":" in line else ""
214
+ card_html += f'<div class="cert-detail">- Cost Range: {cost}</div>'
215
+ else:
216
+ card_html += f'<div class="cert-detail">β€’ {line}</div>'
217
+
218
+ card_html += '</div>'
219
+ return card_html
220
+
221
+ def parse_certifications(content):
222
+ """Parse certification content to group details by certification"""
223
+ lines = content.split('\n')
224
+ cert_groups = []
225
+ current_cert = []
226
+
227
+ # Check if content follows the format from the screenshot with dashes
228
+ has_cert_headers = any(line.strip().startswith('- ') for line in lines)
229
+
230
+ if has_cert_headers:
231
+ cert_name = ""
232
+ cert_details = []
233
+
234
+ for i, line in enumerate(lines):
235
+ line = line.strip()
236
+ if not line:
237
+ continue
238
+
239
+ if line.startswith('- ') and not any(detail in line.lower() for detail in ['difficulty level', 'time commitment', 'cost range']):
240
+ # This is a new certificate name
241
+ if cert_name:
242
+ # Save the previous certificate
243
+ cert_groups.append([f"* {cert_name}"] + cert_details)
244
+
245
+ cert_name = line[2:].strip()
246
+ cert_details = []
247
+ elif line.startswith('- '):
248
+ # This is a detail for the current certificate
249
+ cert_details.append(line)
250
+
251
+ # Add the last certificate
252
+ if cert_name:
253
+ cert_groups.append([f"* {cert_name}"] + cert_details)
254
+ else:
255
+ # Fall back to original parsing
256
+ in_cert = False
257
+ for line in lines:
258
+ if line.strip().startswith('*'):
259
+ if in_cert and current_cert:
260
+ cert_groups.append(current_cert)
261
+ in_cert = True
262
+ current_cert = [line]
263
+ elif in_cert and line.strip():
264
+ current_cert.append(line)
265
+
266
+ if current_cert:
267
+ cert_groups.append(current_cert)
268
+
269
+ return cert_groups
270
+
271
+ def format_sections(sections):
272
+ """Format all sections with proper styling"""
273
+ css = """
274
+ <style>
275
+ :root {
276
+ --bg-color: #1a1b26;
277
+ --text-color: #a9b1d6;
278
+ --heading-color: #7aa2f7;
279
+ --card-bg: #24283b;
280
+ --card-border: #414868;
281
+ --highlight: #bb9af7;
282
+ --progress-bg: #414868;
283
+ --progress-fill: linear-gradient(90deg, #7aa2f7, #bb9af7);
284
+ --card-hover: #2f3549;
285
+ }
286
+
287
+ body {
288
+ font-size: 16px;
289
+ line-height: 1.6;
290
+ background-color: var(--bg-color);
291
+ color: var(--text-color);
292
+ }
293
+
294
+ .skill-bars {
295
+ font-family: monospace;
296
+ margin: 25px 0;
297
+ font-size: 1.1em;
298
+ background-color: var(--card-bg);
299
+ padding: 20px;
300
+ border-radius: 12px;
301
+ border: 1px solid var(--card-border);
302
+ }
303
+
304
+ .skill-bar {
305
+ display: flex;
306
+ align-items: center;
307
+ margin: 15px 0;
308
+ gap: 15px;
309
+ }
310
+
311
+ .skill-name {
312
+ width: 180px;
313
+ text-align: right;
314
+ font-size: 1.1em;
315
+ color: var(--heading-color);
316
+ }
317
+
318
+ .progress-bar {
319
+ flex-grow: 1;
320
+ height: 25px;
321
+ background-color: var(--progress-bg);
322
+ border-radius: 12px;
323
+ overflow: hidden;
324
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
325
+ }
326
+
327
+ .progress {
328
+ height: 100%;
329
+ border-radius: 12px;
330
+ transition: width 0.5s ease-in-out;
331
+ background: var(--progress-fill);
332
+ }
333
+
334
+ .percentage {
335
+ width: 60px;
336
+ font-size: 1.1em;
337
+ font-weight: 500;
338
+ color: var(--text-color);
339
+ }
340
+
341
+ .project-card {
342
+ border: 1px solid var(--card-border);
343
+ padding: 25px;
344
+ margin: 20px 0;
345
+ border-radius: 12px;
346
+ background-color: var(--card-bg);
347
+ box-shadow: 0 4px 6px rgba(0,0,0,0.2);
348
+ font-size: 1.1em;
349
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
350
+ }
351
+
352
+ .project-card:hover {
353
+ transform: translateY(-2px);
354
+ box-shadow: 0 6px 12px rgba(0,0,0,0.3);
355
+ }
356
+
357
+ .project-card div {
358
+ margin: 10px 0;
359
+ line-height: 1.6;
360
+ }
361
+
362
+ .project-card strong {
363
+ color: var(--heading-color);
364
+ margin-right: 10px;
365
+ font-weight: 600;
366
+ }
367
+
368
+ .project-card div:first-child {
369
+ font-size: 1.2em;
370
+ color: var(--highlight);
371
+ margin-bottom: 15px;
372
+ font-weight: 500;
373
+ }
374
+
375
+ h3 {
376
+ font-size: 1.5em;
377
+ margin: 1.5em 0 1em;
378
+ color: var(--heading-color);
379
+ border-bottom: 2px solid var(--card-border);
380
+ padding-bottom: 0.5em;
381
+ }
382
+
383
+ p {
384
+ font-size: 1.1em;
385
+ line-height: 1.6;
386
+ color: var(--text-color);
387
+ margin: 1em 0;
388
+ }
389
+
390
+ ul, ol {
391
+ font-size: 1.1em;
392
+ line-height: 1.6;
393
+ padding-left: 1.5em;
394
+ margin: 1em 0;
395
+ }
396
+
397
+ li {
398
+ margin: 0.8em 0;
399
+ padding-left: 0.5em;
400
+ }
401
+
402
+ strong {
403
+ color: var(--highlight);
404
+ }
405
+
406
+ .role-card {
407
+ background-color: var(--card-bg);
408
+ border: 1px solid var(--card-border);
409
+ border-radius: 12px;
410
+ padding: 20px;
411
+ margin: 15px 0;
412
+ }
413
+
414
+ .role-title {
415
+ color: var(--highlight);
416
+ font-size: 1.2em;
417
+ margin-bottom: 10px;
418
+ }
419
+
420
+ .role-detail {
421
+ margin: 8px 0;
422
+ color: var(--text-color);
423
+ }
424
+
425
+ .bullet-point {
426
+ margin: 10px 0;
427
+ padding-left: 20px;
428
+ position: relative;
429
+ color: var(--text-color);
430
+ }
431
+
432
+ .bullet-point:before {
433
+ content: "β€’";
434
+ color: var(--highlight);
435
+ position: absolute;
436
+ left: 0;
437
+ font-size: 1.2em;
438
+ }
439
+
440
+ .content-line {
441
+ margin: 10px 0;
442
+ padding: 5px 0;
443
+ color: var(--text-color);
444
+ }
445
+
446
+ .content-line strong {
447
+ color: var(--highlight);
448
+ font-weight: 600;
449
+ }
450
+
451
+ .expandable-card {
452
+ border: 1px solid var(--card-border);
453
+ border-radius: 12px;
454
+ background-color: var(--card-bg);
455
+ margin: 15px 0;
456
+ overflow: hidden;
457
+ transition: all 0.3s ease;
458
+ }
459
+
460
+ .expandable-card .card-header {
461
+ padding: 15px 20px;
462
+ cursor: pointer;
463
+ display: flex;
464
+ justify-content: space-between;
465
+ align-items: center;
466
+ border-bottom: 1px solid var(--card-border);
467
+ }
468
+
469
+ .expandable-card .card-header:hover {
470
+ background-color: var(--card-hover);
471
+ }
472
+
473
+ .expandable-card .card-title {
474
+ color: var(--highlight);
475
+ font-size: 1.2em;
476
+ font-weight: 500;
477
+ }
478
+
479
+ .expandable-card .card-content {
480
+ padding: 20px;
481
+ display: none;
482
+ }
483
+
484
+ .expandable-card.expanded .card-content {
485
+ display: block;
486
+ }
487
+
488
+ .learning-card {
489
+ border: 1px solid var(--card-border);
490
+ border-radius: 12px;
491
+ background-color: var(--card-bg);
492
+ padding: 20px;
493
+ margin: 15px 0;
494
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
495
+ }
496
+
497
+ .learning-card:hover {
498
+ transform: translateY(-2px);
499
+ box-shadow: 0 6px 12px rgba(0,0,0,0.3);
500
+ }
501
+
502
+ .learning-card .month-range {
503
+ color: var(--highlight);
504
+ font-size: 1.2em;
505
+ margin-bottom: 10px;
506
+ font-weight: 500;
507
+ }
508
+
509
+ .learning-card .course-name {
510
+ color: var(--heading-color);
511
+ margin: 10px 0;
512
+ }
513
+
514
+ .learning-card .project-name {
515
+ color: var(--text-color);
516
+ margin: 10px 0;
517
+ }
518
+
519
+ .learning-card .outcome {
520
+ color: var(--text-color);
521
+ font-style: italic;
522
+ margin-top: 10px;
523
+ padding-top: 10px;
524
+ border-top: 1px solid var(--card-border);
525
+ }
526
+
527
+ .cert-card {
528
+ border: 1px solid var(--card-border);
529
+ border-radius: 12px;
530
+ background-color: var(--card-bg);
531
+ padding: 20px;
532
+ margin: 15px 0;
533
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
534
+ }
535
+
536
+ .cert-card:hover {
537
+ transform: translateY(-2px);
538
+ box-shadow: 0 6px 12px rgba(0,0,0,0.3);
539
+ }
540
+
541
+ .cert-card .cert-name {
542
+ color: var(--highlight);
543
+ font-size: 1.2em;
544
+ margin-bottom: 10px;
545
+ font-weight: 500;
546
+ }
547
+
548
+ .cert-card .cert-detail {
549
+ color: var(--text-color);
550
+ margin: 8px 0;
551
+ }
552
+ </style>
553
+
554
+ <script>
555
+ document.addEventListener('DOMContentLoaded', function() {
556
+ document.querySelectorAll('.expandable-card .card-header').forEach(header => {
557
+ header.addEventListener('click', () => {
558
+ const card = header.parentElement;
559
+ card.classList.toggle('expanded');
560
+ });
561
+ });
562
+ });
563
+ </script>
564
+ """
565
+
566
+ formatted = {}
567
+ for key, content in sections.items():
568
+ if key == 'skills':
569
+ formatted[key] = css + format_skill_bars(content)
570
+ elif key == 'summary':
571
+ # Format summary as an expandable card
572
+ lines = content.split('\n')
573
+ formatted_content = ['<div class="expandable-card expanded">']
574
+ formatted_content.append('<div class="card-header">')
575
+ formatted_content.append('<div class="card-title">Quick Summary</div>')
576
+ formatted_content.append('<div class="expand-icon">β–Ό</div>')
577
+ formatted_content.append('</div>')
578
+ formatted_content.append('<div class="card-content">')
579
+ for line in lines:
580
+ if line.strip():
581
+ formatted_content.append(f'<div class="content-line">{line}</div>')
582
+ formatted_content.append('</div>')
583
+ formatted_content.append('</div>')
584
+ formatted[key] = '\n'.join(formatted_content)
585
+ elif key == 'roles':
586
+ # Format roles as cards
587
+ formatted[key] = css + format_roles(content)
588
+ elif key == 'projects':
589
+ # Format projects as cards
590
+ formatted[key] = css + format_project_cards(content)
591
+ elif key == 'learning':
592
+ # Format learning path as cards
593
+ formatted_content = []
594
+ current_month = []
595
+ in_month = False
596
+
597
+ for line in content.split('\n'):
598
+ if line.strip().startswith('1.') or line.strip().startswith('2.') or line.strip().startswith('3.'):
599
+ if in_month:
600
+ card_content = '\n'.join(current_month)
601
+ month_range = current_month[0].split(':')[0].replace('*', '').strip()
602
+ card_html = f'''
603
+ <div class="learning-card">
604
+ <div class="month-range">{month_range}</div>
605
+ {format_learning_content(card_content)}
606
+ </div>
607
+ '''
608
+ formatted_content.append(card_html)
609
+ in_month = True
610
+ current_month = [line]
611
+ elif in_month:
612
+ current_month.append(line)
613
+
614
+ if current_month:
615
+ card_content = '\n'.join(current_month)
616
+ month_range = current_month[0].split(':')[0].replace('*', '').strip()
617
+ card_html = f'''
618
+ <div class="learning-card">
619
+ <div class="month-range">{month_range}</div>
620
+ {format_learning_content(card_content)}
621
+ </div>
622
+ '''
623
+ formatted_content.append(card_html)
624
+
625
+ formatted[key] = css + '\n'.join(formatted_content)
626
+ elif key == 'certifications':
627
+ # Format certifications as cards
628
+ formatted_content = []
629
+
630
+ # Parse certifications into groups
631
+ cert_groups = parse_certifications(content)
632
+
633
+ for cert_group in cert_groups:
634
+ formatted_content.append(format_certification_card(cert_group))
635
+
636
+ if not formatted_content:
637
+ # If no certifications were found, add a placeholder
638
+ formatted_content.append('<div class="cert-card"><div class="cert-name">No certifications found</div></div>')
639
+
640
+ formatted[key] = css + '\n'.join(formatted_content)
641
+ else:
642
+ formatted[key] = content
643
+
644
+ return formatted
645
+
646
+ def format_learning_content(content):
647
+ """Helper function to format learning card content"""
648
+ formatted = []
649
+ for line in content.split('\n'):
650
+ line = line.strip()
651
+ if 'Course:' in line:
652
+ course = line.split('Course:')[1].strip().strip('"')
653
+ formatted.append(f'<div class="course-name">πŸ“š Course: {course}</div>')
654
+ elif 'Project:' in line:
655
+ project = line.split('Project:')[1].strip().strip('"')
656
+ formatted.append(f'<div class="project-name">πŸ’» Project: {project}</div>')
657
+ elif 'Expected Outcome:' in line:
658
+ outcome = line.split('Expected Outcome:')[1].strip().strip('"')
659
+ formatted.append(f'<div class="outcome">🎯 Expected Outcome: {outcome}</div>')
660
+ return '\n'.join(formatted)
661
+
662
+ def get_advice_from_agent(bio, interest, resume_file):
663
+ """
664
+ This function prepares the data and calls the Modal backend.
665
+ """
666
+ if not bio or not interest:
667
+ sections = {
668
+ "summary": "Please provide your bio/goals and select an interest area.",
669
+ "roles": "", "skills": "", "learning": "",
670
+ "projects": "", "certifications": ""
671
+ }
672
+ formatted = format_sections(sections)
673
+ return [
674
+ formatted["summary"],
675
+ formatted["roles"],
676
+ formatted["skills"],
677
+ formatted["learning"],
678
+ formatted["projects"],
679
+ formatted["certifications"]
680
+ ]
681
+
682
+ # Show a thinking message immediately
683
+ sections = {
684
+ "summary": "πŸ€” Agent is thinking... Parsing your profile and crafting a response. This may take a moment.",
685
+ "roles": "", "skills": "", "learning": "",
686
+ "projects": "", "certifications": ""
687
+ }
688
+ formatted = format_sections(sections)
689
+ yield [
690
+ formatted["summary"],
691
+ formatted["roles"],
692
+ formatted["skills"],
693
+ formatted["learning"],
694
+ formatted["projects"],
695
+ formatted["certifications"]
696
+ ]
697
+
698
+ payload = {
699
+ "bio": bio,
700
+ "interest": interest,
701
+ }
702
+
703
+ # Handle the optional resume file
704
+ if resume_file is not None:
705
+ with open(resume_file.name, "rb") as f:
706
+ file_content = f.read()
707
+ encoded_file = base64.b64encode(file_content).decode("utf-8")
708
+ payload["resume"] = {
709
+ "name": resume_file.name,
710
+ "data": encoded_file
711
+ }
712
+
713
+ try:
714
+ print("Making request to Modal endpoint...")
715
+ response = requests.post(MODAL_WEB_ENDPOINT_URL, json=payload, timeout=120)
716
+ print(f"Response status code: {response.status_code}")
717
+ response.raise_for_status()
718
+
719
+ result = response.json()
720
+ print("Raw response from Modal:", result)
721
+
722
+ advice_text = result.get("advice", "")
723
+ print("Advice text:", advice_text[:200] + "..." if advice_text else "No advice text")
724
+
725
+ sections = extract_sections(advice_text)
726
+ print("Extracted sections:", sections.keys())
727
+
728
+ # Format sections with proper styling
729
+ formatted = format_sections(sections)
730
+
731
+ output = [
732
+ formatted["summary"],
733
+ formatted["roles"],
734
+ formatted["skills"],
735
+ formatted["learning"],
736
+ formatted["projects"],
737
+ formatted["certifications"]
738
+ ]
739
+ print("Returning output with lengths:", [len(str(x)) for x in output])
740
+ yield output
741
+
742
+ except Exception as e:
743
+ print(f"Error occurred: {str(e)}")
744
+ error_sections = {
745
+ "summary": f"An error occurred: {str(e)}",
746
+ "roles": "Error occurred",
747
+ "skills": "Error occurred",
748
+ "learning": "Error occurred",
749
+ "projects": "Error occurred",
750
+ "certifications": "Error occurred"
751
+ }
752
+ formatted = format_sections(error_sections)
753
+ yield [
754
+ formatted["summary"],
755
+ formatted["roles"],
756
+ formatted["skills"],
757
+ formatted["learning"],
758
+ formatted["projects"],
759
+ formatted["certifications"]
760
+ ]
761
+
762
+ # Define the Gradio UI using Blocks for custom layout
763
+ with gr.Blocks(theme=gr.themes.Soft(), title="AI Career Advisor") as demo:
764
+ gr.Markdown(
765
+ """
766
+ # 🎯 AI Career Advisor
767
+ Get personalized career advice, skill-gap analysis, and a learning roadmap from an AI agent.
768
+ """
769
+ )
770
+
771
+ with gr.Row():
772
+ # Left column for input
773
+ with gr.Column(scale=1):
774
+ gr.Markdown("### πŸ“ Your Profile")
775
+ user_bio = gr.Textbox(
776
+ label="Your Bio or Goal",
777
+ placeholder="e.g., I'm a 3rd-year IT student interested in Machine Learning and want to become an ML Engineer.",
778
+ lines=4
779
+ )
780
+ interest_area = gr.Dropdown(
781
+ label="Primary Area of Interest",
782
+ choices=["AI / Data Science", "Web Development", "Cybersecurity",
783
+ "Cloud Computing", "DevOps", "Game Development"]
784
+ )
785
+ resume_upload = gr.File(
786
+ label="Upload Resume (PDF or DOCX)",
787
+ file_types=[".pdf", ".docx"]
788
+ )
789
+ submit_btn = gr.Button("Get Career Advice", variant="primary")
790
+
791
+ # Right column for output
792
+ with gr.Column(scale=2):
793
+ with gr.Tabs():
794
+ with gr.Tab("πŸ“Š Overview"):
795
+ summary_md = gr.HTML()
796
+ roles_md = gr.HTML()
797
+
798
+ with gr.Tab("🎯 Skills & Learning"):
799
+ skills_md = gr.HTML()
800
+ learning_md = gr.HTML()
801
+
802
+ with gr.Tab("πŸ’‘ Projects"):
803
+ projects_md = gr.HTML()
804
+
805
+ with gr.Tab("πŸŽ“ Certifications"):
806
+ cert_md = gr.HTML()
807
+
808
+ submit_btn.click(
809
+ fn=get_advice_from_agent,
810
+ inputs=[user_bio, interest_area, resume_upload],
811
+ outputs=[summary_md, roles_md, skills_md, learning_md, projects_md, cert_md]
812
+ )
813
+
814
+ gr.Examples(
815
+ examples=[
816
+ ["I am a software developer with 5 years of experience in Java, and I want to transition into a DevOps role.", "DevOps", None],
817
+ ["I'm a final year marketing student fascinated by data. I know some basic Python and SQL and want a career that blends marketing with data analytics.", "AI / Data Science", None],
818
+ ],
819
+ inputs=[user_bio, interest_area, resume_upload]
820
+ )
821
+
822
+ if __name__ == "__main__":
823
+ demo.launch()
modal_agent_gemini.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import os
3
+ from typing import Optional
4
+
5
+ import docx
6
+ import fitz # PyMuPDF
7
+ import modal
8
+ import google.generativeai as genai
9
+
10
+ app = modal.App(
11
+ name="career-advisor-agent-gemini",
12
+ image=modal.Image.debian_slim().pip_install(
13
+ "google-generativeai",
14
+ "pymupdf<1.24.0",
15
+ "python-docx",
16
+ "fastapi[standard]"
17
+ ),
18
+ secrets=[modal.Secret.from_name("my-google-secret")],
19
+ )
20
+
21
+ def parse_resume(file_content: bytes, filename: str) -> str:
22
+ """Parses text from PDF or DOCX files."""
23
+ text = ""
24
+ try:
25
+ if filename.lower().endswith(".pdf"):
26
+ pdf_document = fitz.open(stream=file_content, filetype="pdf")
27
+ for page in pdf_document:
28
+ text += page.get_text()
29
+ pdf_document.close()
30
+ elif filename.lower().endswith(".docx"):
31
+ doc = docx.Document(io.BytesIO(file_content))
32
+ for para in doc.paragraphs:
33
+ text += para.text + "\n"
34
+ except Exception as e:
35
+ print(f"Error parsing file {filename}: {e}")
36
+ return f"Error parsing resume: {e}"
37
+ return text
38
+
39
+
40
+ @app.function(timeout=120)
41
+ def get_career_advice(
42
+ bio: str,
43
+ interest: str,
44
+ resume_text: Optional[str] = None,
45
+ ):
46
+ """The main agent function that queries the Gemini API."""
47
+ try:
48
+ # Configure the Gemini API client
49
+ genai.configure(api_key=os.environ["GOOGLE_API_KEY"])
50
+
51
+ # List available models to debug
52
+ try:
53
+ models = genai.list_models()
54
+ print("Available models:", [model.name for model in models])
55
+ except Exception as e:
56
+ print(f"Could not list models: {e}")
57
+
58
+ # Initialize the Generative Model with explicit model name
59
+ model = genai.GenerativeModel(model_name='gemini-2.0-flash')
60
+
61
+ prompt = f"""
62
+ You are an expert career advisor and coach. Your goal is to provide clear, actionable, and visually structured career advice.
63
+ Format your entire response in Markdown, making it highly visual and structured.
64
+
65
+ Here is the user's profile:
66
+ Primary Interest Area: {interest}
67
+ Bio: {bio}
68
+ Resume Content: {resume_text if resume_text else "No resume provided."}
69
+
70
+ Important Instructions:
71
+ 1. Focus PRIMARILY on the user's chosen interest area ({interest}). All advice should be specifically tailored to this field.
72
+ 2. If a resume is provided, analyze their current skills and experience to provide more personalized recommendations.
73
+ 3. Ensure all recommendations, courses, and projects are SPECIFICALLY relevant to {interest}.
74
+ 4. Use the resume content to identify transferable skills that would be valuable in {interest}.
75
+
76
+ Provide a structured response with the following sections:
77
+
78
+ ### πŸ’« Quick Summary
79
+ A brief 2-3 sentence overview focusing specifically on their potential in {interest}, highlighting relevant existing skills and clear next steps.
80
+
81
+ ### 🎯 Recommended Roles
82
+ Present 3 recommended roles IN THE {interest} FIELD ONLY in this format:
83
+ 1. **Role Name** (Match Score: X/10)
84
+ - Salary Range: $XX,XXX - $XXX,XXX
85
+ - Key Requirements: req1, req2, req3
86
+ - Why It Fits: Brief explanation based on their background
87
+
88
+ ### πŸ“Š Skills Assessment
89
+ Analyze their current skills relevant to {interest}. Use this format:
90
+ ```skill-meter
91
+ Current Skills Relevant to {interest}:
92
+ Skill Name [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘] 50%
93
+ ```
94
+ Include 4-5 most relevant skills for {interest}, showing both strengths and areas for improvement.
95
+
96
+ ### πŸ“š Learning Path
97
+ Present a structured timeline SPECIFIC to {interest}:
98
+ 1. **Month 1-2: Foundation**
99
+ - Course: "Course Name" (Platform) - Must be relevant to {interest}
100
+ - Project: "Project idea" - Must be relevant to {interest}
101
+ - Expected Outcome: "What they'll learn"
102
+
103
+ ### πŸ’‘ Project Portfolio
104
+ Present 3 project ideas AS CARDS that are SPECIFICALLY for {interest}:
105
+ ```project-card
106
+ Project: Name
107
+ Difficulty: β­β­β­β˜†β˜†
108
+ Duration: 2 weeks
109
+ Skills: skill1, skill2
110
+ Description: Brief description
111
+ ```
112
+
113
+ ### πŸŽ“ Certifications
114
+ List 2-3 recommended certifications SPECIFIC to {interest}:
115
+ - Certificate Name (Provider)
116
+ - Difficulty Level: β­β­β­β˜†β˜†
117
+ - Time Commitment: X-Y months
118
+ - Cost Range: $XXX (details)
119
+
120
+ Remember to:
121
+ - Keep ALL advice focused on {interest}
122
+ - Use their resume/background to make recommendations more relevant
123
+ - Be specific and actionable in all recommendations
124
+ - Use proper formatting for visual elements (skill bars, project cards, diagram)
125
+ """
126
+
127
+ try:
128
+ response = model.generate_content(prompt)
129
+ if response and hasattr(response, 'text'):
130
+ return response.text
131
+ else:
132
+ return "Error: Received empty or invalid response from Gemini API"
133
+ except Exception as e:
134
+ print(f"Generate content error: {str(e)}")
135
+ return f"Error generating content: {str(e)}"
136
+
137
+ except KeyError:
138
+ return "Error: GOOGLE_API_KEY not found in environment variables"
139
+ except Exception as e:
140
+ print(f"Unexpected error: {str(e)}")
141
+ return f"An unexpected error occurred: {str(e)}"
142
+
143
+
144
+ # --- FIX 1: Use the new decorator name ---
145
+ @app.function()
146
+ @modal.fastapi_endpoint(method="POST")
147
+ def web_endpoint(data: dict):
148
+ """
149
+ This is the web endpoint that our Gradio app will call.
150
+ """
151
+ try:
152
+ print("Received request with data:", data)
153
+ bio = data.get("bio")
154
+ interest = data.get("interest")
155
+ resume_data = data.get("resume")
156
+
157
+ resume_text = None
158
+ if resume_data:
159
+ import base64
160
+ file_content = base64.b64decode(resume_data["data"])
161
+ resume_text = parse_resume(file_content, resume_data["name"])
162
+ print("Parsed resume text length:", len(resume_text) if resume_text else 0)
163
+
164
+ # Call the main agent function
165
+ advice = get_career_advice.remote(bio, interest, resume_text)
166
+ print("Generated advice length:", len(advice) if advice else 0)
167
+ return {"advice": advice}
168
+ except Exception as e:
169
+ print(f"Error in web endpoint: {str(e)}")
170
+ return {"advice": f"Error occurred in web endpoint: {str(e)}"}
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio
2
+ requests
3
+ python-docx
4
+ pymupdf<1.24.0