File size: 9,794 Bytes
6689b55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ecad0ee
2c36e48
 
 
 
 
ecad0ee
 
 
 
2c36e48
ecad0ee
 
2c36e48
 
 
 
 
 
 
ecad0ee
 
2c36e48
6689b55
ecad0ee
 
 
2c36e48
 
 
 
ecad0ee
6689b55
ecad0ee
 
2c36e48
 
 
ecad0ee
6689b55
2c36e48
6689b55
 
 
ed99897
 
2c36e48
ed99897
 
 
 
2c36e48
ed99897
 
6689b55
ecad0ee
 
 
 
fa5ad43
 
eeed7c6
 
b8d7985
6689b55
2c36e48
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
import gradio as gr
import os
import zipfile
import json
import pandas as pd
import datetime
import shutil
import hashlib
from io import StringIO
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
from pdfminer.pdfpage import PDFPage
from PyPDF2 import PdfReader
from openai import OpenAI, RateLimitError
import backoff
import re

class PDFUtils:
    @staticmethod
    def extract_text_with_pdfminer(path):
        rsrcmgr = PDFResourceManager()
        retstr = StringIO()
        codec = 'utf-8'
        laparams = LAParams()
        device = TextConverter(rsrcmgr, retstr, codec=codec, laparams=laparams)

        with open(path, 'rb') as fp:
            interpreter = PDFPageInterpreter(rsrcmgr, device)
            for page in PDFPage.get_pages(fp, check_extractable=True):
                interpreter.process_page(page)

        text = retstr.getvalue()
        return text

    @staticmethod
    def extract_text_with_pypdf2(path):
        reader = PdfReader(path)
        text = ''.join(page.extract_text() for page in reader.pages)
        return text

    @staticmethod
    def convert_pdf_to_text(path):
        text = PDFUtils.extract_text_with_pdfminer(path)
        if text is None:
            print("Processing using PyPDF2")
            text = PDFUtils.extract_text_with_pypdf2(path)
        return text

class OpenAIClassifier:
    def __init__(self, api_key):
        self.client = OpenAI(api_key=api_key)
        self.processed_resumes = set()

    @backoff.on_exception(backoff.expo, RateLimitError) 
    def completions_with_backoff(self, **kwargs):
        return self.client.chat.completions.create(**kwargs)

    def hash_resume_text(self, resume_text):
        return hashlib.md5(resume_text.encode()).hexdigest()

    def classify_resume(self, resume_text, questions_string):
        resume_hash = self.hash_resume_text(resume_text)
        if resume_hash in self.processed_resumes:
            return "Already Processed"
        
        response = self.client.chat.completions.create(
            model="gpt-3.5-turbo",
            messages=[
                {"role": "system", "content": f"As a very strict hiring manager who is evaluating a resume. Answer each question, let questions be keys and answers be values {questions_string} As a hiring manager parse through the resume thoroughly and answer the following questions with the same index in dictionary format. In the experience section, if there is no direct reference to the number of years of experience, count the number of years from the least present year in the experience section."},
                {"role": "user", "content": resume_text}
            ],
            temperature=0.7,
            max_tokens=516
        )

        summary = str(response.choices[0].message)
        self.processed_resumes.add(resume_hash)

        # Extract JSON substring using regular expression
        json_match = re.search(r"\{.*\}", summary)
        if json_match:
            return json_match.group(0)  # Return only the JSON part
        else:
            return "{}"

def read_json_file(file_path):
    with open(file_path, 'r') as json_file:
        json_data = json.load(json_file)
        return json.dumps(json_data, indent=2)

def process_text(text):
    return text.replace('\\n', '<br>') if text else "Error: Text is None"

def process_pdf(file, model_choice, openai_classifier, questions_string):
    data = PDFUtils.convert_pdf_to_text(file)
    if model_choice == "openai":
        classification = openai_classifier.classify_resume(data, questions_string)
        print(f"Debug: classification output:\n{classification}\n")  # Debugging line to confirm JSON structure
        return {"file_path": file, "result": process_text(classification)}
    else:
        print("Only openai can be utilized")

