Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,109 +1,53 @@
|
|
| 1 |
-
|
| 2 |
-
import gradio as gr
|
| 3 |
-
import PyPDF2
|
| 4 |
-
import docx
|
| 5 |
-
import requests
|
| 6 |
-
import json
|
| 7 |
-
import re # Added for regex pattern replacement
|
| 8 |
-
|
| 9 |
-
# Function to extract text from PDF
|
| 10 |
-
def extract_text_from_pdf(file):
|
| 11 |
-
pdf_reader = PyPDF2.PdfReader(file)
|
| 12 |
-
text = ""
|
| 13 |
-
for page in pdf_reader.pages:
|
| 14 |
-
text += page.extract_text()
|
| 15 |
-
return text
|
| 16 |
-
|
| 17 |
-
# Function to extract text from Word document
|
| 18 |
-
def extract_text_from_docx(file):
|
| 19 |
-
doc = docx.Document(file)
|
| 20 |
-
text = "\n".join([para.text for para in doc.paragraphs])
|
| 21 |
-
return text
|
| 22 |
-
|
| 23 |
-
# Function to process uploaded file
|
| 24 |
-
def process_uploaded_file(file):
|
| 25 |
-
filename = file.name.lower() # Case-insensitive check
|
| 26 |
-
if filename.endswith(".pdf"):
|
| 27 |
-
return extract_text_from_pdf(file)
|
| 28 |
-
elif filename.endswith(".docx"):
|
| 29 |
-
return extract_text_from_docx(file)
|
| 30 |
-
else:
|
| 31 |
-
raise ValueError("Unsupported file format. Please upload a PDF or Word document.")
|
| 32 |
-
|
| 33 |
-
# Function to clean JSON string with escaped backslashes
|
| 34 |
-
def clean_json_string(json_str):
|
| 35 |
-
# Replace escaped backslashes with a temporary marker
|
| 36 |
-
temp_str = json_str.replace('\\_', '__UNDERSCORE__')
|
| 37 |
-
|
| 38 |
-
# Attempt to fix any other common escape sequence issues
|
| 39 |
-
temp_str = temp_str.replace('\\n', '\n')
|
| 40 |
-
temp_str = temp_str.replace('\\t', '\t')
|
| 41 |
-
temp_str = temp_str.replace('\\r', '\r')
|
| 42 |
-
|
| 43 |
-
# Remove any remaining unmatched backslashes
|
| 44 |
-
temp_str = temp_str.replace('\\', '')
|
| 45 |
-
|
| 46 |
-
# Restore underscores
|
| 47 |
-
cleaned_str = temp_str.replace('__UNDERSCORE__', '_')
|
| 48 |
-
|
| 49 |
-
return cleaned_str
|
| 50 |
-
|
| 51 |
-
# Function to call Together API
|
| 52 |
-
def analyze_with_mistral(resume_text, job_description):
|
| 53 |
-
TOGETHER_API_KEY = os.getenv("HUGGINGFACE_API_KEY")
|
| 54 |
-
url = "https://api.together.xyz/v1/chat/completions"
|
| 55 |
-
|
| 56 |
-
messages = [
|
| 57 |
{
|
| 58 |
"role": "system",
|
| 59 |
-
"content": "You are an AI expert in
|
| 60 |
},
|
| 61 |
{
|
| 62 |
"role": "user",
|
| 63 |
"content": """
|
| 64 |
-
Analyze the provided resume against the job description for ATS compatibility. Assess
|
| 65 |
|
| 66 |
-
|
| 67 |
{
|
| 68 |
"ATS Parameters": {
|
| 69 |
"Keywords": {
|
| 70 |
-
"Match": <integer
|
| 71 |
-
"Recommendation": [<list of
|
| 72 |
},
|
| 73 |
"Formatting": {
|
| 74 |
-
"Match": <integer
|
| 75 |
-
"Recommendation": [<list of
|
| 76 |
},
|
| 77 |
"Skills Match": {
|
| 78 |
-
"Match": <integer
|
| 79 |
-
"Recommendation": [<list of
|
| 80 |
},
|
| 81 |
"Experience Relevance": {
|
| 82 |
-
"Match": <integer
|
| 83 |
-
"Recommendation": [<list of
|
| 84 |
},
|
| 85 |
"Education": {
|
| 86 |
-
"Match": <integer
|
| 87 |
-
"Recommendation": [<list of
|
| 88 |
}
|
| 89 |
},
|
| 90 |
"Score": {
|
| 91 |
-
"Keywords": <integer
|
| 92 |
-
"Formatting": <integer
|
| 93 |
-
"Skills Match": <integer
|
| 94 |
-
"Experience Relevance": <integer
|
| 95 |
-
"Education": <integer
|
| 96 |
-
"Overall": <integer average
|
| 97 |
}
|
| 98 |
}
|
| 99 |
|
| 100 |
Rules:
|
| 101 |
-
-
|
| 102 |
-
-
|
| 103 |
-
- "Overall"
|
| 104 |
-
- Base analysis on
|
| 105 |
-
-
|
| 106 |
-
- Keep recommendations concise to fit within response limits.
|
| 107 |
|
| 108 |
Resume:
|
| 109 |
{resume_text}
|
|
@@ -112,104 +56,4 @@ def analyze_with_mistral(resume_text, job_description):
|
|
| 112 |
{job_description}
|
| 113 |
"""
|
| 114 |
}
|
| 115 |
-
]
|
| 116 |
-
|
| 117 |
-
payload = {
|
| 118 |
-
"model": "mistralai/Mistral-7B-Instruct-v0.3",
|
| 119 |
-
"messages": messages,
|
| 120 |
-
"max_tokens": 1500,
|
| 121 |
-
"temperature": 0.7,
|
| 122 |
-
"top_p": 0.9,
|
| 123 |
-
"response_format": {"type": "json_object"}
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
headers = {
|
| 127 |
-
"Authorization": f"Bearer {TOGETHER_API_KEY}",
|
| 128 |
-
"Content-Type": "application/json",
|
| 129 |
-
}
|
| 130 |
-
|
| 131 |
-
try:
|
| 132 |
-
response = requests.post(url, json=payload, headers=headers, timeout=30)
|
| 133 |
-
response.raise_for_status()
|
| 134 |
-
result = response.json()
|
| 135 |
-
content = result.get("choices", [{}])[0].get("message", {}).get("content", "{}")
|
| 136 |
-
|
| 137 |
-
# Clean the JSON string before parsing
|
| 138 |
-
cleaned_content = clean_json_string(content)
|
| 139 |
-
|
| 140 |
-
try:
|
| 141 |
-
parsed = json.loads(cleaned_content)
|
| 142 |
-
# Check if the JSON structure is valid for our use case
|
| 143 |
-
if "ATS_Compatibility" not in parsed or "Overall_Assessment" not in parsed:
|
| 144 |
-
return {
|
| 145 |
-
"error": "API returned unexpected JSON structure",
|
| 146 |
-
"raw_content": cleaned_content[:500] + "..." if len(cleaned_content) > 500 else cleaned_content
|
| 147 |
-
}
|
| 148 |
-
return parsed
|
| 149 |
-
except json.JSONDecodeError as e:
|
| 150 |
-
# Try a more aggressive approach to fix the JSON
|
| 151 |
-
try:
|
| 152 |
-
# Sometimes the model might include trailing characters
|
| 153 |
-
# Try to find the closing bracket of the main JSON object
|
| 154 |
-
match = re.search(r'(\{.*\})', cleaned_content, re.DOTALL)
|
| 155 |
-
if match:
|
| 156 |
-
extracted_json = match.group(1)
|
| 157 |
-
parsed = json.loads(extracted_json)
|
| 158 |
-
if "ATS_Compatibility" in parsed and "Overall_Assessment" in parsed:
|
| 159 |
-
return parsed
|
| 160 |
-
except:
|
| 161 |
-
pass
|
| 162 |
-
|
| 163 |
-
# If all attempts fail, return the error
|
| 164 |
-
return {
|
| 165 |
-
"error": f"Failed to parse API response: {str(e)}",
|
| 166 |
-
"raw_content": cleaned_content[:500] + "..." if len(cleaned_content) > 500 else cleaned_content
|
| 167 |
-
}
|
| 168 |
-
|
| 169 |
-
except requests.exceptions.RequestException as e:
|
| 170 |
-
return {"error": f"API request failed: {str(e)}"}
|
| 171 |
-
except Exception as e:
|
| 172 |
-
return {"error": f"Unexpected error: {str(e)}"}
|
| 173 |
-
|
| 174 |
-
# Main function
|
| 175 |
-
def analyze_resume(file, job_description):
|
| 176 |
-
try:
|
| 177 |
-
if not file:
|
| 178 |
-
return json.dumps({"error": "Please upload a resume file"}, indent=2)
|
| 179 |
-
if not job_description:
|
| 180 |
-
return json.dumps({"error": "Please enter a job description"}, indent=2)
|
| 181 |
-
|
| 182 |
-
resume_text = process_uploaded_file(file)
|
| 183 |
-
result = analyze_with_mistral(resume_text, job_description)
|
| 184 |
-
|
| 185 |
-
# Return as formatted JSON string for better display
|
| 186 |
-
return json.dumps(result, indent=2, ensure_ascii=False)
|
| 187 |
-
|
| 188 |
-
except Exception as e:
|
| 189 |
-
return json.dumps({"error": f"Error analyzing resume: {str(e)}"}, indent=2)
|
| 190 |
-
|
| 191 |
-
# Gradio interface
|
| 192 |
-
with gr.Blocks(fill_height=True, title="Smart ATS Resume Analyzer") as demo:
|
| 193 |
-
with gr.Sidebar():
|
| 194 |
-
gr.Markdown("# Smart ATS Resume Analyzer")
|
| 195 |
-
gr.Markdown("Upload your resume (PDF/Word) and enter a job description to get an ATS compatibility score.")
|
| 196 |
-
|
| 197 |
-
with gr.Row():
|
| 198 |
-
with gr.Column(scale=1):
|
| 199 |
-
resume_upload = gr.File(
|
| 200 |
-
label="Upload Resume (PDF or Word)",
|
| 201 |
-
file_types=[".pdf", ".docx"],
|
| 202 |
-
type="filepath"
|
| 203 |
-
)
|
| 204 |
-
job_desc = gr.Textbox(label="Job Description", lines=10, placeholder="Paste the job description here...")
|
| 205 |
-
submit_btn = gr.Button("Analyze Resume")
|
| 206 |
-
with gr.Column(scale=2):
|
| 207 |
-
output = gr.JSON(label="ATS Analysis Result")
|
| 208 |
-
|
| 209 |
-
submit_btn.click(
|
| 210 |
-
fn=analyze_resume,
|
| 211 |
-
inputs=[resume_upload, job_desc],
|
| 212 |
-
outputs=output
|
| 213 |
-
)
|
| 214 |
-
|
| 215 |
-
demo.launch()
|
|
|
|
| 1 |
+
messages = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
{
|
| 3 |
"role": "system",
|
| 4 |
+
"content": "You are an AI expert in ATS resume analysis. Your task is to analyze a resume against a job description for ATS compatibility and return the result in a specific JSON format."
|
| 5 |
},
|
| 6 |
{
|
| 7 |
"role": "user",
|
| 8 |
"content": """
|
| 9 |
+
Analyze the provided resume against the job description for ATS compatibility. Assess keywords, formatting, skills, experience relevance, and education. Return the result by filling in the EXACT JSON template below with appropriate values and recommendations. Output ONLY the completed JSON, with no additional text, comments, or explanations. Ensure all fields are populated, even if with empty lists or default scores.
|
| 10 |
|
| 11 |
+
JSON Template to Fill:
|
| 12 |
{
|
| 13 |
"ATS Parameters": {
|
| 14 |
"Keywords": {
|
| 15 |
+
"Match": <integer 0-100>,
|
| 16 |
+
"Recommendation": [<list of 0-5 strings>]
|
| 17 |
},
|
| 18 |
"Formatting": {
|
| 19 |
+
"Match": <integer 0-100>,
|
| 20 |
+
"Recommendation": [<list of 0-5 strings>]
|
| 21 |
},
|
| 22 |
"Skills Match": {
|
| 23 |
+
"Match": <integer 0-100>,
|
| 24 |
+
"Recommendation": [<list of 0-5 strings>]
|
| 25 |
},
|
| 26 |
"Experience Relevance": {
|
| 27 |
+
"Match": <integer 0-100>,
|
| 28 |
+
"Recommendation": [<list of 0-5 strings>]
|
| 29 |
},
|
| 30 |
"Education": {
|
| 31 |
+
"Match": <integer 0-100>,
|
| 32 |
+
"Recommendation": [<list of 0-5 strings>]
|
| 33 |
}
|
| 34 |
},
|
| 35 |
"Score": {
|
| 36 |
+
"Keywords": <integer 0-100>,
|
| 37 |
+
"Formatting": <integer 0-100>,
|
| 38 |
+
"Skills Match": <integer 0-100>,
|
| 39 |
+
"Experience Relevance": <integer 0-100>,
|
| 40 |
+
"Education": <integer 0-100>,
|
| 41 |
+
"Overall": <integer 0-100, average of above scores>
|
| 42 |
}
|
| 43 |
}
|
| 44 |
|
| 45 |
Rules:
|
| 46 |
+
- Replace <integer 0-100> with an integer score between 0 and 100.
|
| 47 |
+
- Replace <list of 0-5 strings> with a list of 0 to 5 concise string recommendations (e.g., [] if none).
|
| 48 |
+
- "Overall" is the rounded average of the five "Score" values.
|
| 49 |
+
- Base analysis on ATS requirements (keyword density, simple formatting, etc.) and the job description.
|
| 50 |
+
- Output ONLY the filled JSON template, nothing else.
|
|
|
|
| 51 |
|
| 52 |
Resume:
|
| 53 |
{resume_text}
|
|
|
|
| 56 |
{job_description}
|
| 57 |
"""
|
| 58 |
}
|
| 59 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|