Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -834,7 +834,7 @@ def process_pdf(file):
|
|
| 834 |
esco_occ_future = executor.submit(classify_esco_by_hierarchical_level, responsibilities)
|
| 835 |
qualification_future = executor.submit(extract_qualification, responsibilities)
|
| 836 |
skills_future = executor.submit(extract_skills, responsibilities)
|
| 837 |
-
|
| 838 |
job_family = job_family_future.result()
|
| 839 |
occ_group = occ_group_future.result()
|
| 840 |
esco_occ = esco_occ_future.result()
|
|
@@ -843,16 +843,10 @@ def process_pdf(file):
|
|
| 843 |
|
| 844 |
log_debug(f"Identified {job_family}")
|
| 845 |
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
has_esco = esco_occ.get("Level_5_ESCO_code") is not None
|
| 849 |
-
skill_esco_extract = []
|
| 850 |
-
skill_esco_map = []
|
| 851 |
-
if has_esco:
|
| 852 |
-
Level_5_code = esco_occ["Level_5_ESCO_code"]
|
| 853 |
-
skill_esco_extract = review_skills(Level_5_code)
|
| 854 |
-
skill_esco_map = map_proficiency_and_assessment(skill_esco_extract, responsibilities)
|
| 855 |
|
|
|
|
|
|
|
| 856 |
time.sleep(6)
|
| 857 |
assessment_lookup = {item['skill_name']: item for item in skill_map}
|
| 858 |
joined_skills = [
|
|
@@ -870,6 +864,43 @@ def process_pdf(file):
|
|
| 870 |
for skill in skills
|
| 871 |
]
|
| 872 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 873 |
# Prepare all data for JSON output
|
| 874 |
result_data = {
|
| 875 |
"file_name": os.path.basename(file.name),
|
|
@@ -881,18 +912,8 @@ def process_pdf(file):
|
|
| 881 |
"interview_questions": build_interview(responsibilities, skills),
|
| 882 |
"skills": joined_skills,
|
| 883 |
"esco_levels": {f"Level_{i}_ESCO_{field}": esco_occ.get(f"Level_{i}_ESCO_{field}")
|
| 884 |
-
for i in range(1,
|
| 885 |
-
"esco_skills":
|
| 886 |
-
"skills": [
|
| 887 |
-
{
|
| 888 |
-
"skill_name": skill["skill_name"],
|
| 889 |
-
"skill_description": skill["skill_description"],
|
| 890 |
-
"skill_code": skill["skill_code"],
|
| 891 |
-
**assessment_esco_lookup.get(skill["skill_name"], {})
|
| 892 |
-
}
|
| 893 |
-
for skill in (skill_esco_extract if has_esco else [])
|
| 894 |
-
]
|
| 895 |
-
},
|
| 896 |
"processing_time": time.strftime("%Y-%m-%d %H:%M:%S")
|
| 897 |
}
|
| 898 |
|
|
@@ -902,7 +923,7 @@ def process_pdf(file):
|
|
| 902 |
json_path = f.name
|
| 903 |
log_debug(f"Results saved to temporary JSON file: {json_path}")
|
| 904 |
|
| 905 |
-
# Format outputs for display
|
| 906 |
formatted_skills = format_skill_cards(joined_skills)
|
| 907 |
formatted_ccog = format_ccog_card(result_data['ccoq_levels'])
|
| 908 |
formatted_esco_levels = format_esco_card(result_data['esco_levels'])
|
|
@@ -914,11 +935,11 @@ def process_pdf(file):
|
|
| 914 |
job_family,
|
| 915 |
"\n".join(qualification),
|
| 916 |
formatted_ccog,
|
| 917 |
-
"\n".join(
|
| 918 |
formatted_skills,
|
| 919 |
formatted_esco_levels,
|
| 920 |
formatted_esco_skills,
|
| 921 |
-
|
| 922 |
json_path # Return path to JSON file
|
| 923 |
)
|
| 924 |
|
|
@@ -939,6 +960,8 @@ def process_pdf(file):
|
|
| 939 |
error_message,
|
| 940 |
None # No JSON path on error
|
| 941 |
)
|
|
|
|
|
|
|
| 942 |
# ================= Build Word Report =================
|
| 943 |
from docx import Document
|
| 944 |
import os
|
|
@@ -947,34 +970,37 @@ import time
|
|
| 947 |
import tempfile
|
| 948 |
from typing import Dict, List, Union
|
| 949 |
|
| 950 |
-
def
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
esco_skills: Dict
|
| 960 |
-
) -> str:
|
| 961 |
-
"""
|
| 962 |
-
Generate a comprehensive Word document from analysis results with multiple fallback mechanisms.
|
| 963 |
|
| 964 |
-
Args:
|
| 965 |
-
file_name: Original PDF filename
|
| 966 |
-
responsibilities: Extracted responsibilities text
|
| 967 |
-
job_family: Identified job family
|
| 968 |
-
qualification: Required qualifications
|
| 969 |
-
ccoq_levels: CCOG classification levels
|
| 970 |
-
interview: Generated interview questions
|
| 971 |
-
skills: List of required skills
|
| 972 |
-
esco_levels: ESCO classification levels
|
| 973 |
-
esco_skills: ESCO mapped skills
|
| 974 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 975 |
Returns:
|
| 976 |
Path to the generated Word document
|
| 977 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 978 |
# Initialize document with metadata
|
| 979 |
doc = Document()
|
| 980 |
doc.core_properties.author = "IOM Talent Management System"
|
|
@@ -983,10 +1009,10 @@ def generate_word_document(
|
|
| 983 |
# Default values for all fields
|
| 984 |
default_values = {
|
| 985 |
"file": "Unknown file",
|
| 986 |
-
"responsibilities": "No responsibilities extracted",
|
| 987 |
-
"classified_job_family": "No job family identified",
|
| 988 |
-
"qualification": ["No qualification information available"],
|
| 989 |
-
"interview": ["No interview questions generated"],
|
| 990 |
"skills": {"skills": [{"skill_name": "No skills identified", "description": "", "code": ""}]},
|
| 991 |
"skills_esco": {"skills": [{"skill_name": "No ESCO skills identified", "description": "", "code": ""}]}
|
| 992 |
}
|
|
@@ -994,13 +1020,15 @@ def generate_word_document(
|
|
| 994 |
# Safely build the result dictionary with fallbacks
|
| 995 |
try:
|
| 996 |
result = {
|
| 997 |
-
"file":
|
| 998 |
-
"responsibilities": responsibilities
|
| 999 |
-
"classified_job_family": job_family
|
| 1000 |
-
"qualification":
|
| 1001 |
-
"interview":
|
| 1002 |
-
"skills":
|
| 1003 |
-
"skills_esco":
|
|
|
|
|
|
|
| 1004 |
}
|
| 1005 |
|
| 1006 |
# Add level information with validation
|
|
@@ -1014,115 +1042,49 @@ def generate_word_document(
|
|
| 1014 |
log_debug(f"Error building result dictionary: {str(e)}")
|
| 1015 |
result = default_values
|
| 1016 |
|
| 1017 |
-
#
|
| 1018 |
try:
|
| 1019 |
# Document header
|
| 1020 |
doc.add_heading('Job Description Analysis Report', level=0)
|
| 1021 |
doc.add_paragraph(f"Generated on {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
| 1022 |
doc.add_paragraph("International Organization for Migration", style="Intense Quote")
|
| 1023 |
|
| 1024 |
-
# Metadata table
|
| 1025 |
-
table = doc.add_table(rows=1, cols=2)
|
| 1026 |
-
table.style = 'Light Shading Accent 1'
|
| 1027 |
-
hdr_cells = table.rows[0].cells
|
| 1028 |
-
hdr_cells[0].text = 'Field'
|
| 1029 |
-
hdr_cells[1].text = 'Value'
|
| 1030 |
-
|
| 1031 |
-
def _add_table_row(table, field, value):
|
| 1032 |
-
row = table.add_row().cells
|
| 1033 |
-
row[0].text = field
|
| 1034 |
-
row[1].text = str(value or "Not available")
|
| 1035 |
-
|
| 1036 |
-
_add_table_row(table, "File Name", result["file"])
|
| 1037 |
-
_add_table_row(table, "Job Family", result["classified_job_family"])
|
| 1038 |
-
|
| 1039 |
-
# Section generator with error handling
|
| 1040 |
-
def _add_section(heading, content, level=2):
|
| 1041 |
-
doc.add_heading(heading, level=level)
|
| 1042 |
-
if not content:
|
| 1043 |
-
doc.add_paragraph("No information available", style='Subtle Emphasis')
|
| 1044 |
-
return
|
| 1045 |
-
|
| 1046 |
-
if isinstance(content, (list, tuple)):
|
| 1047 |
-
for item in content:
|
| 1048 |
-
if item and str(item).strip():
|
| 1049 |
-
doc.add_paragraph(str(item).strip(), style='List Bullet' if level > 2 else None)
|
| 1050 |
-
elif isinstance(content, dict):
|
| 1051 |
-
for k, v in content.items():
|
| 1052 |
-
if v is not None:
|
| 1053 |
-
doc.add_paragraph(f"{k}: {v}")
|
| 1054 |
-
elif isinstance(content, str):
|
| 1055 |
-
doc.add_paragraph(content)
|
| 1056 |
-
|
| 1057 |
-
# Core sections
|
| 1058 |
-
_add_section("1. Responsibilities", result["responsibilities"])
|
| 1059 |
-
_add_section("2. Qualifications", result["qualification"])
|
| 1060 |
-
|
| 1061 |
-
# Skills sections with robust handling
|
| 1062 |
-
def _add_skills_section(heading, skills_data):
|
| 1063 |
-
doc.add_heading(heading, level=2)
|
| 1064 |
-
if not skills_data or not skills_data.get("skills"):
|
| 1065 |
-
doc.add_paragraph("No skills information available", style='Subtle Emphasis')
|
| 1066 |
-
return
|
| 1067 |
-
|
| 1068 |
-
try:
|
| 1069 |
-
skills_table = doc.add_table(rows=1, cols=4)
|
| 1070 |
-
skills_table.style = 'Medium List 2 Accent 1'
|
| 1071 |
-
hdr = skills_table.rows[0].cells
|
| 1072 |
-
hdr[0].text = 'Skill'
|
| 1073 |
-
hdr[1].text = 'Description'
|
| 1074 |
-
hdr[2].text = 'Proficiency'
|
| 1075 |
-
hdr[3].text = 'Assessment'
|
| 1076 |
-
|
| 1077 |
-
for skill in skills_data["skills"]:
|
| 1078 |
-
if not isinstance(skill, dict):
|
| 1079 |
-
continue
|
| 1080 |
-
|
| 1081 |
-
row = skills_table.add_row().cells
|
| 1082 |
-
row[0].text = str(skill.get("skill_name", "Unnamed skill"))
|
| 1083 |
-
row[1].text = str(skill.get("skill_description", ""))[:100] + ("..." if len(str(skill.get("skill_description", ""))) > 100 else "")
|
| 1084 |
-
row[2].text = str(skill.get("proficiency_level", "Not specified"))
|
| 1085 |
-
row[3].text = str(skill.get("assessment_method", "Not specified"))
|
| 1086 |
-
except Exception as e:
|
| 1087 |
-
doc.add_paragraph(f"Could not display skills table: {str(e)}", style='Subtle Emphasis')
|
| 1088 |
-
|
| 1089 |
-
_add_skills_section("3. Required Skills", result["skills"])
|
| 1090 |
-
_add_skills_section("4. ESCO Mapped Skills", result["skills_esco"])
|
| 1091 |
-
|
| 1092 |
-
# Classification sections
|
| 1093 |
-
def _add_classification_section(heading, prefix, levels=4):
|
| 1094 |
-
doc.add_heading(heading, level=2)
|
| 1095 |
-
found = False
|
| 1096 |
-
for i in range(1, levels+1):
|
| 1097 |
-
code = result.get(f"{prefix}_{i}_code")
|
| 1098 |
-
name = result.get(f"{prefix}_{i}_name")
|
| 1099 |
-
desc = result.get(f"{prefix}_{i}_desc")
|
| 1100 |
-
|
| 1101 |
-
if any([code, name, desc]):
|
| 1102 |
-
found = True
|
| 1103 |
-
doc.add_heading(f"Level {i}", level=3)
|
| 1104 |
-
if code:
|
| 1105 |
-
doc.add_paragraph(f"Code: {code}")
|
| 1106 |
-
if name:
|
| 1107 |
-
doc.add_paragraph(f"Name: {name}")
|
| 1108 |
-
if desc:
|
| 1109 |
-
doc.add_paragraph(f"Description: {desc}")
|
| 1110 |
-
|
| 1111 |
-
if not found:
|
| 1112 |
-
doc.add_paragraph("No classification information available", style='Subtle Emphasis')
|
| 1113 |
-
|
| 1114 |
-
_add_classification_section("5. CCOG Classification", "Level_CCOG")
|
| 1115 |
-
_add_classification_section("6. ESCO Classification", "Level_ESCO", levels=5)
|
| 1116 |
-
|
| 1117 |
-
# Interview questions
|
| 1118 |
-
doc.add_heading("7. Suggested Interview Questions", level=2)
|
| 1119 |
-
if result["interview"] and any(q.strip() for q in result["interview"]):
|
| 1120 |
-
for i, question in enumerate(result["interview"], 1):
|
| 1121 |
-
if question.strip():
|
| 1122 |
-
doc.add_paragraph(f"{i}. {question}", style='List Number')
|
| 1123 |
-
else:
|
| 1124 |
-
doc.add_paragraph("No interview questions generated", style='Subtle Emphasis')
|
| 1125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1126 |
# Footer
|
| 1127 |
doc.add_paragraph()
|
| 1128 |
doc.add_paragraph("Generated by IOM Talent Management AI Tool", style='Footer')
|
|
@@ -1134,7 +1096,7 @@ def generate_word_document(
|
|
| 1134 |
doc.add_heading("Partial Report Generated", level=1)
|
| 1135 |
doc.add_paragraph(f"Some sections could not be generated due to: {str(e)}")
|
| 1136 |
|
| 1137 |
-
#
|
| 1138 |
try:
|
| 1139 |
# Generate appropriate filename
|
| 1140 |
if file_name and isinstance(file_name, str):
|
|
@@ -1167,6 +1129,7 @@ def generate_word_document(
|
|
| 1167 |
error_doc.save(fallback_path)
|
| 1168 |
return fallback_path
|
| 1169 |
|
|
|
|
| 1170 |
# ================= GRADIO INTERFACE =================
|
| 1171 |
with gr.Blocks(
|
| 1172 |
title="AI-powered tool to review Job Position Description",
|
|
@@ -1197,17 +1160,30 @@ css="""
|
|
| 1197 |
/* Gradio layout fixes */
|
| 1198 |
.gradio-container {
|
| 1199 |
max-width: none !important;
|
| 1200 |
-
padding: 0 !important;
|
| 1201 |
}
|
| 1202 |
|
| 1203 |
.gradio-container .gradio-row {
|
| 1204 |
-
|
| 1205 |
-
|
|
|
|
|
|
|
|
|
|
| 1206 |
}
|
| 1207 |
-
|
| 1208 |
.gradio-container .gradio-column {
|
| 1209 |
-
|
| 1210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1211 |
}
|
| 1212 |
|
| 1213 |
/* Set the size of the SVG icon for file download */
|
|
@@ -1363,14 +1339,15 @@ label {
|
|
| 1363 |
box-sizing: border-box;
|
| 1364 |
}
|
| 1365 |
|
|
|
|
|
|
|
| 1366 |
.skills-container {
|
| 1367 |
-
|
| 1368 |
-
|
| 1369 |
-
|
| 1370 |
-
|
| 1371 |
-
|
| 1372 |
-
|
| 1373 |
-
box-sizing: border-box;
|
| 1374 |
}
|
| 1375 |
|
| 1376 |
/* Card styling */
|
|
@@ -1678,6 +1655,15 @@ progress::-webkit-progress-value {
|
|
| 1678 |
padding: 1rem !important;
|
| 1679 |
}
|
| 1680 |
/* Responsive Layout */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1681 |
@media (max-width: 768px) {
|
| 1682 |
.gr-row {
|
| 1683 |
flex-direction: column !important;
|
|
@@ -1789,7 +1775,7 @@ progress::-webkit-progress-value {
|
|
| 1789 |
with gr.Row():
|
| 1790 |
with gr.Column():
|
| 1791 |
gr.Markdown("## Interview Questions")
|
| 1792 |
-
interview_output = gr.Textbox(label="
|
| 1793 |
|
| 1794 |
with gr.Row():
|
| 1795 |
with gr.Column():
|
|
@@ -1824,6 +1810,7 @@ progress::-webkit-progress-value {
|
|
| 1824 |
interactive=False,
|
| 1825 |
elem_classes=["debug-console"]
|
| 1826 |
)
|
|
|
|
| 1827 |
|
| 1828 |
submit_btn.click(
|
| 1829 |
fn=process_pdf,
|
|
@@ -1838,22 +1825,15 @@ progress::-webkit-progress-value {
|
|
| 1838 |
skills_output,
|
| 1839 |
esco_levels_output,
|
| 1840 |
esco_skills_output,
|
| 1841 |
-
debug_console if DEBUG else None
|
|
|
|
| 1842 |
]
|
| 1843 |
)
|
| 1844 |
|
| 1845 |
download_btn.click(
|
| 1846 |
fn=generate_word_document,
|
| 1847 |
inputs=[
|
| 1848 |
-
|
| 1849 |
-
responsibilities_output,
|
| 1850 |
-
job_family_output,
|
| 1851 |
-
qualification_output,
|
| 1852 |
-
ccoq_levels_output,
|
| 1853 |
-
interview_output,
|
| 1854 |
-
skills_output,
|
| 1855 |
-
esco_levels_output,
|
| 1856 |
-
esco_skills_output
|
| 1857 |
],
|
| 1858 |
outputs=gr.File(label="Download the corresponding Word report")
|
| 1859 |
)
|
|
|
|
| 834 |
esco_occ_future = executor.submit(classify_esco_by_hierarchical_level, responsibilities)
|
| 835 |
qualification_future = executor.submit(extract_qualification, responsibilities)
|
| 836 |
skills_future = executor.submit(extract_skills, responsibilities)
|
| 837 |
+
|
| 838 |
job_family = job_family_future.result()
|
| 839 |
occ_group = occ_group_future.result()
|
| 840 |
esco_occ = esco_occ_future.result()
|
|
|
|
| 843 |
|
| 844 |
log_debug(f"Identified {job_family}")
|
| 845 |
|
| 846 |
+
interview = build_interview(responsibilities, skills)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 847 |
|
| 848 |
+
## Map skills from responsibilities
|
| 849 |
+
skill_map = map_proficiency_and_assessment(skills, responsibilities)
|
| 850 |
time.sleep(6)
|
| 851 |
assessment_lookup = {item['skill_name']: item for item in skill_map}
|
| 852 |
joined_skills = [
|
|
|
|
| 864 |
for skill in skills
|
| 865 |
]
|
| 866 |
|
| 867 |
+
|
| 868 |
+
## Generate ESCO skills if we have level 5 mapping....
|
| 869 |
+
has_esco = esco_occ.get("Level_5_ESCO_code") is not None
|
| 870 |
+
skill_esco_extract = []
|
| 871 |
+
skill_esco_map = []
|
| 872 |
+
if has_esco:
|
| 873 |
+
Level_5_code = esco_occ["Level_5_ESCO_code"]
|
| 874 |
+
skill_esco_extract = review_skills(Level_5_code)
|
| 875 |
+
skill_esco_map = map_proficiency_and_assessment(skill_esco_extract, responsibilities)
|
| 876 |
+
else:
|
| 877 |
+
log_debug(f"No Level 5 ESCO code found for {os.path.basename(file.name)}, skipping ESCO skills mapping")
|
| 878 |
+
|
| 879 |
+
joined_skills_esco = []
|
| 880 |
+
if has_esco and skill_esco_extract:
|
| 881 |
+
assessment_esco_lookup = {item['skill_name']: item for item in skill_esco_map}
|
| 882 |
+
joined_skills_esco = [
|
| 883 |
+
{
|
| 884 |
+
"skill_name": skill["skill_name"],
|
| 885 |
+
"skill_description": skill["skill_description"],
|
| 886 |
+
"skill_code": skill["skill_code"],
|
| 887 |
+
**assessment_esco_lookup.get(skill["skill_name"], {})
|
| 888 |
+
}
|
| 889 |
+
for skill in skill_esco_extract
|
| 890 |
+
]
|
| 891 |
+
|
| 892 |
+
if has_esco:
|
| 893 |
+
esco_levels = {f"Level_{i}_ESCO_{field}": esco_occ.get(f"Level_{i}_ESCO_{field}")
|
| 894 |
+
for i in range(1, 6) for field in ["code", "name", "desc"]}
|
| 895 |
+
esco_skills = {
|
| 896 |
+
"skills": joined_skills_esco
|
| 897 |
+
}
|
| 898 |
+
else:
|
| 899 |
+
esco_levels = {f"Level_{i}_ESCO_{field}": None
|
| 900 |
+
for i in range(1, 6) for field in ["code", "name", "desc"]}
|
| 901 |
+
esco_skills = None
|
| 902 |
+
|
| 903 |
+
|
| 904 |
# Prepare all data for JSON output
|
| 905 |
result_data = {
|
| 906 |
"file_name": os.path.basename(file.name),
|
|
|
|
| 912 |
"interview_questions": build_interview(responsibilities, skills),
|
| 913 |
"skills": joined_skills,
|
| 914 |
"esco_levels": {f"Level_{i}_ESCO_{field}": esco_occ.get(f"Level_{i}_ESCO_{field}")
|
| 915 |
+
for i in range(1, 5) for field in ["code", "name", "desc"]},
|
| 916 |
+
"esco_skills": esco_skills,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 917 |
"processing_time": time.strftime("%Y-%m-%d %H:%M:%S")
|
| 918 |
}
|
| 919 |
|
|
|
|
| 923 |
json_path = f.name
|
| 924 |
log_debug(f"Results saved to temporary JSON file: {json_path}")
|
| 925 |
|
| 926 |
+
# Format outputs for display through html cards
|
| 927 |
formatted_skills = format_skill_cards(joined_skills)
|
| 928 |
formatted_ccog = format_ccog_card(result_data['ccoq_levels'])
|
| 929 |
formatted_esco_levels = format_esco_card(result_data['esco_levels'])
|
|
|
|
| 935 |
job_family,
|
| 936 |
"\n".join(qualification),
|
| 937 |
formatted_ccog,
|
| 938 |
+
"\n".join(interview),
|
| 939 |
formatted_skills,
|
| 940 |
formatted_esco_levels,
|
| 941 |
formatted_esco_skills,
|
| 942 |
+
debug_message if DEBUG else None,
|
| 943 |
json_path # Return path to JSON file
|
| 944 |
)
|
| 945 |
|
|
|
|
| 960 |
error_message,
|
| 961 |
None # No JSON path on error
|
| 962 |
)
|
| 963 |
+
|
| 964 |
+
|
| 965 |
# ================= Build Word Report =================
|
| 966 |
from docx import Document
|
| 967 |
import os
|
|
|
|
| 970 |
import tempfile
|
| 971 |
from typing import Dict, List, Union
|
| 972 |
|
| 973 |
+
def create_error_doc(message: str) -> str:
|
| 974 |
+
"""Create a simple Word document with an error message."""
|
| 975 |
+
doc = Document()
|
| 976 |
+
doc.add_heading('Error Generating Report', level=1)
|
| 977 |
+
doc.add_paragraph(message)
|
| 978 |
+
temp_file = tempfile.NamedTemporaryFile(suffix=".docx", delete=False)
|
| 979 |
+
doc.save(temp_file.name)
|
| 980 |
+
return temp_file.name
|
| 981 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 982 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 983 |
|
| 984 |
+
def generate_word_document(json_path: Optional[str]) -> str:
|
| 985 |
+
"""
|
| 986 |
+
Generate a Word document from the analysis results JSON file.
|
| 987 |
+
|
| 988 |
+
Args:
|
| 989 |
+
json_path: Path to the JSON file containing analysis results
|
| 990 |
+
|
| 991 |
Returns:
|
| 992 |
Path to the generated Word document
|
| 993 |
"""
|
| 994 |
+
if not json_path or not os.path.exists(json_path):
|
| 995 |
+
return create_error_doc("No valid analysis data was provided.")
|
| 996 |
+
|
| 997 |
+
try:
|
| 998 |
+
with open(json_path, 'r') as f:
|
| 999 |
+
data = json.load(f)
|
| 1000 |
+
except Exception as e:
|
| 1001 |
+
return create_error_doc(f"Failed to load JSON file: {str(e)}")
|
| 1002 |
+
|
| 1003 |
+
|
| 1004 |
# Initialize document with metadata
|
| 1005 |
doc = Document()
|
| 1006 |
doc.core_properties.author = "IOM Talent Management System"
|
|
|
|
| 1009 |
# Default values for all fields
|
| 1010 |
default_values = {
|
| 1011 |
"file": "Unknown file",
|
| 1012 |
+
"responsibilities": "No responsibilities extracted.",
|
| 1013 |
+
"classified_job_family": "No job family identified.",
|
| 1014 |
+
"qualification": ["No qualification information available."],
|
| 1015 |
+
"interview": ["No interview questions generated."],
|
| 1016 |
"skills": {"skills": [{"skill_name": "No skills identified", "description": "", "code": ""}]},
|
| 1017 |
"skills_esco": {"skills": [{"skill_name": "No ESCO skills identified", "description": "", "code": ""}]}
|
| 1018 |
}
|
|
|
|
| 1020 |
# Safely build the result dictionary with fallbacks
|
| 1021 |
try:
|
| 1022 |
result = {
|
| 1023 |
+
"file": data.get("file", default_values["file"]),
|
| 1024 |
+
"responsibilities": data.get("responsibilities", default_values["responsibilities"]),
|
| 1025 |
+
"classified_job_family": data.get("job_family", default_values["classified_job_family"]),
|
| 1026 |
+
"qualification": data.get("qualification", default_values["qualification"]),
|
| 1027 |
+
"interview": data.get("interview", default_values["interview"]),
|
| 1028 |
+
"skills": data.get("skills", default_values["skills"]),
|
| 1029 |
+
"skills_esco": data.get("skills_esco", default_values["skills_esco"]),
|
| 1030 |
+
"ccog_levels": data.get("ccog_levels", {}),
|
| 1031 |
+
"esco_levels": data.get("esco_levels", {})
|
| 1032 |
}
|
| 1033 |
|
| 1034 |
# Add level information with validation
|
|
|
|
| 1042 |
log_debug(f"Error building result dictionary: {str(e)}")
|
| 1043 |
result = default_values
|
| 1044 |
|
| 1045 |
+
# DOCUMENT CONTENT GENERATION
|
| 1046 |
try:
|
| 1047 |
# Document header
|
| 1048 |
doc.add_heading('Job Description Analysis Report', level=0)
|
| 1049 |
doc.add_paragraph(f"Generated on {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
| 1050 |
doc.add_paragraph("International Organization for Migration", style="Intense Quote")
|
| 1051 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1052 |
|
| 1053 |
+
|
| 1054 |
+
doc.add_heading('Position Description Analysis Report', level=1)
|
| 1055 |
+
doc.add_paragraph(f"File: {result['file']}")
|
| 1056 |
+
doc.add_paragraph(f"Job Family: {result['classified_job_family']}")
|
| 1057 |
+
|
| 1058 |
+
doc.add_heading('Responsibilities', level=2)
|
| 1059 |
+
doc.add_paragraph(result['responsibilities'])
|
| 1060 |
+
|
| 1061 |
+
doc.add_heading('Qualifications', level=2)
|
| 1062 |
+
for item in result['qualification']:
|
| 1063 |
+
doc.add_paragraph(item, style='List Bullet')
|
| 1064 |
+
|
| 1065 |
+
doc.add_heading('Interview Questions', level=2)
|
| 1066 |
+
for item in result['interview']:
|
| 1067 |
+
doc.add_paragraph(item, style='List Bullet')
|
| 1068 |
+
|
| 1069 |
+
doc.add_heading('Skills (Extracted)', level=2)
|
| 1070 |
+
for skill in result['skills'].get("skills", []):
|
| 1071 |
+
doc.add_paragraph(f"{skill.get('skill_name', 'Unnamed Skill')} - {skill.get('description', '')}")
|
| 1072 |
+
|
| 1073 |
+
doc.add_heading('Skills (ESCO)', level=2)
|
| 1074 |
+
for skill in result['skills_esco'].get("skills", []):
|
| 1075 |
+
doc.add_paragraph(f"{skill.get('skill_name', 'Unnamed Skill')} - {skill.get('description', '')}")
|
| 1076 |
+
|
| 1077 |
+
if result["ccog_levels"]:
|
| 1078 |
+
doc.add_heading('C-COG Levels', level=2)
|
| 1079 |
+
for key, value in result["ccog_levels"].items():
|
| 1080 |
+
doc.add_paragraph(f"{key}: {value}")
|
| 1081 |
+
|
| 1082 |
+
if result["esco_levels"]:
|
| 1083 |
+
doc.add_heading('ESCO Levels', level=2)
|
| 1084 |
+
for key, value in result["esco_levels"].items():
|
| 1085 |
+
doc.add_paragraph(f"{key}: {value}")
|
| 1086 |
+
|
| 1087 |
+
|
| 1088 |
# Footer
|
| 1089 |
doc.add_paragraph()
|
| 1090 |
doc.add_paragraph("Generated by IOM Talent Management AI Tool", style='Footer')
|
|
|
|
| 1096 |
doc.add_heading("Partial Report Generated", level=1)
|
| 1097 |
doc.add_paragraph(f"Some sections could not be generated due to: {str(e)}")
|
| 1098 |
|
| 1099 |
+
# FILE SAVING WITH MULTIPLE FALLBACKS
|
| 1100 |
try:
|
| 1101 |
# Generate appropriate filename
|
| 1102 |
if file_name and isinstance(file_name, str):
|
|
|
|
| 1129 |
error_doc.save(fallback_path)
|
| 1130 |
return fallback_path
|
| 1131 |
|
| 1132 |
+
|
| 1133 |
# ================= GRADIO INTERFACE =================
|
| 1134 |
with gr.Blocks(
|
| 1135 |
title="AI-powered tool to review Job Position Description",
|
|
|
|
| 1160 |
/* Gradio layout fixes */
|
| 1161 |
.gradio-container {
|
| 1162 |
max-width: none !important;
|
| 1163 |
+
padding: 0 20px !important;
|
| 1164 |
}
|
| 1165 |
|
| 1166 |
.gradio-container .gradio-row {
|
| 1167 |
+
max-width: 100% !important;
|
| 1168 |
+
margin: 0 auto !important;
|
| 1169 |
+
flex: 1 !important;
|
| 1170 |
+
display: grid !important;
|
| 1171 |
+
grid-template-columns: 1fr !important;
|
| 1172 |
}
|
| 1173 |
+
|
| 1174 |
.gradio-container .gradio-column {
|
| 1175 |
+
min-width: 0 !important;
|
| 1176 |
+
padding: 0 !important;
|
| 1177 |
+
flex: 1 !important;
|
| 1178 |
+
max-width: none !important;
|
| 1179 |
+
}
|
| 1180 |
+
|
| 1181 |
+
/* Ensure the parent container doesn't constrain the grid */
|
| 1182 |
+
.container-wrap {
|
| 1183 |
+
width: 100%;
|
| 1184 |
+
max-width: none !important;
|
| 1185 |
+
padding: 0 !important;
|
| 1186 |
+
margin: 0 !important;
|
| 1187 |
}
|
| 1188 |
|
| 1189 |
/* Set the size of the SVG icon for file download */
|
|
|
|
| 1339 |
box-sizing: border-box;
|
| 1340 |
}
|
| 1341 |
|
| 1342 |
+
|
| 1343 |
+
|
| 1344 |
.skills-container {
|
| 1345 |
+
display: grid;
|
| 1346 |
+
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
| 1347 |
+
gap: 1.5rem;
|
| 1348 |
+
padding: 1rem;
|
| 1349 |
+
width: 100%;
|
| 1350 |
+
margin: 0 auto;
|
|
|
|
| 1351 |
}
|
| 1352 |
|
| 1353 |
/* Card styling */
|
|
|
|
| 1655 |
padding: 1rem !important;
|
| 1656 |
}
|
| 1657 |
/* Responsive Layout */
|
| 1658 |
+
|
| 1659 |
+
/* For larger screens */
|
| 1660 |
+
@media (min-width: 1200px) {
|
| 1661 |
+
.skills-container {
|
| 1662 |
+
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
|
| 1663 |
+
max-width: 1400px;
|
| 1664 |
+
}
|
| 1665 |
+
}
|
| 1666 |
+
|
| 1667 |
@media (max-width: 768px) {
|
| 1668 |
.gr-row {
|
| 1669 |
flex-direction: column !important;
|
|
|
|
| 1775 |
with gr.Row():
|
| 1776 |
with gr.Column():
|
| 1777 |
gr.Markdown("## Interview Questions")
|
| 1778 |
+
interview_output = gr.Textbox(label="", lines=10, interactive=False)
|
| 1779 |
|
| 1780 |
with gr.Row():
|
| 1781 |
with gr.Column():
|
|
|
|
| 1810 |
interactive=False,
|
| 1811 |
elem_classes=["debug-console"]
|
| 1812 |
)
|
| 1813 |
+
temp_json_path = gr.Textbox(label="", interactive=False)
|
| 1814 |
|
| 1815 |
submit_btn.click(
|
| 1816 |
fn=process_pdf,
|
|
|
|
| 1825 |
skills_output,
|
| 1826 |
esco_levels_output,
|
| 1827 |
esco_skills_output,
|
| 1828 |
+
debug_console if DEBUG else None,
|
| 1829 |
+
temp_json_path_ouput
|
| 1830 |
]
|
| 1831 |
)
|
| 1832 |
|
| 1833 |
download_btn.click(
|
| 1834 |
fn=generate_word_document,
|
| 1835 |
inputs=[
|
| 1836 |
+
temp_json_path_ouput
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1837 |
],
|
| 1838 |
outputs=gr.File(label="Download the corresponding Word report")
|
| 1839 |
)
|