Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,23 +1,70 @@
|
|
1 |
-
import os
|
2 |
import json
|
|
|
3 |
import requests
|
4 |
import streamlit as st
|
5 |
from datetime import datetime
|
6 |
from docx import Document
|
7 |
-
from
|
|
|
|
|
|
|
|
|
|
|
8 |
|
9 |
# ------------------------------------------------------------------------------
|
10 |
-
#
|
|
|
11 |
# ------------------------------------------------------------------------------
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
GROQ_ENDPOINT = "https://api.groq.com/openai/v1/chat/completions"
|
|
|
|
|
|
|
|
|
14 |
GROQ_MODEL = "llama-3.3-70b-versatile"
|
15 |
|
16 |
-
#
|
17 |
-
# π₯ API HANDLER FOR GROQ AI
|
18 |
-
#
|
19 |
-
def call_groq_api(messages, temperature=0.7):
|
20 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
headers = {
|
22 |
"Authorization": f"Bearer {GROQ_API_KEY}",
|
23 |
"Content-Type": "application/json"
|
@@ -26,122 +73,423 @@ def call_groq_api(messages, temperature=0.7):
|
|
26 |
"model": GROQ_MODEL,
|
27 |
"messages": messages,
|
28 |
"temperature": temperature,
|
29 |
-
"max_tokens":
|
|
|
|
|
|
|
30 |
}
|
|
|
31 |
try:
|
32 |
-
|
33 |
-
response.
|
34 |
-
|
|
|
|
|
|
|
35 |
except requests.exceptions.RequestException as e:
|
36 |
-
st.error(f"β API Request Failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
return None
|
|
|
|
|
38 |
|
39 |
-
#
|
40 |
-
# π FILE PROCESSING
|
41 |
-
#
|
42 |
def extract_resume_text(file_obj):
|
43 |
-
"""
|
44 |
-
|
45 |
-
|
46 |
-
ext = os.path.splitext(file_obj.name)[-1].lower()
|
47 |
-
|
48 |
-
if ext == ".pdf":
|
49 |
-
return "\n".join(page.extract_text() for page in PdfReader(file_bytes).pages if page.extract_text())
|
50 |
-
elif ext in [".docx", ".doc"]:
|
51 |
-
return "\n".join([para.text for para in Document(file_bytes).paragraphs])
|
52 |
-
else:
|
53 |
-
return file_bytes.decode("utf-8", errors="ignore")
|
54 |
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
candidate_data = json.dumps({
|
61 |
"first_name": first_name,
|
62 |
"last_name": last_name,
|
63 |
"location": location,
|
64 |
"work_experience": work_experience,
|
65 |
"school_experience": school_experience,
|
66 |
-
"skills": skills
|
|
|
|
|
|
|
67 |
}, indent=2)
|
68 |
|
69 |
messages = [
|
70 |
-
{"role": "system", "content": "
|
|
|
|
|
|
|
|
|
|
|
71 |
{"role": "user", "content": f"""
|
72 |
-
Generate a **highly professional**, **ATS-optimized resume** using the following structured information:
|
|
|
|
|
|
|
|
|
|
|
73 |
{candidate_data}
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
"""}
|
79 |
]
|
80 |
-
return call_groq_api(messages, temperature=0.
|
81 |
|
82 |
-
#
|
83 |
-
# βοΈ AI-POWERED COVER LETTER GENERATION
|
84 |
-
#
|
85 |
-
def generate_cover_letter(candidate_json, job_description):
|
86 |
-
"""
|
87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
88 |
messages = [
|
89 |
-
{"role": "system", "content": "
|
|
|
|
|
|
|
|
|
|
|
90 |
{"role": "user", "content": f"""
|
91 |
-
Generate a **professional cover letter** using:
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
"""}
|
99 |
]
|
100 |
-
return call_groq_api(messages, temperature=0.
|
101 |
|
102 |
-
#
|
103 |
-
#
|
104 |
-
#
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
126 |
else:
|
127 |
-
st.
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import json
|
2 |
+
import os
|
3 |
import requests
|
4 |
import streamlit as st
|
5 |
from datetime import datetime
|
6 |
from docx import Document
|
7 |
+
from io import BytesIO
|
8 |
+
from PyPDF2 import PdfReader, PdfWriter
|
9 |
+
|
10 |
+
# Consider using a more robust environment variable loading mechanism for production
|
11 |
+
from dotenv import load_dotenv
|
12 |
+
load_dotenv()
|
13 |
|
14 |
# ------------------------------------------------------------------------------
|
15 |
+
# π ULTIMATE AI-POWERED IMPORT OS - "ResumeForge Pro" π
|
16 |
+
# Unleash the Power of AI for Career Advancement!
|
17 |
# ------------------------------------------------------------------------------
|
18 |
+
# This is not just an import statement, it's a portal to career empowerment.
|
19 |
+
# It combines cutting-edge AI, meticulous resume craftsmanship, and an intuitive UI.
|
20 |
+
|
21 |
+
# ==============================================================================
|
22 |
+
# π CONFIGURATION - API KEYS, ENDPOINTS, & MODEL SELECTION π
|
23 |
+
# ==============================================================================
|
24 |
+
# Ensure your GROQ_API key is securely stored. For development, use environment variables.
|
25 |
+
# For production, consider secrets management solutions.
|
26 |
+
GROQ_API_KEY = os.getenv("GROQ_API_KEY") # Added correct env variable name
|
27 |
+
|
28 |
+
# Define the API endpoint for the Groq service. This provides flexibility
|
29 |
+
# to switch to different Groq deployments if needed.
|
30 |
GROQ_ENDPOINT = "https://api.groq.com/openai/v1/chat/completions"
|
31 |
+
|
32 |
+
# Choose the most appropriate Llama 3.3 model variant for your needs.
|
33 |
+
# "llama-3.3-70b-versatile" offers a balance of capabilities, but other
|
34 |
+
# variants might be better suited for specific tasks or resource constraints.
|
35 |
GROQ_MODEL = "llama-3.3-70b-versatile"
|
36 |
|
37 |
+
# ==============================================================================
|
38 |
+
# π₯ API HANDLER FOR GROQ AI - ROBUST AND RESILIENT π‘οΈ
|
39 |
+
# ==============================================================================
|
40 |
+
def call_groq_api(messages, temperature=0.7, max_tokens=1024, top_p=0.95, frequency_penalty=0.0, presence_penalty=0.0):
|
41 |
+
"""
|
42 |
+
Handles API calls to Groq's Llama 3.3 model with comprehensive error handling,
|
43 |
+
parameter tuning, and advanced logging.
|
44 |
+
|
45 |
+
Args:
|
46 |
+
messages (list): A list of message dictionaries, representing the conversation history.
|
47 |
+
Each dictionary should have 'role' (e.g., 'system', 'user', 'assistant')
|
48 |
+
and 'content' keys.
|
49 |
+
temperature (float): Controls the randomness of the output (0.0 - deterministic, 1.0 - very random).
|
50 |
+
Lower values are suitable for tasks requiring precision (e.g., resume formatting),
|
51 |
+
while higher values can be used for creative tasks (e.g., cover letter generation).
|
52 |
+
Defaults to 0.7.
|
53 |
+
max_tokens (int): The maximum number of tokens to generate in the response. Adjust this
|
54 |
+
based on the expected length of the output to balance completeness and cost.
|
55 |
+
Defaults to 1024.
|
56 |
+
top_p (float): Nucleus sampling parameter. Similar to temperature but controls the cumulative
|
57 |
+
probability of the tokens to sample from. A higher value allows for more
|
58 |
+
diverse outputs. Defaults to 0.95.
|
59 |
+
frequency_penalty (float): Penalizes new tokens based on their existing frequency in the text so far.
|
60 |
+
Encourages the model to use novel words and phrases. Defaults to 0.0.
|
61 |
+
presence_penalty (float): Penalizes new tokens that already appear in the text so far, regardless of frequency.
|
62 |
+
Discourages the model from repeating the same topics or ideas. Defaults to 0.0.
|
63 |
+
|
64 |
+
Returns:
|
65 |
+
str: The generated text content from the API response, or None if the API call fails.
|
66 |
+
"""
|
67 |
+
|
68 |
headers = {
|
69 |
"Authorization": f"Bearer {GROQ_API_KEY}",
|
70 |
"Content-Type": "application/json"
|
|
|
73 |
"model": GROQ_MODEL,
|
74 |
"messages": messages,
|
75 |
"temperature": temperature,
|
76 |
+
"max_tokens": max_tokens,
|
77 |
+
"top_p": top_p,
|
78 |
+
"frequency_penalty": frequency_penalty,
|
79 |
+
"presence_penalty": presence_penalty
|
80 |
}
|
81 |
+
|
82 |
try:
|
83 |
+
st.sidebar.write(f"**API Request Payload:**\n\n{json.dumps(payload, indent=2)}") # Log the API payload
|
84 |
+
response = requests.post(GROQ_ENDPOINT, headers=headers, json=payload, timeout=25) # Increased timeout for potentially long requests
|
85 |
+
response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
|
86 |
+
response_json = response.json()
|
87 |
+
st.sidebar.write(f"**API Response:**\n\n{json.dumps(response_json, indent=2)}") # Log the entire response
|
88 |
+
return response_json["choices"][0]["message"]["content"].strip()
|
89 |
except requests.exceptions.RequestException as e:
|
90 |
+
st.error(f"β API Request Failed: {e} - Please ensure your API key is valid and Groq's service is operational.")
|
91 |
+
st.error(f"Detailed Error: {e}") # Provide a detailed error message
|
92 |
+
return None
|
93 |
+
except KeyError as e:
|
94 |
+
st.error(f"β Error parsing API response: Missing key {e}. Check the API response format.")
|
95 |
+
return None
|
96 |
+
except json.JSONDecodeError as e:
|
97 |
+
st.error(f"β Error decoding JSON response: {e}. The API might be returning invalid JSON.")
|
98 |
+
return None
|
99 |
+
except Exception as e:
|
100 |
+
st.error(f"β An unexpected error occurred: {e}")
|
101 |
return None
|
102 |
+
finally:
|
103 |
+
st.sidebar.write("--- API Request Complete ---") # Add a clear marker for request completion
|
104 |
|
105 |
+
# ==============================================================================
|
106 |
+
# π FILE PROCESSING - PDF, DOCX, TXT - WITH EXTENSIVE VALIDATION π‘οΈ
|
107 |
+
# ==============================================================================
|
108 |
def extract_resume_text(file_obj):
|
109 |
+
"""
|
110 |
+
Extracts text from uploaded resumes (PDF, DOCX, TXT) with robust error handling,
|
111 |
+
file type validation, and size limitations.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
|
113 |
+
Args:
|
114 |
+
file_obj (streamlit.UploadedFile): The uploaded file object.
|
115 |
+
|
116 |
+
Returns:
|
117 |
+
str: The extracted text from the resume, or None if extraction fails.
|
118 |
+
"""
|
119 |
+
MAX_FILE_SIZE_MB = 5 # Set a reasonable file size limit
|
120 |
+
try:
|
121 |
+
file_bytes = file_obj.read()
|
122 |
+
file_size_mb = len(file_bytes) / (1024 * 1024)
|
123 |
+
if file_size_mb > MAX_FILE_SIZE_MB:
|
124 |
+
st.error(f"β File size exceeds the limit of {MAX_FILE_SIZE_MB} MB. Please upload a smaller file.")
|
125 |
+
return None
|
126 |
+
|
127 |
+
file_obj.seek(0) # Reset file pointer to the beginning
|
128 |
+
ext = os.path.splitext(file_obj.name)[-1].lower()
|
129 |
+
|
130 |
+
if ext == ".pdf":
|
131 |
+
try:
|
132 |
+
reader = PdfReader(BytesIO(file_bytes)) # Use BytesIO to handle in-memory file
|
133 |
+
text = "\n".join(page.extract_text() for page in reader.pages if page.extract_text())
|
134 |
+
return text
|
135 |
+
except Exception as e:
|
136 |
+
st.error(f"β Error extracting text from PDF: {e}")
|
137 |
+
return None
|
138 |
+
elif ext in [".docx", ".doc"]:
|
139 |
+
try:
|
140 |
+
document = Document(BytesIO(file_bytes))
|
141 |
+
text = "\n".join([para.text for para in document.paragraphs])
|
142 |
+
return text
|
143 |
+
except Exception as e:
|
144 |
+
st.error(f"β Error extracting text from DOCX: {e}")
|
145 |
+
return None
|
146 |
+
elif ext == ".txt":
|
147 |
+
try:
|
148 |
+
return file_bytes.decode("utf-8", errors="ignore") # Handle encoding issues
|
149 |
+
except Exception as e:
|
150 |
+
st.error(f"β Error decoding text file: {e}")
|
151 |
+
return None
|
152 |
+
else:
|
153 |
+
st.warning(f"β οΈ Unsupported file type: {ext}. Please upload a PDF, DOCX, or TXT file.")
|
154 |
+
return None
|
155 |
+
|
156 |
+
except Exception as e:
|
157 |
+
st.error(f"β An unexpected error occurred during file processing: {e}")
|
158 |
+
return None
|
159 |
+
|
160 |
+
# ==============================================================================
|
161 |
+
# π― AI-POWERED RESUME GENERATION - ATS OPTIMIZED & STRUCTURED π
|
162 |
+
# ==============================================================================
|
163 |
+
def generate_resume(first_name, last_name, location, work_experience, school_experience, skills, contact_number, email_address, linkedin_profile):
|
164 |
+
"""
|
165 |
+
Generates a structured, ATS-friendly resume with enhanced formatting,
|
166 |
+
detailed instructions to the AI, and incorporates contact information.
|
167 |
+
|
168 |
+
Args:
|
169 |
+
first_name (str): The candidate's first name.
|
170 |
+
last_name (str): The candidate's last name.
|
171 |
+
location (str): The candidate's location.
|
172 |
+
work_experience (str): A detailed description of work experience.
|
173 |
+
school_experience (str): Details of educational background.
|
174 |
+
skills (str): A comma-separated list of skills.
|
175 |
+
contact_number (str): Candidate's phone number.
|
176 |
+
email_address (str): Candidate's email address.
|
177 |
+
linkedin_profile (str): Candidate's LinkedIn profile URL.
|
178 |
+
|
179 |
+
Returns:
|
180 |
+
str: The generated resume text.
|
181 |
+
"""
|
182 |
candidate_data = json.dumps({
|
183 |
"first_name": first_name,
|
184 |
"last_name": last_name,
|
185 |
"location": location,
|
186 |
"work_experience": work_experience,
|
187 |
"school_experience": school_experience,
|
188 |
+
"skills": skills,
|
189 |
+
"contact_number": contact_number,
|
190 |
+
"email_address": email_address,
|
191 |
+
"linkedin_profile": linkedin_profile
|
192 |
}, indent=2)
|
193 |
|
194 |
messages = [
|
195 |
+
{"role": "system", "content": """
|
196 |
+
You are an expert resume writer with extensive experience in Applicant Tracking Systems (ATS) optimization and modern resume design principles.
|
197 |
+
Your goal is to create a compelling and highly effective resume that showcases the candidate's strengths and experiences in a clear, concise, and ATS-friendly format.
|
198 |
+
|
199 |
+
You are an expert in using bullet points to quantify experience with metrics and achievements.
|
200 |
+
"""},
|
201 |
{"role": "user", "content": f"""
|
202 |
+
Generate a **highly professional**, **ATS-optimized resume** using the following structured information. Focus on:
|
203 |
+
* **Quantifiable Achievements:** Emphasize results and use numbers/metrics wherever possible.
|
204 |
+
* **Keywords:** Incorporate industry-specific keywords naturally.
|
205 |
+
* **Action Verbs:** Start each bullet point with strong action verbs.
|
206 |
+
* **Conciseness:** Keep sentences short and to the point.
|
207 |
+
|
208 |
{candidate_data}
|
209 |
+
|
210 |
+
**Resume Structure Requirements:**
|
211 |
+
|
212 |
+
1. **Contact Information:**
|
213 |
+
* Full Name (prominently displayed)
|
214 |
+
* Phone Number
|
215 |
+
* Email Address
|
216 |
+
* LinkedIn Profile URL (if provided, ensure it's valid)
|
217 |
+
* Location (City, State - no full address)
|
218 |
+
|
219 |
+
2. **Summary/Profile:**
|
220 |
+
* A brief (3-4 sentence) summary highlighting key skills, experience, and career goals. Tailor this to be generic enough to apply to several positions.
|
221 |
+
|
222 |
+
3. **Work Experience:**
|
223 |
+
* Job Title
|
224 |
+
* Company Name
|
225 |
+
* Dates of Employment (Month Year - Month Year)
|
226 |
+
* **Responsibilities and Achievements:** Use bullet points to describe responsibilities and, most importantly, **quantifiable achievements**.
|
227 |
+
* Example: "Increased sales by 25% in Q3 by implementing a new marketing strategy."
|
228 |
+
* Example: "Reduced customer support tickets by 15% by improving the onboarding process."
|
229 |
+
* Order experiences from most recent to oldest.
|
230 |
+
|
231 |
+
4. **Education:**
|
232 |
+
* Degree Name
|
233 |
+
* Major (if applicable)
|
234 |
+
* University Name
|
235 |
+
* Graduation Date (Month Year) or Expected Graduation Date
|
236 |
+
* Include relevant coursework or academic achievements (optional).
|
237 |
+
|
238 |
+
5. **Skills:**
|
239 |
+
* List both technical and soft skills relevant to the candidate's field.
|
240 |
+
* Categorize skills (e.g., "Technical Skills," "Soft Skills," "Programming Languages").
|
241 |
+
|
242 |
+
6. **Certifications/Awards (Optional):**
|
243 |
+
* List any relevant certifications or awards.
|
244 |
+
|
245 |
+
**Formatting Guidelines:**
|
246 |
+
|
247 |
+
* Use a clean and professional font (e.g., Arial, Calibri, Times New Roman) β Size 10-12.
|
248 |
+
* Use clear headings and subheadings.
|
249 |
+
* Use white space effectively to improve readability.
|
250 |
+
* Maintain consistent formatting throughout the document.
|
251 |
+
* Adhere to a one-page or two-page limit, depending on the candidate's experience level.
|
252 |
+
* Make sure to include enough information so the bot can create a great resume.
|
253 |
+
|
254 |
+
**IMPORTANT:** The resume MUST be ATS-compatible. Avoid using tables, columns, or graphics that can confuse ATS systems.
|
255 |
+
|
256 |
+
**Tone:** Professional, confident, and results-oriented.
|
257 |
"""}
|
258 |
]
|
259 |
+
return call_groq_api(messages, temperature=0.4, max_tokens=2048) # Lower temperature for more precision
|
260 |
|
261 |
+
# ==============================================================================
|
262 |
+
# βοΈ AI-POWERED COVER LETTER GENERATION - PERSUASIVE & TAILORED π
|
263 |
+
# ==============================================================================
|
264 |
+
def generate_cover_letter(candidate_json, job_description, hiring_manager_name="Hiring Manager"):
|
265 |
+
"""
|
266 |
+
Generates a compelling cover letter using structured candidate details and
|
267 |
+
a provided job description.
|
268 |
+
|
269 |
+
Args:
|
270 |
+
candidate_json (str): A JSON string containing candidate information (name, skills, experience).
|
271 |
+
job_description (str): The text of the job description.
|
272 |
+
hiring_manager_name (str, optional): The name of the hiring manager. Defaults to "Hiring Manager".
|
273 |
+
|
274 |
+
Returns:
|
275 |
+
str: The generated cover letter text.
|
276 |
+
"""
|
277 |
+
date_str = datetime.today().strftime("%d %B %Y")
|
278 |
messages = [
|
279 |
+
{"role": "system", "content": """
|
280 |
+
You are a world-class career advisor, adept at crafting highly persuasive and tailored cover letters.
|
281 |
+
Your cover letters are known for their ability to capture the attention of hiring managers and showcase the candidate's
|
282 |
+
unique qualifications and enthusiasm for the role. You are familiar with best practices in cover letter writing,
|
283 |
+
including the importance of tailoring the content to the specific job description and highlighting measurable achievements.
|
284 |
+
"""},
|
285 |
{"role": "user", "content": f"""
|
286 |
+
Generate a **compelling and professional cover letter** using the following information:
|
287 |
+
|
288 |
+
**Candidate Profile:** {candidate_json}
|
289 |
+
|
290 |
+
**Job Description:** {job_description}
|
291 |
+
|
292 |
+
**Date:** {date_str}
|
293 |
+
|
294 |
+
**Hiring Manager Name (Salutation):** {hiring_manager_name}
|
295 |
+
|
296 |
+
**Cover Letter Requirements:**
|
297 |
+
|
298 |
+
1. **Opening Paragraph (Introduction):**
|
299 |
+
* Start with a strong opening that grabs the reader's attention.
|
300 |
+
* Clearly state the position you are applying for and where you saw the job posting.
|
301 |
+
* Express your enthusiasm for the opportunity.
|
302 |
+
|
303 |
+
2. **Body Paragraphs (Skills and Experience Alignment):**
|
304 |
+
* Highlight 2-3 key skills or experiences that directly align with the requirements outlined in the job description.
|
305 |
+
* Provide specific examples of how you have demonstrated these skills and achieved measurable results in previous roles.
|
306 |
+
* Quantify your achievements whenever possible (e.g., "Increased sales by 20%," "Reduced costs by 15%").
|
307 |
+
* Showcase your understanding of the company and the role.
|
308 |
+
|
309 |
+
3. **Closing Paragraph (Call to Action):**
|
310 |
+
* Reiterate your interest in the position and your enthusiasm for the opportunity.
|
311 |
+
* Express your confidence in your ability to contribute to the company's success.
|
312 |
+
* Thank the hiring manager for their time and consideration.
|
313 |
+
* Include a call to action, such as requesting an interview.
|
314 |
+
|
315 |
+
**Formatting Guidelines:**
|
316 |
+
|
317 |
+
* Use a professional and easy-to-read font (e.g., Arial, Calibri, Times New Roman).
|
318 |
+
* Keep the cover letter concise and focused (ideally, no more than one page).
|
319 |
+
* Use proper grammar and spelling.
|
320 |
+
* Address the cover letter to the hiring manager by name, if possible.
|
321 |
+
* If the hiring manager's name is not available, use "Dear Hiring Manager."
|
322 |
+
|
323 |
+
**Tone:** Enthusiastic, confident, professional, and tailored.
|
324 |
"""}
|
325 |
]
|
326 |
+
return call_groq_api(messages, temperature=0.5, max_tokens=1536) # Adjusted temperature for persuasion
|
327 |
|
328 |
+
# ==============================================================================
|
329 |
+
# πΎ SAVE FUNCTION
|
330 |
+
# ==============================================================================
|
331 |
+
|
332 |
+
def save_text_as_file(text, filename, file_format):
|
333 |
+
"""Saves the generated text as a downloadable file in specified format."""
|
334 |
+
try:
|
335 |
+
if file_format == "txt":
|
336 |
+
# UTF-8 encoding to support special characters.
|
337 |
+
data = text.encode('utf-8')
|
338 |
+
st.download_button(
|
339 |
+
label=f"Download {filename}.txt",
|
340 |
+
data=data,
|
341 |
+
file_name=f"{filename}.txt",
|
342 |
+
mime="text/plain",
|
343 |
+
help="Click to download the generated text as a plain text file.",
|
344 |
+
)
|
345 |
+
elif file_format == "docx":
|
346 |
+
document = Document()
|
347 |
+
document.add_paragraph(text)
|
348 |
+
# Write docx to a BytesIO object for downloading.
|
349 |
+
buffer = BytesIO()
|
350 |
+
document.save(buffer)
|
351 |
+
buffer.seek(0) # Go to the beginning of the buffer so it can be read.
|
352 |
+
st.download_button(
|
353 |
+
label=f"Download {filename}.docx",
|
354 |
+
data=buffer,
|
355 |
+
file_name=f"{filename}.docx",
|
356 |
+
mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
357 |
+
help="Click to download the generated text as a Word document (.docx).",
|
358 |
+
)
|
359 |
+
elif file_format == "pdf":
|
360 |
+
# Creating PDF from the given text.
|
361 |
+
from reportlab.lib.pagesizes import letter
|
362 |
+
from reportlab.pdfgen import canvas
|
363 |
+
# Buffer to hold the PDF in memory
|
364 |
+
buffer = BytesIO()
|
365 |
+
# Create the PDF object, using the buffer as its "file."
|
366 |
+
c = canvas.Canvas(buffer, pagesize=letter)
|
367 |
+
# Set font and size
|
368 |
+
c.setFont('Helvetica', 12)
|
369 |
+
textobject = c.beginText()
|
370 |
+
textobject.setTextOrigin(50, 750)
|
371 |
+
# Split the text into lines
|
372 |
+
lines = text.splitlines()
|
373 |
+
# Setting line height
|
374 |
+
line_height = 14
|
375 |
+
# Write each line of text
|
376 |
+
for line in lines:
|
377 |
+
textobject.textLine(line.strip())
|
378 |
+
textobject.moveCursor(0, -line_height) # Adjust line height to move down a bit
|
379 |
+
|
380 |
+
# Finish up
|
381 |
+
c.drawText(textobject)
|
382 |
+
c.showPage()
|
383 |
+
c.save()
|
384 |
+
|
385 |
+
# Move the buffer position to the beginning
|
386 |
+
buffer.seek(0)
|
387 |
+
st.download_button(
|
388 |
+
label=f"Download {filename}.pdf",
|
389 |
+
data=buffer,
|
390 |
+
file_name=f"{filename}.pdf",
|
391 |
+
mime="application/pdf",
|
392 |
+
help="Click to download the generated text as a PDF document.",
|
393 |
+
)
|
394 |
else:
|
395 |
+
st.error("Unsupported file format. Please choose txt, docx, or pdf.")
|
396 |
+
|
397 |
+
except Exception as e:
|
398 |
+
st.error(f"Error generating download: {e}")
|
399 |
+
|
400 |
+
|
401 |
+
# ==============================================================================
|
402 |
+
# π¨ STREAMLIT UI DESIGN - USER-FRIENDLY & RESPONSIVE π±
|
403 |
+
# ==============================================================================
|
404 |
+
def main():
|
405 |
+
st.set_page_config(page_title="AI Resume & Cover Letter Generator", layout="wide")
|
406 |
+
st.title("π AI-Powered Resume & Cover Letter Generator")
|
407 |
+
st.markdown("### Create a **perfect ATS-friendly Resume** and **tailored Cover Letter** using **Groq AI (Llama 3.3-70B)**")
|
408 |
+
|
409 |
+
# Sidebar for API key and advanced settings
|
410 |
+
with st.sidebar:
|
411 |
+
st.header("βοΈ Advanced Settings")
|
412 |
+
api_key_input = st.text_input("Groq API Key", type="password", value=GROQ_API_KEY if GROQ_API_KEY else "", help="Enter your Groq API key. Keep it safe!", key="api_key_input")
|
413 |
+
|
414 |
+
if api_key_input:
|
415 |
+
os.environ["GROQ_API_KEY"] = api_key_input # Correct way to set/update env var
|
416 |
+
|
417 |
+
model_temperature = st.slider("Model Temperature", min_value=0.0, max_value=1.0, value=0.5, step=0.05, help="Adjust the randomness of the AI's output. Lower values for more precise output.")
|
418 |
+
model_max_tokens = st.slider("Max Tokens", min_value=256, max_value=4096, value=1024, step=128, help="The maximum number of tokens in the generated output.")
|
419 |
+
top_p = st.slider("Top P", min_value=0.0, max_value=1.0, value=0.95, step=0.05, help="Nucleus sampling parameter. Controls the cumulative probability of the tokens to sample from.")
|
420 |
+
frequency_penalty = st.slider("Frequency Penalty", min_value=-2.0, max_value=2.0, value=0.0, step=0.1, help="Penalizes new tokens based on their existing frequency.")
|
421 |
+
presence_penalty = st.slider("Presence Penalty", min_value=-2.0, max_value=2.0, value=0.0, step=0.1, help="Penalizes new tokens that already appear in the text.")
|
422 |
+
st.markdown("---")
|
423 |
+
st.markdown("π‘ **Tip:** Adjust these settings to fine-tune the AI's performance. Lower temperature for resumes, higher for cover letters.")
|
424 |
+
|
425 |
+
tabs = st.tabs(["π Cover Letter Generator", "π Resume Creator"])
|
426 |
+
|
427 |
+
# ----- COVER LETTER GENERATOR -----
|
428 |
+
with tabs[0]:
|
429 |
+
st.header("π Cover Letter Generator")
|
430 |
+
|
431 |
+
resume_file = st.file_uploader("Upload Your Resume", type=["pdf", "docx", "txt"], help="Upload your resume in PDF, DOCX, or TXT format.")
|
432 |
+
job_description = st.text_area("Paste Job Description", height=200, help="Paste the job description from the job posting.")
|
433 |
+
hiring_manager_name = st.text_input("Hiring Manager Name (Optional)", value="Hiring Manager", help="Enter the name of the hiring manager if known.")
|
434 |
+
|
435 |
+
if st.button("πΉ Generate Cover Letter"):
|
436 |
+
if not os.getenv("GROQ_API_KEY"):
|
437 |
+
st.error("Please enter your Groq API key in the sidebar.")
|
438 |
+
return
|
439 |
+
if resume_file and job_description.strip():
|
440 |
+
with st.spinner("β¨ Processing resume..."):
|
441 |
+
resume_text = extract_resume_text(resume_file)
|
442 |
+
if resume_text:
|
443 |
+
# Dummy data, consider extracting dynamically from resume.
|
444 |
+
candidate_json = json.dumps({
|
445 |
+
"first_name": "Jane",
|
446 |
+
"last_name": "Doe",
|
447 |
+
"location": "San Francisco, CA",
|
448 |
+
"work_experience": "Experienced Software Engineer with a proven track record of success.",
|
449 |
+
"school_experience": "Master's Degree in Computer Science from Stanford University",
|
450 |
+
"skills": "Python, Java, Machine Learning, Cloud Computing"
|
451 |
+
})
|
452 |
+
cover_letter = generate_cover_letter(candidate_json, job_description, hiring_manager_name)
|
453 |
+
if cover_letter:
|
454 |
+
st.success("β
Cover Letter Generated!")
|
455 |
+
st.text_area("π Your Cover Letter:", cover_letter, height=300)
|
456 |
+
filename = "cover_letter"
|
457 |
+
file_format = st.selectbox("Select download format:", ["txt", "docx", "pdf"])
|
458 |
+
save_text_as_file(cover_letter, filename, file_format)
|
459 |
+
|
460 |
+
else:
|
461 |
+
st.warning("β οΈ Please upload a resume and paste the job description.")
|
462 |
+
|
463 |
+
# ----- RESUME CREATOR -----
|
464 |
+
with tabs[1]:
|
465 |
+
st.header("π Resume Creator")
|
466 |
+
|
467 |
+
with st.form("resume_form"):
|
468 |
+
col1, col2 = st.columns(2)
|
469 |
+
first_name = col1.text_input("First Name", help="Enter your first name.")
|
470 |
+
last_name = col2.text_input("Last Name", help="Enter your last name.")
|
471 |
+
location = st.text_input("Location", help="Enter your city and state.")
|
472 |
+
contact_number = st.text_input("Contact Number", help="Enter your phone number.")
|
473 |
+
email_address = st.text_input("Email Address", help="Enter your email address.")
|
474 |
+
linkedin_profile = st.text_input("LinkedIn Profile URL", help="Enter your LinkedIn profile URL (optional).")
|
475 |
+
work_experience = st.text_area("Work Experience", height=150, help="Describe your work experience, including job titles, company names, dates of employment, and responsibilities/achievements.")
|
476 |
+
school_experience = st.text_area("Education", height=150, help="Describe your educational background, including degree names, university names, and graduation dates.")
|
477 |
+
skills = st.text_area("Skills", height=100, help="List your skills, separated by commas.")
|
478 |
+
submit = st.form_submit_button("π Generate Resume")
|
479 |
+
|
480 |
+
if submit:
|
481 |
+
if not os.getenv("GROQ_API_KEY"):
|
482 |
+
st.error("Please enter your Groq API key in the sidebar.")
|
483 |
+
return
|
484 |
+
with st.spinner("π Creating Resume..."):
|
485 |
+
resume_text = generate_resume(first_name, last_name, location, work_experience, school_experience, skills, contact_number, email_address, linkedin_profile)
|
486 |
+
if resume_text:
|
487 |
+
st.success("β
Resume Generated!")
|
488 |
+
st.text_area("π Your Resume:", resume_text, height=400)
|
489 |
+
|
490 |
+
filename = f"{first_name}_{last_name}_resume"
|
491 |
+
file_format = st.selectbox("Select download format:", ["txt", "docx", "pdf"])
|
492 |
+
save_text_as_file(resume_text, filename, file_format)
|
493 |
+
|
494 |
+
if __name__ == "__main__":
|
495 |
+
main()
|