|
|
import os |
|
|
import json |
|
|
from typing import List |
|
|
from urllib.parse import quote |
|
|
import logging |
|
|
import gradio as gr |
|
|
import pandas as pd |
|
|
from server import cv_processor, job_processor, applicant_evaluator |
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
logging.basicConfig(level=logging.INFO) |
|
|
|
|
|
|
|
|
logo_base64 = cv_processor.encode_base64( |
|
|
"static/AIRecruiterAgent.png" |
|
|
) |
|
|
|
|
|
def evaluate_applicants( |
|
|
cv_files: List[str], |
|
|
job_description: str, |
|
|
|
|
|
) -> pd.DataFrame: |
|
|
""" |
|
|
Evaluate applicants' CVs against the job description. |
|
|
|
|
|
Parameters |
|
|
---------- |
|
|
cv_files: List[str] |
|
|
List of CV file paths to evaluate. |
|
|
job_description: str |
|
|
The job description text to evaluate against. |
|
|
|
|
|
Returns |
|
|
------- |
|
|
pd.DataFrame: DataFrame containing evaluation results with match scores and reasoning. |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.info("Getting job annotation from job description.") |
|
|
job_annotation = job_processor.get_job_content(job_description) |
|
|
|
|
|
evaluation_res = [] |
|
|
for cv_file in cv_files: |
|
|
|
|
|
logger.info("Getting cv annotation from CV file: %s", cv_file.name) |
|
|
cv_annotation = cv_processor.get_cv_content(cv_file.name) |
|
|
|
|
|
|
|
|
logger.info("Evaluating applicant CV against job description.") |
|
|
res = applicant_evaluator.evaluate_applicant( |
|
|
cv_annotation["cv"]["annotation"], |
|
|
job_annotation["job"]["annotation"] |
|
|
) |
|
|
evaluation = json.loads(res["evaluation"]) |
|
|
cv_base64 = cv_processor.encode_base64(cv_file.name) |
|
|
|
|
|
score = float(evaluation["match_score"]) |
|
|
if score >= 0.8: |
|
|
match_labels = "Strong Matched" |
|
|
elif score >= 0.5: |
|
|
match_labels = "Partially Matched" |
|
|
else: |
|
|
match_labels = "Not Matched" |
|
|
evaluation_res.append( |
|
|
{ |
|
|
"Applicant": os.path.basename(cv_file.name), |
|
|
"Match Score": evaluation["match_score"], |
|
|
"Match Labels": match_labels, |
|
|
"Match Reasoning": evaluation["match_reasoning"], |
|
|
"CV Base64": cv_base64, |
|
|
"CV Url": f"gradio_api/file={cv_file.name}" |
|
|
} |
|
|
) |
|
|
|
|
|
evaluation_res = sorted( |
|
|
evaluation_res, |
|
|
key=lambda d: d['Match Score'], |
|
|
reverse=True |
|
|
) |
|
|
return pd.DataFrame.from_records(evaluation_res) |
|
|
|
|
|
|
|
|
def df_select_callback(df: pd.DataFrame, evt: gr.SelectData): |
|
|
selected_row = evt.row_value |
|
|
if not selected_row: |
|
|
return "No row selected.", "" |
|
|
|
|
|
match_score = selected_row[1] |
|
|
match_labels = selected_row[2] |
|
|
match_reasoning = selected_row[3] |
|
|
|
|
|
cv_base64 = selected_row[4] |
|
|
pdf_url = selected_row[5] |
|
|
pdf_encoded_url = f"https://agents-mcp-hackathon-airecruiteragent.hf.space/{pdf_url}" |
|
|
|
|
|
if cv_base64: |
|
|
pdf_encoded = gr.HTML( |
|
|
""" |
|
|
<!-- The Modal --> |
|
|
<div id="myModal" class="modal" style="display: none;"> |
|
|
|
|
|
<!-- Modal content --> |
|
|
<div class="modal-content"> |
|
|
<div class="modal-header"> |
|
|
<span class="close" onclick="hide_pdfviewer()">×</span> |
|
|
<h2>Candidate CV</h2> |
|
|
</div> |
|
|
<div class="modal-body"> |
|
|
|
|
|
<div id="my-pdf" class="pdfobject-container" style="height: 100%"> |
|
|
<object data="data:application/pdf;base64,{cv_base64}" type="application/pdf" style="height: 100%; width: 100%;"> |
|
|
<iframe src="https://docs.google.com/viewer?url={pdf_encoded_url}&embedded=true" style="height: 100%; width: 100%;" frameborder="0" allow="accelerometer; ambient-light-sensor; autoplay; battery; camera; document-domain; encrypted-media; fullscreen; geolocation; gyroscope; layout-animations; legacy-image-formats; magnetometer; microphone; midi; oversized-images; payment; picture-in-picture; publickey-credentials-get; sync-xhr; usb; vr ; wake-lock; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-downloads"></iframe> |
|
|
</object> |
|
|
</div> |
|
|
</div> |
|
|
<div class="modal-footer"> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
""".format( |
|
|
title=selected_row[0], |
|
|
cv_base64=cv_base64, |
|
|
pdf_url=pdf_url, |
|
|
pdf_encoded_url=pdf_encoded_url |
|
|
), |
|
|
label="CV PDF Viewer", |
|
|
elem_id="pdf_viewer", |
|
|
min_height="0px", |
|
|
max_height="100%", |
|
|
visible=True, |
|
|
) |
|
|
else: |
|
|
pdf_encoded = gr.HTML( |
|
|
"", |
|
|
label="CV PDF Viewer", |
|
|
elem_id="pdf_viewer", |
|
|
min_height="0px", |
|
|
max_height="100%", |
|
|
visible=False, |
|
|
) |
|
|
|
|
|
return match_reasoning, pdf_encoded |
|
|
|
|
|
|
|
|
head = """ |
|
|
<script src="https://unpkg.com/[email protected]/pdfobject.min.js"></script> |
|
|
<script type="text/javascript"> |
|
|
function hide_pdfviewer(){ |
|
|
document.getElementById('myModal').style.display="none"; |
|
|
} |
|
|
|
|
|
function show_pdfviewer(){ |
|
|
document.getElementById('myModal').style.display="block"; |
|
|
} |
|
|
</script> |
|
|
""" |
|
|
|
|
|
css = """ |
|
|
.logo-container { |
|
|
width:100%; |
|
|
height:auto; |
|
|
padding:1%; |
|
|
} |
|
|
|
|
|
.logo-img { |
|
|
margin-left:2%; |
|
|
float:left; |
|
|
height:40px; |
|
|
width:40px; |
|
|
} |
|
|
|
|
|
/* The Modal (background) */ |
|
|
.modal { |
|
|
display: none; |
|
|
position: fixed; |
|
|
z-index: 1000; /* Sit on top */ |
|
|
padding-top: 100px; |
|
|
left: 0; |
|
|
top: 0; |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
overflow: auto; /* Enable scroll if needed */ |
|
|
background-color: rgb(0,0,0); /* Fallback color */ |
|
|
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ |
|
|
} |
|
|
|
|
|
/* Modal Content */ |
|
|
.modal-content { |
|
|
position: relative; |
|
|
background-color: #fefefe; |
|
|
margin: auto; |
|
|
padding: 0; |
|
|
border: 1px solid #888; |
|
|
width: 80%; |
|
|
height: 90%; |
|
|
overflow: auto; |
|
|
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19); |
|
|
-webkit-animation-name: animatetop; |
|
|
-webkit-animation-duration: 0.4s; |
|
|
animation-name: animatetop; |
|
|
animation-duration: 0.4s |
|
|
} |
|
|
|
|
|
/* Add Animation */ |
|
|
@-webkit-keyframes animatetop { |
|
|
from {top:-300px; opacity:0} |
|
|
to {top:0; opacity:1} |
|
|
} |
|
|
|
|
|
@keyframes animatetop { |
|
|
from {top:-300px; opacity:0} |
|
|
to {top:0; opacity:1} |
|
|
} |
|
|
|
|
|
/* The Close Button */ |
|
|
.close { |
|
|
color: white; |
|
|
float: right; |
|
|
font-size: 28px; |
|
|
font-weight: bold; |
|
|
} |
|
|
|
|
|
.close:hover, |
|
|
.close:focus { |
|
|
color: #000; |
|
|
text-decoration: none; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.modal-header { |
|
|
padding: 2px 16px; |
|
|
background-color: #5cb85c; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.modal-body { |
|
|
padding: 2px 16px; |
|
|
height: 90%; |
|
|
} |
|
|
|
|
|
.modal-footer { |
|
|
padding: 2px 16px; |
|
|
background-color: #5cb85c; |
|
|
color: white; |
|
|
} |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with gr.Blocks(head=head, css=css) as demo_app: |
|
|
|
|
|
with gr.Row(): |
|
|
gr.HTML( |
|
|
""" |
|
|
<div style="text-align:center; margin-bottom: 10px;"> |
|
|
<div class="logo-container" style="display: inline-block; text-align: center;"> |
|
|
<img src="data:image/png;base64,{logo_base64}" alt="AI Recruiter Agent" style="height: 100px; width: auto; " class="logo-img"> |
|
|
<h1 style="color: #4A90E2; font-size: 2.5em;"> |
|
|
AI Recruiter Agent |
|
|
</h1> |
|
|
<h3>Revolutionizing recruitment with AI-driven insights.</h3> |
|
|
</div> |
|
|
|
|
|
</div> |
|
|
""".format(logo_base64=logo_base64) |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
|
|
|
with gr.Column(scale=1): |
|
|
gr.Markdown("### Upload Resumes and Job Description") |
|
|
gr.Markdown( |
|
|
"Upload multiple resumes in PDF format and provide the job description text for evaluation." |
|
|
) |
|
|
|
|
|
cv_files = gr.Files( |
|
|
file_count="multiple", |
|
|
file_types=[".pdf"], |
|
|
label="Upload Candidate Resume files", |
|
|
height="150px", |
|
|
) |
|
|
job_description = gr.TextArea( |
|
|
placeholder="Enter Job Description text here...", |
|
|
label="Job Description", |
|
|
lines=12, |
|
|
max_lines=12, |
|
|
) |
|
|
with gr.Row(): |
|
|
|
|
|
start_over_button = gr.Button("Start Over", variant="secondary") |
|
|
submit_button = gr.Button("Match Applicants", variant="primary") |
|
|
|
|
|
|
|
|
with gr.Column(scale=1): |
|
|
gr.Markdown("### Evaluation Results") |
|
|
gr.Markdown( |
|
|
"Click on the results to view detailed evaluation of each applicant against the job description." |
|
|
) |
|
|
|
|
|
result_df = gr.DataFrame( |
|
|
headers=[ |
|
|
"Applicant", |
|
|
"Match Score", |
|
|
"Match Labels", |
|
|
], |
|
|
|
|
|
elem_id="myResult", |
|
|
type="pandas", |
|
|
interactive=False, |
|
|
show_row_numbers=True, |
|
|
column_widths=["50%", "25%", "25%", "0%", "0%", "0%"], |
|
|
wrap=False, |
|
|
) |
|
|
|
|
|
result_detail_textbox = gr.Textbox( |
|
|
label="Detailed Evaluation", |
|
|
placeholder="Click on a row to see detailed evaluation.", |
|
|
lines=8, |
|
|
max_lines=8, |
|
|
) |
|
|
show_pdf_button = gr.Button( |
|
|
"Show Selected Candidate CV", |
|
|
variant="secondary", |
|
|
elem_id="show_pdf_button", |
|
|
) |
|
|
|
|
|
pdf_viewer = gr.HTML( |
|
|
"", |
|
|
label="CV PDF Viewer", |
|
|
elem_id="pdf_viewer", |
|
|
min_height="0px", |
|
|
max_height="100%", |
|
|
visible=False, |
|
|
) |
|
|
|
|
|
|
|
|
def reset_fields(): |
|
|
return ( |
|
|
[], "", [], "", |
|
|
gr.HTML( |
|
|
"", |
|
|
label="CV PDF Viewer", |
|
|
elem_id="pdf_viewer", |
|
|
min_height="0px", |
|
|
max_height="100%", |
|
|
visible=False, |
|
|
) |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
start_over_button.click( |
|
|
fn=reset_fields, |
|
|
inputs=None, |
|
|
outputs=[ |
|
|
cv_files, |
|
|
job_description, |
|
|
result_df, |
|
|
result_detail_textbox, |
|
|
pdf_viewer |
|
|
] |
|
|
) |
|
|
submit_button.click( |
|
|
fn=evaluate_applicants, inputs=[cv_files, job_description], outputs=[result_df] |
|
|
) |
|
|
result_df.select( |
|
|
fn=df_select_callback, |
|
|
inputs=[result_df], |
|
|
outputs=[result_detail_textbox, pdf_viewer], |
|
|
) |
|
|
|
|
|
show_pdf_button.click( |
|
|
fn=lambda: gr.update(visible=True), |
|
|
inputs=None, |
|
|
outputs=pdf_viewer, |
|
|
js="show_pdfviewer()", |
|
|
) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo_app.launch( |
|
|
mcp_server=True, |
|
|
allowed_paths=["static"] |
|
|
) |
|
|
|