def unzip_and_process(zip_file, model_choice, openai_classifier, questions_string):
    extract_folder, selected_folder, rejected_folder, error_folder = 'extracted_files/', 'extracted_files/Selected/', 'extracted_files/Rejected/', 'extracted_files/Error/'
    os.makedirs(extract_folder, exist_ok=True)
    os.makedirs(selected_folder, exist_ok=True)
    os.makedirs(rejected_folder, exist_ok=True)
    os.makedirs(error_folder, exist_ok=True)

    for root, dirs, files in os.walk(extract_folder):
        for file_name in files:
            os.remove(os.path.join(root, file_name))

    with zipfile.ZipFile(zip_file, 'r') as zip_ref:
        zip_ref.extractall(extract_folder)

    df_results = pd.DataFrame()
    questions_dict = json.loads(questions_string)
    questions_list = list(questions_dict.values())
    df_results = pd.DataFrame(columns=questions_list)

    for root, dirs, files in os.walk(extract_folder):
        for file_name in files:
            file_path = os.path.join(root, file_name)
            if file_path.lower().endswith('.pdf'):
                try:
                    result = process_pdf(file_path, model_choice, openai_classifier, questions_string)

                    # If already processed, skip this file
                    if result['result'] == "Already Processed":
                        print(f"{file_name} is already processed. Skipping.")
                        continue

                    cleaned_data_str = result['result'].replace("<br>", "").replace("\\", "")
                    
                    # Attempt to parse JSON data
                    data_dict = json.loads(cleaned_data_str)
                    df_resume = pd.DataFrame(data_dict.items(), columns=["Key", os.path.basename(file_path)])
                    df_resume["Key"] = df_resume["Key"].replace(questions_dict)
                    df_results = pd.concat([df_results, df_resume.set_index(["Key"]).T.reset_index(drop=True)], axis=0, ignore_index=True)

                    # Decide on the destination based on the final selection key
                    selection_rejection = data_dict.get(list(data_dict.keys())[-1])
                    destination = selected_folder if selection_rejection == "Selected" else rejected_folder if selection_rejection == "Rejected" else error_folder
                    shutil.move(file_path, os.path.join(destination, os.path.basename(file_path)))

                except json.JSONDecodeError as e:
                    print(f"JSON decoding error for {file_path}: {e}")
                    shutil.move(file_path, os.path.join(error_folder, file_name))
                except Exception as ex:
                    print(f"Error processing file {file_path}: {ex}")
                    shutil.move(file_path, os.path.join(error_folder, file_name))
            else:
                shutil.move(file_path, os.path.join(error_folder, file_name))
                print(f"File '{file_name}' is not a PDF. Moved to error directory.")
    
    # Save processed results to an Excel file
    timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
    excel_file = f'extracted_files/output_results_{timestamp}.xlsx'
    df_results.to_excel(excel_file, index=False)
    print(f"Processed resumes saved to {excel_file}")
    return excel_file  # Return the path to the Excel file

def run_interface(questions_json, resumes_zip):
    try:
        # Handle the questions.json file content
        if isinstance(questions_json, bytes):
            # Parse bytes directly as JSON string
            questions_string = questions_json.decode('utf-8')
        elif isinstance(questions_json, str):
            # If it's a file path
            with open(questions_json, 'r') as f:
                questions_string = f.read()
        else:
            raise ValueError("Unexpected questions_json type")

        # Handle the ZIP file
        if isinstance(resumes_zip, bytes):
            # Write bytes to a temporary zip file
            temp_zip_path = "temp_uploaded_resumes.zip"
            with open(temp_zip_path, "wb") as f:
                f.write(resumes_zip)
            zip_path = temp_zip_path
        elif isinstance(resumes_zip, str):
            zip_path = resumes_zip
        else:
            raise ValueError("Unexpected resumes_zip type")

        openai_classifier = OpenAIClassifier(api_key="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")
        output_file = unzip_and_process(zip_path, "openai", openai_classifier, questions_string)
        
        # Clean up temporary zip file if it was created
        if isinstance(resumes_zip, bytes) and os.path.exists(temp_zip_path):
            os.remove(temp_zip_path)
            
        return output_file

    except Exception as e:
        print(f"Error in run_interface: {str(e)}")
        # Clean up temporary files in case of error
        if 'temp_zip_path' in locals() and os.path.exists(temp_zip_path):
            os.remove(temp_zip_path)
        raise gr.Error(f"Processing failed: {str(e)}")

# Set up the Gradio interface
interface = gr.Interface(
    fn=run_interface,
    inputs=[
        gr.File(
            label="Upload questions.json", 
            type="binary",
            file_types=[".json"]
        ),
        gr.File(
            label="Upload ZIP of Resumes", 
            type="binary",
            file_types=[".zip"]
        )
    ],
    outputs=gr.File(label="Processed Results Excel"),
    title="Resume Analysis System",
    description="Upload a questions.json file and a ZIP file containing resumes to process.",
    allow_flagging="never",
    examples=[
        ["questions.json", "to_be_screened_2.zip"]
    ],
    cache_examples=False
) 

# Launch with debugging and sharing enabled
interface.launch(debug=True, share=True)