Cristian Martinez
commited on
Commit
Β·
fcd80f3
0
Parent(s):
Initial hackathon demo
Browse files- .gitignore +12 -0
- .python-version +1 -0
- README.md +0 -0
- app.py +125 -0
- pyproject.toml +12 -0
- requirements.txt +5 -0
.gitignore
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Python-generated files
|
2 |
+
__pycache__/
|
3 |
+
*.py[oc]
|
4 |
+
build/
|
5 |
+
dist/
|
6 |
+
wheels/
|
7 |
+
*.egg-info
|
8 |
+
|
9 |
+
# Virtual environments
|
10 |
+
.venv
|
11 |
+
uv.lock
|
12 |
+
.env
|
.python-version
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
3.12
|
README.md
ADDED
File without changes
|
app.py
ADDED
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app.py
|
2 |
+
|
3 |
+
import os, json
|
4 |
+
from dotenv import load_dotenv
|
5 |
+
from openai import OpenAI
|
6 |
+
from pypdf import PdfReader
|
7 |
+
import gradio as gr
|
8 |
+
import re, json
|
9 |
+
|
10 |
+
def extract_json(text: str) -> str:
|
11 |
+
"""(You already have this) Strip ``` fences so we can parse JSON."""
|
12 |
+
text = re.sub(r"```(?:json)?\s*", "", text)
|
13 |
+
text = re.sub(r"\s*```", "", text)
|
14 |
+
start = text.find("{"); end = text.rfind("}")
|
15 |
+
return text[start:end+1] if start!=-1 and end!=-1 else text
|
16 |
+
|
17 |
+
def format_results_as_md(res: dict) -> str:
|
18 |
+
"""Turn the parsed dict into a nice Markdown string."""
|
19 |
+
if "error" in res:
|
20 |
+
return f"**Error:** {res['error']}\n\n```\n{res.get('raw','')}\n```"
|
21 |
+
|
22 |
+
md = ["## π― Top Skill Gaps"]
|
23 |
+
for gap in res.get("skill_gaps", []):
|
24 |
+
md.append(f"- {gap}")
|
25 |
+
|
26 |
+
md.append("\n## π Course Recommendations")
|
27 |
+
for course in res.get("course_recommendations", []):
|
28 |
+
name = course.get("course_name")
|
29 |
+
provider = course.get("provider")
|
30 |
+
link = course.get("link")
|
31 |
+
md.append(f"- [{name}]({link}) _(Provider: {provider})_")
|
32 |
+
|
33 |
+
md.append("\n## π
Certification Recommendations")
|
34 |
+
for cert in res.get("certification_recommendations", []):
|
35 |
+
md.append(f"- {cert}")
|
36 |
+
|
37 |
+
return "\n".join(md)
|
38 |
+
|
39 |
+
# βββ 1) Load keys βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
40 |
+
|
41 |
+
load_dotenv() # expects OPENAI_API_KEY in your .env
|
42 |
+
openai = OpenAI()
|
43 |
+
|
44 |
+
# βββ 2) Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
45 |
+
|
46 |
+
def read_resume(file_obj):
|
47 |
+
"""Extract text from a PDF or return raw text."""
|
48 |
+
name = file_obj.name.lower()
|
49 |
+
if name.endswith(".pdf"):
|
50 |
+
reader = PdfReader(file_obj.name)
|
51 |
+
return "\n".join(page.extract_text() or "" for page in reader.pages)
|
52 |
+
else:
|
53 |
+
return file_obj.read().decode("utf-8")
|
54 |
+
|
55 |
+
def analyze_skills_and_recs(resume_text, titles_list):
|
56 |
+
prompt = {
|
57 |
+
"role":"user",
|
58 |
+
"content":(
|
59 |
+
"You are an expert career coach.\n\n"
|
60 |
+
"Here is a candidate's resume (first 2k chars):\n```\n"
|
61 |
+
+ resume_text[:2000] + "\n```\n\n"
|
62 |
+
"They want to apply for these roles:\n β’ "
|
63 |
+
+ "\n β’ ".join(titles_list) + "\n\n"
|
64 |
+
"Please reply _only_ in JSON with these fields:\n"
|
65 |
+
' "skill_gaps": [ "β¦" ],\n'
|
66 |
+
' "course_recommendations": [\n'
|
67 |
+
' { "course_name": "β¦", "provider": "β¦", "link": "β¦" }\n'
|
68 |
+
' ],\n'
|
69 |
+
' "certification_recommendations": [ "β¦" ]\n'
|
70 |
+
)
|
71 |
+
}
|
72 |
+
resp = openai.chat.completions.create(
|
73 |
+
model="gpt-4o-mini",
|
74 |
+
messages=[prompt]
|
75 |
+
)
|
76 |
+
raw = resp.choices[0].message.content
|
77 |
+
cleaned = extract_json(raw)
|
78 |
+
try:
|
79 |
+
data = json.loads(cleaned)
|
80 |
+
except json.JSONDecodeError:
|
81 |
+
return {
|
82 |
+
"error": "Still could not parse after stripping fences",
|
83 |
+
"raw": raw
|
84 |
+
}
|
85 |
+
return format_results_as_md(data)
|
86 |
+
|
87 |
+
# βββ 3) Pipeline ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
88 |
+
|
89 |
+
def run_agent(resume_file, titles_str):
|
90 |
+
resume = read_resume(resume_file)
|
91 |
+
titles = [t.strip() for t in titles_str.split(",") if t.strip()]
|
92 |
+
if not titles:
|
93 |
+
return {"error": "Please enter at least one job title."}
|
94 |
+
return analyze_skills_and_recs(resume, titles)
|
95 |
+
|
96 |
+
# βββ 4) Gradio UI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
97 |
+
|
98 |
+
app = gr.Blocks()
|
99 |
+
|
100 |
+
with app:
|
101 |
+
gr.Markdown("## π Skill Gap & Course Advisor")
|
102 |
+
gr.Markdown(
|
103 |
+
"Upload your resume, enter the roles you want (comma-separated), "
|
104 |
+
"and get back:\n"
|
105 |
+
"1. Your top skill gaps\n"
|
106 |
+
"2. 5 course recommendations (with names, providers, links)\n"
|
107 |
+
"3. 3 certification recommendations"
|
108 |
+
)
|
109 |
+
|
110 |
+
with gr.Row():
|
111 |
+
resume_input = gr.File(label="π Resume (PDF or TXT)")
|
112 |
+
titles_input = gr.Textbox(
|
113 |
+
label="π― Target Job Titles",
|
114 |
+
placeholder="e.g. Data Scientist, ML Engineer"
|
115 |
+
)
|
116 |
+
|
117 |
+
run_btn = gr.Button("π Analyze & Recommend")
|
118 |
+
output = gr.Markdown(label="π Recommendations")
|
119 |
+
run_btn.click(run_agent, [resume_input, titles_input], output)
|
120 |
+
|
121 |
+
if __name__ == "__main__":
|
122 |
+
# # Local:
|
123 |
+
# app.launch(server_name="0.0.0.0", server_port=7860, debug=True)
|
124 |
+
# For hackathon submission (MCP mode):
|
125 |
+
app.launch(mcp_server=True, server_name="0.0.0.0", server_port=7860)
|
pyproject.toml
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "hackathon-job-agent"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = "Add your description here"
|
5 |
+
readme = "README.md"
|
6 |
+
requires-python = ">=3.12"
|
7 |
+
dependencies = [
|
8 |
+
"gradio>=5.33.0",
|
9 |
+
"openai>=1.84.0",
|
10 |
+
"pypdf>=5.6.0",
|
11 |
+
"python-dotenv>=1.1.0",
|
12 |
+
]
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio
|
2 |
+
openai
|
3 |
+
pypdf
|
4 |
+
python-dotenv
|
5 |
+
requests
|