Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import time
|
3 |
+
import tempfile
|
4 |
+
import gradio as gr
|
5 |
+
import warnings
|
6 |
+
from pathlib import Path
|
7 |
+
import PyPDF2
|
8 |
+
import markdown
|
9 |
+
from datetime import datetime, timedelta
|
10 |
+
from collections import defaultdict
|
11 |
+
import threading
|
12 |
+
|
13 |
+
warnings.filterwarnings('ignore')
|
14 |
+
|
15 |
+
# Rate limiting
|
16 |
+
class RateLimiter:
|
17 |
+
def __init__(self, max_requests=5, time_window=60):
|
18 |
+
self.max_requests = max_requests
|
19 |
+
self.time_window = time_window
|
20 |
+
self.requests = defaultdict(list)
|
21 |
+
self.lock = threading.Lock()
|
22 |
+
|
23 |
+
def is_allowed(self, user_id):
|
24 |
+
with self.lock:
|
25 |
+
now = datetime.now()
|
26 |
+
# Clean old requests
|
27 |
+
self.requests[user_id] = [
|
28 |
+
req_time for req_time in self.requests[user_id]
|
29 |
+
if now - req_time < timedelta(seconds=self.time_window)
|
30 |
+
]
|
31 |
+
|
32 |
+
if len(self.requests[user_id]) >= self.max_requests:
|
33 |
+
return False
|
34 |
+
|
35 |
+
self.requests[user_id].append(now)
|
36 |
+
return True
|
37 |
+
|
38 |
+
# Global rate limiter
|
39 |
+
rate_limiter = RateLimiter(max_requests=3, time_window=300) # 3 requests per 5 minutes
|
40 |
+
|
41 |
+
def extract_text_from_pdf(pdf_file):
|
42 |
+
"""Extract text from uploaded PDF file."""
|
43 |
+
try:
|
44 |
+
reader = PyPDF2.PdfReader(pdf_file)
|
45 |
+
text = ""
|
46 |
+
for page in reader.pages:
|
47 |
+
text += page.extract_text() + "\n"
|
48 |
+
return text.strip()
|
49 |
+
except Exception as e:
|
50 |
+
return f"Error reading PDF: {str(e)}"
|
51 |
+
|
52 |
+
def setup_crewai():
|
53 |
+
"""Initialize CrewAI components."""
|
54 |
+
try:
|
55 |
+
from crewai import Agent, Task, Crew
|
56 |
+
from crewai_tools import ScrapeWebsiteTool, SerperDevTool
|
57 |
+
from langchain_openai import ChatOpenAI
|
58 |
+
|
59 |
+
# Initialize tools
|
60 |
+
search_tool = SerperDevTool()
|
61 |
+
scrape_tool = ScrapeWebsiteTool()
|
62 |
+
|
63 |
+
# Initialize LLM
|
64 |
+
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
|
65 |
+
|
66 |
+
# Create agents
|
67 |
+
researcher = Agent(
|
68 |
+
role="Job Requirements Analyst",
|
69 |
+
goal="Extract and analyze key job requirements efficiently",
|
70 |
+
tools=[scrape_tool, search_tool],
|
71 |
+
verbose=False,
|
72 |
+
backstory="Expert at quickly identifying essential job requirements and qualifications from job postings.",
|
73 |
+
llm=llm,
|
74 |
+
)
|
75 |
+
|
76 |
+
resume_strategist = Agent(
|
77 |
+
role="Resume Enhancement Specialist",
|
78 |
+
goal="Optimize resumes to match job requirements effectively",
|
79 |
+
tools=[],
|
80 |
+
verbose=False,
|
81 |
+
backstory="Skilled at tailoring resumes to highlight relevant experience and skills for specific job applications.",
|
82 |
+
llm=llm,
|
83 |
+
)
|
84 |
+
|
85 |
+
return researcher, resume_strategist, llm
|
86 |
+
|
87 |
+
except ImportError:
|
88 |
+
raise Exception("CrewAI not installed. Please install required packages.")
|
89 |
+
|
90 |
+
def create_tasks(researcher, resume_strategist, job_url, resume_text):
|
91 |
+
"""Create optimized tasks for the crew."""
|
92 |
+
from crewai import Task
|
93 |
+
|
94 |
+
# Research task - focused and efficient
|
95 |
+
research_task = Task(
|
96 |
+
description=f"""
|
97 |
+
Analyze the job posting at {job_url} and extract the top 10 most important:
|
98 |
+
1. Required skills and technologies
|
99 |
+
2. Key qualifications and experience levels
|
100 |
+
3. Preferred background and certifications
|
101 |
+
|
102 |
+
Focus on the most critical requirements only.
|
103 |
+
""",
|
104 |
+
expected_output="A concise list of the top 10 most important job requirements.",
|
105 |
+
agent=researcher,
|
106 |
+
)
|
107 |
+
|
108 |
+
# Resume optimization task
|
109 |
+
resume_task = Task(
|
110 |
+
description=f"""
|
111 |
+
Using the job requirements from the research task, optimize this resume:
|
112 |
+
|
113 |
+
{resume_text}
|
114 |
+
|
115 |
+
Instructions:
|
116 |
+
1. Rewrite the professional summary to align with the job
|
117 |
+
2. Highlight relevant experience and skills
|
118 |
+
3. Adjust technical skills section to match requirements
|
119 |
+
4. Ensure ATS-friendly formatting
|
120 |
+
5. Keep the same factual information but present it strategically
|
121 |
+
|
122 |
+
Return the complete optimized resume in markdown format.
|
123 |
+
""",
|
124 |
+
expected_output="A complete, optimized resume in markdown format tailored to the job requirements.",
|
125 |
+
agent=resume_strategist,
|
126 |
+
context=[research_task]
|
127 |
+
)
|
128 |
+
|
129 |
+
return research_task, resume_task
|
130 |
+
|
131 |
+
def process_application(pdf_file, job_url, user_session):
|
132 |
+
"""Main processing function with rate limiting."""
|
133 |
+
|
134 |
+
# Rate limiting check
|
135 |
+
if not rate_limiter.is_allowed(user_session):
|
136 |
+
return "β οΈ Rate limit exceeded. Please wait 5 minutes before submitting another request.", ""
|
137 |
+
|
138 |
+
if not pdf_file or not job_url:
|
139 |
+
return "β Please provide both a PDF resume and job URL.", ""
|
140 |
+
|
141 |
+
try:
|
142 |
+
# Extract text from PDF
|
143 |
+
with gr.Progress() as progress:
|
144 |
+
progress(0.1, desc="Extracting text from PDF...")
|
145 |
+
resume_text = extract_text_from_pdf(pdf_file)
|
146 |
+
|
147 |
+
if "Error reading PDF" in resume_text:
|
148 |
+
return f"β {resume_text}", ""
|
149 |
+
|
150 |
+
progress(0.3, desc="Setting up AI agents...")
|
151 |
+
researcher, resume_strategist, llm = setup_crewai()
|
152 |
+
|
153 |
+
progress(0.5, desc="Creating optimization tasks...")
|
154 |
+
research_task, resume_task = create_tasks(researcher, resume_strategist, job_url, resume_text)
|
155 |
+
|
156 |
+
progress(0.7, desc="Analyzing job requirements...")
|
157 |
+
# Execute tasks
|
158 |
+
from crewai import Crew
|
159 |
+
crew = Crew(
|
160 |
+
agents=[researcher, resume_strategist],
|
161 |
+
tasks=[research_task, resume_task],
|
162 |
+
verbose=False
|
163 |
+
)
|
164 |
+
|
165 |
+
progress(0.9, desc="Generating tailored resume...")
|
166 |
+
result = crew.kickoff()
|
167 |
+
|
168 |
+
progress(1.0, desc="Complete!")
|
169 |
+
|
170 |
+
# Convert markdown to HTML for better display
|
171 |
+
html_result = markdown.markdown(str(result))
|
172 |
+
|
173 |
+
return "β
Resume successfully tailored!", html_result
|
174 |
+
|
175 |
+
except Exception as e:
|
176 |
+
return f"β Error processing your request: {str(e)}", ""
|
177 |
+
|
178 |
+
def create_interface():
|
179 |
+
"""Create the Gradio interface."""
|
180 |
+
|
181 |
+
with gr.Blocks(
|
182 |
+
title="CV Tailor - AI Resume Optimizer",
|
183 |
+
theme=gr.themes.Soft(),
|
184 |
+
css="""
|
185 |
+
.gradio-container {
|
186 |
+
max-width: 1200px;
|
187 |
+
margin: auto;
|
188 |
+
}
|
189 |
+
.header {
|
190 |
+
text-align: center;
|
191 |
+
margin-bottom: 30px;
|
192 |
+
}
|
193 |
+
.rate-limit-info {
|
194 |
+
background-color: #f0f8ff;
|
195 |
+
padding: 10px;
|
196 |
+
border-radius: 5px;
|
197 |
+
margin-bottom: 20px;
|
198 |
+
}
|
199 |
+
"""
|
200 |
+
) as app:
|
201 |
+
|
202 |
+
gr.HTML("""
|
203 |
+
<div class="header">
|
204 |
+
<h1>π― CV Tailor - AI Resume Optimizer</h1>
|
205 |
+
<p>Upload your PDF resume and job URL to get an AI-tailored resume that matches the job requirements!</p>
|
206 |
+
</div>
|
207 |
+
""")
|
208 |
+
|
209 |
+
gr.HTML("""
|
210 |
+
<div class="rate-limit-info">
|
211 |
+
<strong>β‘ Rate Limit:</strong> 3 requests per 5 minutes to manage API costs.
|
212 |
+
Please be patient and make each request count!
|
213 |
+
</div>
|
214 |
+
""")
|
215 |
+
|
216 |
+
with gr.Row():
|
217 |
+
with gr.Column(scale=1):
|
218 |
+
pdf_input = gr.File(
|
219 |
+
label="π Upload Your Resume (PDF)",
|
220 |
+
file_types=[".pdf"],
|
221 |
+
file_count="single"
|
222 |
+
)
|
223 |
+
|
224 |
+
job_url_input = gr.Textbox(
|
225 |
+
label="π Job Posting URL",
|
226 |
+
placeholder="https://company.com/jobs/position",
|
227 |
+
lines=1
|
228 |
+
)
|
229 |
+
|
230 |
+
submit_btn = gr.Button(
|
231 |
+
"π Generate Tailored Resume",
|
232 |
+
variant="primary",
|
233 |
+
size="lg"
|
234 |
+
)
|
235 |
+
|
236 |
+
# Examples
|
237 |
+
gr.Examples(
|
238 |
+
examples=[
|
239 |
+
["https://jobs.lever.co/example-company/software-engineer"],
|
240 |
+
["https://www.linkedin.com/jobs/view/example-job-id"],
|
241 |
+
["https://careers.google.com/jobs/results/example-position"]
|
242 |
+
],
|
243 |
+
inputs=job_url_input,
|
244 |
+
label="π Example Job URLs"
|
245 |
+
)
|
246 |
+
|
247 |
+
with gr.Column(scale=2):
|
248 |
+
status_output = gr.Textbox(
|
249 |
+
label="π Status",
|
250 |
+
interactive=False,
|
251 |
+
lines=1
|
252 |
+
)
|
253 |
+
|
254 |
+
result_output = gr.HTML(
|
255 |
+
label="π Tailored Resume",
|
256 |
+
value="Your optimized resume will appear here..."
|
257 |
+
)
|
258 |
+
|
259 |
+
# Event handlers
|
260 |
+
submit_btn.click(
|
261 |
+
fn=process_application,
|
262 |
+
inputs=[pdf_input, job_url_input, gr.State(lambda: str(time.time()))],
|
263 |
+
outputs=[status_output, result_output]
|
264 |
+
)
|
265 |
+
|
266 |
+
# Footer
|
267 |
+
gr.HTML("""
|
268 |
+
<div style="text-align: center; margin-top: 50px; color: #666;">
|
269 |
+
<p>Powered by CrewAI & OpenAI GPT-4o Mini |
|
270 |
+
<a href="https://github.com/joaomdmoura/crewAI" target="_blank">CrewAI</a> |
|
271 |
+
Built with β€οΈ using Gradio</p>
|
272 |
+
</div>
|
273 |
+
""")
|
274 |
+
|
275 |
+
return app
|
276 |
+
|
277 |
+
# Requirements for requirements.txt
|
278 |
+
requirements_txt = """
|
279 |
+
crewai==0.28.8
|
280 |
+
crewai-tools==0.1.6
|
281 |
+
langchain-openai==0.1.7
|
282 |
+
langchain-community==0.0.29
|
283 |
+
gradio==4.20.0
|
284 |
+
PyPDF2==3.0.1
|
285 |
+
markdown==3.5.2
|
286 |
+
python-dotenv==1.0.0
|
287 |
+
"""
|
288 |
+
|
289 |
+
# Create the app
|
290 |
+
if __name__ == "__main__":
|
291 |
+
# Set up environment variables
|
292 |
+
# In Hugging Face Spaces, set these in the Settings > Variables section
|
293 |
+
if not os.getenv("OPENAI_API_KEY"):
|
294 |
+
print("οΏ½οΏ½οΈ Warning: OPENAI_API_KEY not found in environment variables")
|
295 |
+
|
296 |
+
app = create_interface()
|
297 |
+
app.launch(
|
298 |
+
server_name="0.0.0.0",
|
299 |
+
server_port=7860,
|
300 |
+
show_api=False,
|
301 |
+
share=False
|
302 |
+
)
|