Spaces:
Sleeping
Sleeping
import json | |
import os | |
import requests | |
import streamlit as st | |
from datetime import datetime | |
from docx import Document | |
from io import BytesIO | |
from PyPDF2 import PdfReader, PdfWriter | |
# Consider using a more robust environment variable loading mechanism for production | |
from dotenv import load_dotenv | |
load_dotenv() | |
# ------------------------------------------------------------------------------ | |
# π ULTIMATE AI-POWERED IMPORT OS - "ResumeForge Pro" π | |
# Unleash the Power of AI for Career Advancement! | |
# ------------------------------------------------------------------------------ | |
# This is not just an import statement, it's a portal to career empowerment. | |
# It combines cutting-edge AI, meticulous resume craftsmanship, and an intuitive UI. | |
# ============================================================================== | |
# π CONFIGURATION - API KEYS, ENDPOINTS, & MODEL SELECTION π | |
# ============================================================================== | |
# Ensure your GROQ_API key is securely stored. For development, use environment variables. | |
# For production, consider secrets management solutions. | |
GROQ_API_KEY = os.getenv("GROQ_API_KEY") # Added correct env variable name | |
# Define the API endpoint for the Groq service. This provides flexibility | |
# to switch to different Groq deployments if needed. | |
GROQ_ENDPOINT = "https://api.groq.com/openai/v1/chat/completions" | |
# Choose the most appropriate Llama 3.3 model variant for your needs. | |
# "llama-3.3-70b-versatile" offers a balance of capabilities, but other | |
# variants might be better suited for specific tasks or resource constraints. | |
GROQ_MODEL = "llama-3.3-70b-versatile" | |
# ============================================================================== | |
# π₯ API HANDLER FOR GROQ AI - ROBUST AND RESILIENT π‘οΈ | |
# ============================================================================== | |
def call_groq_api(messages, temperature=0.7, max_tokens=1024, top_p=0.95, frequency_penalty=0.0, presence_penalty=0.0): | |
""" | |
Handles API calls to Groq's Llama 3.3 model with comprehensive error handling, | |
parameter tuning, and advanced logging. | |
Args: | |
messages (list): A list of message dictionaries, representing the conversation history. | |
Each dictionary should have 'role' (e.g., 'system', 'user', 'assistant') | |
and 'content' keys. | |
temperature (float): Controls the randomness of the output (0.0 - deterministic, 1.0 - very random). | |
Lower values are suitable for tasks requiring precision (e.g., resume formatting), | |
while higher values can be used for creative tasks (e.g., cover letter generation). | |
Defaults to 0.7. | |
max_tokens (int): The maximum number of tokens to generate in the response. Adjust this | |
based on the expected length of the output to balance completeness and cost. | |
Defaults to 1024. | |
top_p (float): Nucleus sampling parameter. Similar to temperature but controls the cumulative | |
probability of the tokens to sample from. A higher value allows for more | |
diverse outputs. Defaults to 0.95. | |
frequency_penalty (float): Penalizes new tokens based on their existing frequency in the text so far. | |
Encourages the model to use novel words and phrases. Defaults to 0.0. | |
presence_penalty (float): Penalizes new tokens that already appear in the text so far, regardless of frequency. | |
Discourages the model from repeating the same topics or ideas. Defaults to 0.0. | |
Returns: | |
str: The generated text content from the API response, or None if the API call fails. | |
""" | |
headers = { | |
"Authorization": f"Bearer {GROQ_API_KEY}", | |
"Content-Type": "application/json" | |
} | |
payload = { | |
"model": GROQ_MODEL, | |
"messages": messages, | |
"temperature": temperature, | |
"max_tokens": max_tokens, | |
"top_p": top_p, | |
"frequency_penalty": frequency_penalty, | |
"presence_penalty": presence_penalty | |
} | |
try: | |
st.sidebar.write(f"**API Request Payload:**\n\n{json.dumps(payload, indent=2)}") # Log the API payload | |
response = requests.post(GROQ_ENDPOINT, headers=headers, json=payload, timeout=25) # Increased timeout for potentially long requests | |
response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx) | |
response_json = response.json() | |
st.sidebar.write(f"**API Response:**\n\n{json.dumps(response_json, indent=2)}") # Log the entire response | |
return response_json["choices"][0]["message"]["content"].strip() | |
except requests.exceptions.RequestException as e: | |
st.error(f"β API Request Failed: {e} - Please ensure your API key is valid and Groq's service is operational.") | |
st.error(f"Detailed Error: {e}") # Provide a detailed error message | |
return None | |
except KeyError as e: | |
st.error(f"β Error parsing API response: Missing key {e}. Check the API response format.") | |
return None | |
except json.JSONDecodeError as e: | |
st.error(f"β Error decoding JSON response: {e}. The API might be returning invalid JSON.") | |
return None | |
except Exception as e: | |
st.error(f"β An unexpected error occurred: {e}") | |
return None | |
finally: | |
st.sidebar.write("--- API Request Complete ---") # Add a clear marker for request completion | |
# ============================================================================== | |
# π FILE PROCESSING - PDF, DOCX, TXT - WITH EXTENSIVE VALIDATION π‘οΈ | |
# ============================================================================== | |
def extract_resume_text(file_obj): | |
""" | |
Extracts text from uploaded resumes (PDF, DOCX, TXT) with robust error handling, | |
file type validation, and size limitations. | |
Args: | |
file_obj (streamlit.UploadedFile): The uploaded file object. | |
Returns: | |
str: The extracted text from the resume, or None if extraction fails. | |
""" | |
MAX_FILE_SIZE_MB = 5 # Set a reasonable file size limit | |
try: | |
file_bytes = file_obj.read() | |
file_size_mb = len(file_bytes) / (1024 * 1024) | |
if file_size_mb > MAX_FILE_SIZE_MB: | |
st.error(f"β File size exceeds the limit of {MAX_FILE_SIZE_MB} MB. Please upload a smaller file.") | |
return None | |
file_obj.seek(0) # Reset file pointer to the beginning | |
ext = os.path.splitext(file_obj.name)[-1].lower() | |
if ext == ".pdf": | |
try: | |
reader = PdfReader(BytesIO(file_bytes)) # Use BytesIO to handle in-memory file | |
text = "\n".join(page.extract_text() for page in reader.pages if page.extract_text()) | |
return text | |
except Exception as e: | |
st.error(f"β Error extracting text from PDF: {e}") | |
return None | |
elif ext in [".docx", ".doc"]: | |
try: | |
document = Document(BytesIO(file_bytes)) | |
text = "\n".join([para.text for para in document.paragraphs]) | |
return text | |
except Exception as e: | |
st.error(f"β Error extracting text from DOCX: {e}") | |
return None | |
elif ext == ".txt": | |
try: | |
return file_bytes.decode("utf-8", errors="ignore") # Handle encoding issues | |
except Exception as e: | |
st.error(f"β Error decoding text file: {e}") | |
return None | |
else: | |
st.warning(f"β οΈ Unsupported file type: {ext}. Please upload a PDF, DOCX, or TXT file.") | |
return None | |
except Exception as e: | |
st.error(f"β An unexpected error occurred during file processing: {e}") | |
return None | |
# ============================================================================== | |
# π― AI-POWERED RESUME GENERATION - ATS OPTIMIZED & STRUCTURED π | |
# ============================================================================== | |
def generate_resume(first_name, last_name, location, work_experience, school_experience, skills, contact_number, email_address, linkedin_profile): | |
""" | |
Generates a structured, ATS-friendly resume with enhanced formatting, | |
detailed instructions to the AI, and incorporates contact information. | |
Args: | |
first_name (str): The candidate's first name. | |
last_name (str): The candidate's last name. | |
location (str): The candidate's location. | |
work_experience (str): A detailed description of work experience. | |
school_experience (str): Details of educational background. | |
skills (str): A comma-separated list of skills. | |
contact_number (str): Candidate's phone number. | |
email_address (str): Candidate's email address. | |
linkedin_profile (str): Candidate's LinkedIn profile URL. | |
Returns: | |
str: The generated resume text. | |
""" | |
candidate_data = json.dumps({ | |
"full_name": f"{first_name} {last_name}", # Added full_name | |
"location": location, | |
"contact_number": contact_number, # Include contact_number | |
"email_address": email_address, # Include email_address | |
"linkedin_profile": linkedin_profile, # Include linkedin_profile | |
"work_experience": work_experience, | |
"school_experience": school_experience, | |
"skills": skills, | |
}, indent=2) | |
messages = [ | |
{"role": "system", "content": """ | |
You are an expert resume writer with extensive experience in Applicant Tracking Systems (ATS) optimization and modern resume design principles. | |
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. | |
You are an expert in using bullet points to quantify experience with metrics and achievements. | |
You will focus on extracting information given in work experience, education, and skills to accurately tailor the resume | |
"""}, | |
{"role": "user", "content": f""" | |
Generate a **highly professional**, **ATS-optimized resume** using the following structured information. Focus on: | |
* **Quantifiable Achievements:** Emphasize results and use numbers/metrics wherever possible. | |
* **Keywords:** Incorporate industry-specific keywords naturally. | |
* **Action Verbs:** Start each bullet point with strong action verbs. | |
* **Conciseness:** Keep sentences short and to the point. | |
* **Use the info provided and do not make things up.** | |
{candidate_data} | |
**Resume Structure Requirements:** | |
1. **Contact Information:** | |
* Full Name (prominently displayed) | |
* Phone Number | |
* Email Address | |
* LinkedIn Profile URL (if provided, ensure it's valid) | |
* Location (City, State - no full address) | |
2. **Summary/Profile:** | |
* A brief (3-4 sentence) summary highlighting key skills, experience, and career goals. Tailor this to be generic enough to apply to several positions. | |
3. **Work Experience:** | |
* Job Title | |
* Company Name | |
* Dates of Employment (Month Year - Month Year) | |
* **Responsibilities and Achievements:** Use bullet points to describe responsibilities and, most importantly, **quantifiable achievements**. | |
* Example: "Increased sales by 25% in Q3 by implementing a new marketing strategy." | |
* Example: "Reduced customer support tickets by 15% by improving the onboarding process." | |
* Order experiences from most recent to oldest. | |
4. **Education:** | |
* Degree Name | |
* Major (if applicable) | |
* University Name | |
* Graduation Date (Month Year) or Expected Graduation Date | |
* Include relevant coursework or academic achievements (optional). | |
5. **Skills:** | |
* List both technical and soft skills relevant to the candidate's field. | |
* Categorize skills (e.g., "Technical Skills," "Soft Skills," "Programming Languages"). | |
6. **Certifications/Awards (Optional):** | |
* List any relevant certifications or awards. | |
**Formatting Guidelines:** | |
* Use a clean and professional font (e.g., Arial, Calibri, Times New Roman) β Size 10-12. | |
* Use clear headings and subheadings. | |
* Use white space effectively to improve readability. | |
* Maintain consistent formatting throughout the document. | |
* Adhere to a one-page or two-page limit, depending on the candidate's experience level. | |
* Make sure to include enough information so the bot can create a great resume. | |
**IMPORTANT:** The resume MUST be ATS-compatible. Avoid using tables, columns, or graphics that can confuse ATS systems. | |
**Tone:** Professional, confident, and results-oriented. | |
"""} | |
] | |
return call_groq_api(messages, temperature=0.4, max_tokens=2048) # Lower temperature for more precision | |
# ============================================================================== | |
# βοΈ AI-POWERED COVER LETTER GENERATION - PERSUASIVE & TAILORED π | |
# ============================================================================== | |
def generate_cover_letter(candidate_json, job_description, hiring_manager_name="Hiring Manager"): | |
""" | |
Generates a compelling cover letter using structured candidate details and | |
a provided job description. | |
Args: | |
candidate_json (str): A JSON string containing candidate information (name, skills, experience). | |
job_description (str): The text of the job description. | |
hiring_manager_name (str, optional): The name of the hiring manager. Defaults to "Hiring Manager". | |
Returns: | |
str: The generated cover letter text. | |
""" | |
date_str = datetime.today().strftime("%d %B %Y") | |
messages = [ | |
{"role": "system", "content": """ | |
You are a world-class career advisor, adept at crafting highly persuasive and tailored cover letters. | |
Your cover letters are known for their ability to capture the attention of hiring managers and showcase the candidate's | |
unique qualifications and enthusiasm for the role. You are familiar with best practices in cover letter writing, | |
including the importance of tailoring the content to the specific job description and highlighting measurable achievements. | |
"""}, | |
{"role": "user", "content": f""" | |
Generate a **compelling and professional cover letter** using the following information: | |
**Candidate Profile:** {candidate_json} | |
**Job Description:** {job_description} | |
**Date:** {date_str} | |
**Hiring Manager Name (Salutation):** {hiring_manager_name} | |
**Cover Letter Requirements:** | |
1. **Opening Paragraph (Introduction):** | |
* Start with a strong opening that grabs the reader's attention. | |
* Clearly state the position you are applying for and where you saw the job posting. | |
* Express your enthusiasm for the opportunity. | |
2. **Body Paragraphs (Skills and Experience Alignment):** | |
* Highlight 2-3 key skills or experiences that directly align with the requirements outlined in the job description. | |
* Provide specific examples of how you have demonstrated these skills and achieved measurable results in previous roles. | |
* Quantify your achievements whenever possible (e.g., "Increased sales by 20%," "Reduced costs by 15%"). | |
* Showcase your understanding of the company and the role. | |
3. **Closing Paragraph (Call to Action):** | |
* Reiterate your interest in the position and your enthusiasm for the opportunity. | |
* Express your confidence in your ability to contribute to the company's success. | |
* Thank the hiring manager for their time and consideration. | |
* Include a call to action, such as requesting an interview. | |
**Formatting Guidelines:** | |
* Use a professional and easy-to-read font (e.g., Arial, Calibri, Times New Roman). | |
* Keep the cover letter concise and focused (ideally, no more than one page). | |
* Use proper grammar and spelling. | |
* Address the cover letter to the hiring manager by name, if possible. | |
* If the hiring manager's name is not available, use "Dear Hiring Manager." | |
**Tone:** Enthusiastic, confident, professional, and tailored. | |
"""} | |
] | |
return call_groq_api(messages, temperature=0.5, max_tokens=1536) # Adjusted temperature for persuasion | |
# ============================================================================== | |
# πΎ SAVE FUNCTION | |
# ============================================================================== | |
def save_text_as_file(text, filename, file_format): | |
"""Saves the generated text as a downloadable file in specified format.""" | |
try: | |
if file_format == "txt": | |
# UTF-8 encoding to support special characters. | |
data = text.encode('utf-8') | |
st.download_button( | |
label=f"Download {filename}.txt", | |
data=data, | |
file_name=f"{filename}.txt", | |
mime="text/plain", | |
help="Click to download the generated text as a plain text file.", | |
) | |
elif file_format == "docx": | |
document = Document() | |
document.add_paragraph(text) | |
# Write docx to a BytesIO object for downloading. | |
buffer = BytesIO() | |
document.save(buffer) | |
buffer.seek(0) # Go to the beginning of the buffer so it can be read. | |
st.download_button( | |
label=f"Download {filename}.docx", | |
data=buffer, | |
file_name=f"{filename}.docx", | |
mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document", | |
help="Click to download the generated text as a Word document (.docx).", | |
) | |
elif file_format == "pdf": | |
# Creating PDF from the given text. | |
from reportlab.lib.pagesizes import letter | |
from reportlab.pdfgen import canvas | |
# Buffer to hold the PDF in memory | |
buffer = BytesIO() | |
# Create the PDF object, using the buffer as its "file." | |
c = canvas.Canvas(buffer, pagesize=letter) | |
# Set font and size | |
c.setFont('Helvetica', 12) | |
textobject = c.beginText() | |
textobject.setTextOrigin(50, 750) | |
# Split the text into lines | |
lines = text.splitlines() | |
# Setting line height | |
line_height = 14 | |
# Write each line of text | |
for line in lines: | |
textobject.textLine(line.strip()) | |
textobject.moveCursor(0, -line_height) # Adjust line height to move down a bit | |
# Finish up | |
c.drawText(textobject) | |
c.showPage() | |
c.save() | |
# Move the buffer position to the beginning | |
buffer.seek(0) | |
st.download_button( | |
label=f"Download {filename}.pdf", | |
data=buffer, | |
file_name=f"{filename}.pdf", | |
mime="application/pdf", | |
help="Click to download the generated text as a PDF document.", | |
) | |
else: | |
st.error("Unsupported file format. Please choose txt, docx, or pdf.") | |
except Exception as e: | |
st.error(f"Error generating download: {e}") | |
# ============================================================================== | |
# π¨ STREAMLIT UI DESIGN - USER-FRIENDLY & RESPONSIVE π± | |
# ============================================================================== | |
def main(): | |
st.set_page_config(page_title="AI Resume & Cover Letter Generator", layout="wide") | |
st.title("π AI-Powered Resume & Cover Letter Generator") | |
st.markdown("### Create a **perfect ATS-friendly Resume** and **tailored Cover Letter** using **Groq AI (Llama 3.3-70B)**") | |
# Sidebar for API key and advanced settings | |
with st.sidebar: | |
st.header("βοΈ Advanced Settings") | |
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") | |
if api_key_input: | |
os.environ["GROQ_API_KEY"] = api_key_input # Correct way to set/update env var | |
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.") | |
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.") | |
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.") | |
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.") | |
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.") | |
st.markdown("---") | |
st.markdown("π‘ **Tip:** Adjust these settings to fine-tune the AI's performance. Lower temperature for resumes, higher for cover letters.") | |
tabs = st.tabs(["π Cover Letter Generator", "π Resume Creator"]) | |
# ----- COVER LETTER GENERATOR ----- | |
with tabs[0]: | |
st.header("π Cover Letter Generator") | |
resume_file = st.file_uploader("Upload Your Resume", type=["pdf", "docx", "txt"], help="Upload your resume in PDF, DOCX, or TXT format.") | |
job_description = st.text_area("Paste Job Description", height=200, help="Paste the job description from the job posting.") | |
hiring_manager_name = st.text_input("Hiring Manager Name (Optional)", value="Hiring Manager", help="Enter the name of the hiring manager if known.") | |
if st.button("πΉ Generate Cover Letter"): | |
if not os.getenv("GROQ_API_KEY"): | |
st.error("Please enter your Groq API key in the sidebar.") | |
return | |
if resume_file and job_description.strip(): | |
with st.spinner("β¨ Processing resume..."): | |
resume_text = extract_resume_text(resume_file) | |
if resume_text: | |
# Dummy data, consider extracting dynamically from resume. | |
candidate_json = json.dumps({ | |
"first_name": "Jane", | |
"last_name": "Doe", | |
"location": "San Francisco, CA", | |
"work_experience": "Experienced Software Engineer with a proven track record of success.", | |
"school_experience": "Master's Degree in Computer Science from Stanford University", | |
"skills": "Python, Java, Machine Learning, Cloud Computing" | |
}) | |
cover_letter = generate_cover_letter(candidate_json, job_description, hiring_manager_name) | |
if cover_letter: | |
st.success("β Cover Letter Generated!") | |
st.text_area("π Your Cover Letter:", cover_letter, height=300) | |
filename = "cover_letter" | |
file_format = st.selectbox("Select download format:", ["txt", "docx", "pdf"]) | |
save_text_as_file(cover_letter, filename, file_format) | |
else: | |
st.warning("β οΈ Please upload a resume and paste the job description.") | |
# ----- RESUME CREATOR ----- | |
with tabs[1]: | |
st.header("π Resume Creator") | |
with st.form("resume_form"): | |
col1, col2 = st.columns(2) | |
first_name = col1.text_input("First Name", help="Enter your first name.", value="Oluwafemi") # Pre-filled value | |
last_name = col2.text_input("Last Name", help="Enter your last name.", value="Idiakhoa") # Pre-filled value | |
location = st.text_input("Location", help="Enter your city and state.", value="Houston, Texas") # Pre-filled value | |
contact_number = st.text_input("Contact Number", help="Enter your phone number.", value="404-922-0516") # Pre-filled value | |
email_address = st.text_input("Email Address", help="Enter your email address.", value="[email protected]") # Pre-filled value | |
linkedin_profile = st.text_input("LinkedIn Profile URL", help="Enter your LinkedIn profile URL (optional).") | |
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.", value="Verizon feb-2023") # Pre-filled value | |
school_experience = st.text_area("Education", height=150, help="Describe your educational background, including degree names, university names, and graduation dates.", value="Master in Engineering") # Pre-filled value | |
skills = st.text_area("Skills", height=100, help="List your skills, separated by commas.") | |
submit = st.form_submit_button("π Generate Resume") | |
if submit: | |
if not os.getenv("GROQ_API_KEY"): | |
st.error("Please enter your Groq API key in the sidebar.") | |
return | |
with st.spinner("π Creating Resume..."): | |
resume_text = generate_resume(first_name, last_name, location, work_experience, school_experience, skills, contact_number, email_address, linkedin_profile) | |
if resume_text: | |
st.success("β Resume Generated!") | |
st.text_area("π Your Resume:", resume_text, height=400) | |
filename = f"{first_name}_{last_name}_resume" | |
file_format = st.selectbox("Select download format:", ["txt", "docx", "pdf"]) | |
save_text_as_file(resume_text, filename, file_format) | |
if __name__ == "__main__": | |
main() |