root
commited on
Commit
Β·
19f7d68
1
Parent(s):
da61f37
ss
Browse files
app.py
CHANGED
@@ -59,10 +59,7 @@ with st.sidebar:
|
|
59 |
|
60 |
# LLM Settings
|
61 |
st.subheader("LLM Settings")
|
62 |
-
|
63 |
-
if use_llm_explanations:
|
64 |
-
hf_token = st.text_input("Hugging Face Token (optional)", type="password",
|
65 |
-
help="Enter your HF token for better rate limits")
|
66 |
|
67 |
st.markdown("---")
|
68 |
st.markdown("### π€ Advanced Pipeline")
|
@@ -74,7 +71,6 @@ with st.sidebar:
|
|
74 |
st.markdown("### π Models Used")
|
75 |
st.markdown("- **Embedding**: BAAI/bge-large-en-v1.5")
|
76 |
st.markdown("- **Cross-Encoder**: ms-marco-MiniLM-L6-v2")
|
77 |
-
st.markdown("- **LLM Explanations**: Qwen/Qwen3-14B")
|
78 |
st.markdown("- **Intent Analysis**: Qwen/Qwen3-1.7B")
|
79 |
st.markdown("### π Scoring Formula")
|
80 |
st.markdown("**Final Score = Cross-Encoder (0-1) + BM25 (0.1-0.2) + Intent (0-0.3)**")
|
@@ -90,24 +86,10 @@ if 'resume_texts' not in st.session_state:
|
|
90 |
st.session_state.resume_texts = []
|
91 |
if 'file_names' not in st.session_state:
|
92 |
st.session_state.file_names = []
|
93 |
-
|
94 |
-
st.session_state.explanations_generated = False
|
95 |
if 'current_job_description' not in st.session_state:
|
96 |
st.session_state.current_job_description = ""
|
97 |
-
#
|
98 |
-
try:
|
99 |
-
if 'qwen3_tokenizer' not in st.session_state:
|
100 |
-
st.session_state.qwen3_tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-14B")
|
101 |
-
if 'qwen3_model' not in st.session_state:
|
102 |
-
st.session_state.qwen3_model = AutoModelForCausalLM.from_pretrained(
|
103 |
-
"Qwen/Qwen3-14B",
|
104 |
-
torch_dtype="auto",
|
105 |
-
device_map="auto"
|
106 |
-
)
|
107 |
-
except Exception as e:
|
108 |
-
st.warning(f"β οΈ Could not load Qwen3-14B: {str(e)}")
|
109 |
-
st.session_state.qwen3_tokenizer = None
|
110 |
-
st.session_state.qwen3_model = None
|
111 |
|
112 |
# Separate smaller model for intent analysis
|
113 |
try:
|
@@ -566,76 +548,7 @@ Reason: [Brief justification]"""
|
|
566 |
|
567 |
return explanation
|
568 |
|
569 |
-
def generate_llm_explanation(self, resume_text, job_description, score, skills, max_retries=3):
|
570 |
-
"""Generate detailed explanation using Qwen3-14B"""
|
571 |
-
if not st.session_state.qwen3_model:
|
572 |
-
return self.generate_simple_explanation(score, score, score, skills)
|
573 |
-
|
574 |
-
# Truncate texts to manage token limits
|
575 |
-
resume_snippet = resume_text[:2000] if len(resume_text) > 2000 else resume_text
|
576 |
-
job_snippet = job_description[:1000] if len(job_description) > 1000 else job_description
|
577 |
-
|
578 |
-
prompt = f"""You are an expert HR analyst. Analyze this individual candidate's resume against the job requirements and write EXACTLY 150 words explaining why this specific candidate is suitable for the position.
|
579 |
|
580 |
-
Structure your 150-word analysis as follows:
|
581 |
-
1. Experience alignment (40-50 words)
|
582 |
-
2. Key strengths and skills match (40-50 words)
|
583 |
-
3. Unique value proposition (40-50 words)
|
584 |
-
4. Overall recommendation (10-20 words)
|
585 |
-
|
586 |
-
Job Requirements:
|
587 |
-
{job_snippet}
|
588 |
-
|
589 |
-
Candidate's Resume:
|
590 |
-
{resume_snippet}
|
591 |
-
|
592 |
-
Identified Matching Skills: {', '.join(skills[:10])}
|
593 |
-
Compatibility Score: {score:.1%}
|
594 |
-
|
595 |
-
Write a professional, detailed 150-word analysis for THIS INDIVIDUAL CANDIDATE:"""
|
596 |
-
|
597 |
-
for attempt in range(max_retries):
|
598 |
-
try:
|
599 |
-
response = generate_qwen3_response(
|
600 |
-
prompt,
|
601 |
-
st.session_state.qwen3_tokenizer,
|
602 |
-
st.session_state.qwen3_model,
|
603 |
-
max_new_tokens=200
|
604 |
-
)
|
605 |
-
|
606 |
-
# Extract the response and ensure it's about 150 words
|
607 |
-
explanation = response.strip()
|
608 |
-
word_count = len(explanation.split())
|
609 |
-
|
610 |
-
# If response is close to 150 words (130-170), accept it
|
611 |
-
if 130 <= word_count <= 170:
|
612 |
-
return explanation
|
613 |
-
|
614 |
-
# If response is too short or too long, try again with adjusted prompt
|
615 |
-
if word_count < 130:
|
616 |
-
# Response too short, try again
|
617 |
-
continue
|
618 |
-
elif word_count > 170:
|
619 |
-
# Response too long, truncate to approximately 150 words
|
620 |
-
words = explanation.split()
|
621 |
-
truncated = ' '.join(words[:150])
|
622 |
-
# Add proper ending if truncated
|
623 |
-
if not truncated.endswith('.'):
|
624 |
-
truncated += '.'
|
625 |
-
return truncated
|
626 |
-
|
627 |
-
return explanation
|
628 |
-
|
629 |
-
except Exception as e:
|
630 |
-
if attempt < max_retries - 1:
|
631 |
-
time.sleep(2) # Wait before retry
|
632 |
-
continue
|
633 |
-
else:
|
634 |
-
# Fallback to simple explanation
|
635 |
-
return self.generate_simple_explanation(score, score, score, skills)
|
636 |
-
|
637 |
-
# If all retries failed, use simple explanation
|
638 |
-
return self.generate_simple_explanation(score, score, score, skills)
|
639 |
|
640 |
def create_download_link(df, filename="resume_screening_results.csv"):
|
641 |
"""Create download link for results"""
|
@@ -672,7 +585,6 @@ if st.session_state.resume_texts:
|
|
672 |
st.session_state.resume_texts = []
|
673 |
st.session_state.file_names = []
|
674 |
st.session_state.results = []
|
675 |
-
st.session_state.explanations_generated = False
|
676 |
st.session_state.current_job_description = ""
|
677 |
st.rerun()
|
678 |
|
@@ -835,120 +747,64 @@ elif input_method == "π Load from Hugging Face Dataset":
|
|
835 |
# Processing and Results
|
836 |
st.header("π Step 3: Analyze Resumes")
|
837 |
|
838 |
-
#
|
839 |
-
|
840 |
-
|
841 |
-
|
842 |
-
|
843 |
-
|
844 |
-
|
845 |
-
|
846 |
-
|
847 |
-
|
848 |
-
|
849 |
-
|
850 |
-
|
851 |
-
|
852 |
-
|
853 |
-
|
854 |
-
|
855 |
-
|
856 |
-
|
857 |
-
|
858 |
-
|
859 |
-
|
860 |
-
|
861 |
-
|
862 |
-
idx = result_data['index']
|
863 |
-
name = st.session_state.file_names[idx]
|
864 |
-
text = st.session_state.resume_texts[idx]
|
865 |
-
|
866 |
-
# Extract skills
|
867 |
-
skills = screener.extract_skills(text, job_description)
|
868 |
-
|
869 |
-
results.append({
|
870 |
-
'rank': rank,
|
871 |
-
'name': name,
|
872 |
-
'final_score': result_data['final_score'],
|
873 |
-
'cross_encoder_score': result_data['cross_encoder_score'],
|
874 |
-
'bm25_score': result_data['bm25_score'],
|
875 |
-
'intent_score': result_data['intent_score'],
|
876 |
-
'skills': skills,
|
877 |
-
'text': text,
|
878 |
-
'text_preview': text[:500] + "..." if len(text) > 500 else text,
|
879 |
-
'explanation': None # No detailed explanation yet
|
880 |
-
})
|
881 |
-
|
882 |
-
# Add simple explanations for now
|
883 |
-
for result in results:
|
884 |
-
result['explanation'] = screener.generate_simple_explanation(
|
885 |
-
result['final_score'],
|
886 |
-
result['cross_encoder_score'],
|
887 |
-
result['bm25_score'],
|
888 |
-
result['skills']
|
889 |
-
)
|
890 |
-
|
891 |
-
# Store in session state
|
892 |
-
st.session_state.results = results
|
893 |
-
st.session_state.explanations_generated = False
|
894 |
-
st.session_state.current_job_description = job_description
|
895 |
-
|
896 |
-
st.success(f"π Advanced pipeline complete! Found top {len(st.session_state.results)} candidates.")
|
897 |
-
|
898 |
-
except Exception as e:
|
899 |
-
st.error(f"β Error during analysis: {str(e)}")
|
900 |
-
|
901 |
-
# Second button: Generate AI explanations (slower, optional)
|
902 |
-
with col2:
|
903 |
-
# Show this button only if we have results and LLM is enabled
|
904 |
-
show_explanation_button = (
|
905 |
-
st.session_state.results and
|
906 |
-
use_llm_explanations and
|
907 |
-
st.session_state.qwen3_model and
|
908 |
-
not st.session_state.explanations_generated
|
909 |
-
)
|
910 |
-
|
911 |
-
if show_explanation_button:
|
912 |
-
if st.button("π€ Generate AI Explanations",
|
913 |
-
type="secondary",
|
914 |
-
help="Generate detailed 150-word explanations using Qwen3-14B (takes longer)"):
|
915 |
-
with st.spinner("π€ Generating detailed AI explanations..."):
|
916 |
-
try:
|
917 |
-
explanation_progress = st.progress(0)
|
918 |
-
explanation_text = st.empty()
|
919 |
-
|
920 |
-
for i, result in enumerate(st.session_state.results):
|
921 |
-
explanation_text.text(f"π€ Generating AI explanation for candidate {i+1}/{len(st.session_state.results)}...")
|
922 |
-
|
923 |
-
llm_explanation = screener.generate_llm_explanation(
|
924 |
-
result['text'],
|
925 |
-
st.session_state.current_job_description,
|
926 |
-
result['final_score'],
|
927 |
-
result['skills']
|
928 |
-
)
|
929 |
-
result['explanation'] = llm_explanation
|
930 |
-
|
931 |
-
explanation_progress.progress((i + 1) / len(st.session_state.results))
|
932 |
-
|
933 |
-
explanation_progress.empty()
|
934 |
-
explanation_text.empty()
|
935 |
-
|
936 |
-
# Mark explanations as generated
|
937 |
-
st.session_state.explanations_generated = True
|
938 |
|
939 |
-
|
|
|
940 |
|
941 |
-
|
942 |
-
|
943 |
-
|
944 |
-
|
945 |
-
|
946 |
-
|
947 |
-
|
948 |
-
|
949 |
-
|
950 |
-
|
951 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
952 |
|
953 |
# Display Results
|
954 |
if st.session_state.results:
|
@@ -1110,7 +966,6 @@ with col1:
|
|
1110 |
st.session_state.resume_texts = []
|
1111 |
st.session_state.file_names = []
|
1112 |
st.session_state.results = []
|
1113 |
-
st.session_state.explanations_generated = False
|
1114 |
st.session_state.current_job_description = ""
|
1115 |
st.success("β
Resumes cleared!")
|
1116 |
st.rerun()
|
@@ -1120,7 +975,6 @@ with col2:
|
|
1120 |
st.session_state.resume_texts = []
|
1121 |
st.session_state.file_names = []
|
1122 |
st.session_state.results = []
|
1123 |
-
st.session_state.explanations_generated = False
|
1124 |
st.session_state.current_job_description = ""
|
1125 |
|
1126 |
if torch.cuda.is_available():
|
@@ -1134,7 +988,7 @@ st.markdown("---")
|
|
1134 |
st.markdown(
|
1135 |
"""
|
1136 |
<div style='text-align: center; color: #666;'>
|
1137 |
-
π Powered by BAAI/bge-large-en-v1.5 & Qwen3-
|
1138 |
</div>
|
1139 |
""",
|
1140 |
unsafe_allow_html=True
|
|
|
59 |
|
60 |
# LLM Settings
|
61 |
st.subheader("LLM Settings")
|
62 |
+
st.info("π‘ Intent analysis using Qwen3-1.7B is always enabled")
|
|
|
|
|
|
|
63 |
|
64 |
st.markdown("---")
|
65 |
st.markdown("### π€ Advanced Pipeline")
|
|
|
71 |
st.markdown("### π Models Used")
|
72 |
st.markdown("- **Embedding**: BAAI/bge-large-en-v1.5")
|
73 |
st.markdown("- **Cross-Encoder**: ms-marco-MiniLM-L6-v2")
|
|
|
74 |
st.markdown("- **Intent Analysis**: Qwen/Qwen3-1.7B")
|
75 |
st.markdown("### π Scoring Formula")
|
76 |
st.markdown("**Final Score = Cross-Encoder (0-1) + BM25 (0.1-0.2) + Intent (0-0.3)**")
|
|
|
86 |
st.session_state.resume_texts = []
|
87 |
if 'file_names' not in st.session_state:
|
88 |
st.session_state.file_names = []
|
89 |
+
|
|
|
90 |
if 'current_job_description' not in st.session_state:
|
91 |
st.session_state.current_job_description = ""
|
92 |
+
# No need for Qwen3-14B model since we're not generating explanations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
|
94 |
# Separate smaller model for intent analysis
|
95 |
try:
|
|
|
548 |
|
549 |
return explanation
|
550 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
551 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
552 |
|
553 |
def create_download_link(df, filename="resume_screening_results.csv"):
|
554 |
"""Create download link for results"""
|
|
|
585 |
st.session_state.resume_texts = []
|
586 |
st.session_state.file_names = []
|
587 |
st.session_state.results = []
|
|
|
588 |
st.session_state.current_job_description = ""
|
589 |
st.rerun()
|
590 |
|
|
|
747 |
# Processing and Results
|
748 |
st.header("π Step 3: Analyze Resumes")
|
749 |
|
750 |
+
# Run Advanced Pipeline Analysis
|
751 |
+
if st.button("π Advanced Pipeline Analysis",
|
752 |
+
disabled=not (job_description and st.session_state.resume_texts),
|
753 |
+
type="primary",
|
754 |
+
help="Run the complete 5-stage advanced pipeline"):
|
755 |
+
if len(st.session_state.resume_texts) == 0:
|
756 |
+
st.error("β Please upload resumes first!")
|
757 |
+
elif not job_description.strip():
|
758 |
+
st.error("β Please enter a job description!")
|
759 |
+
else:
|
760 |
+
with st.spinner("π Running Advanced Pipeline Analysis..."):
|
761 |
+
try:
|
762 |
+
# Run the advanced pipeline
|
763 |
+
pipeline_results = screener.advanced_pipeline_ranking(
|
764 |
+
st.session_state.resume_texts, job_description, final_top_k=top_k
|
765 |
+
)
|
766 |
+
|
767 |
+
# Prepare results for display
|
768 |
+
results = []
|
769 |
+
|
770 |
+
for rank, result_data in enumerate(pipeline_results, 1):
|
771 |
+
idx = result_data['index']
|
772 |
+
name = st.session_state.file_names[idx]
|
773 |
+
text = st.session_state.resume_texts[idx]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
774 |
|
775 |
+
# Extract skills
|
776 |
+
skills = screener.extract_skills(text, job_description)
|
777 |
|
778 |
+
results.append({
|
779 |
+
'rank': rank,
|
780 |
+
'name': name,
|
781 |
+
'final_score': result_data['final_score'],
|
782 |
+
'cross_encoder_score': result_data['cross_encoder_score'],
|
783 |
+
'bm25_score': result_data['bm25_score'],
|
784 |
+
'intent_score': result_data['intent_score'],
|
785 |
+
'skills': skills,
|
786 |
+
'text': text,
|
787 |
+
'text_preview': text[:500] + "..." if len(text) > 500 else text,
|
788 |
+
'explanation': None # Will be filled with simple explanation
|
789 |
+
})
|
790 |
+
|
791 |
+
# Add simple explanations
|
792 |
+
for result in results:
|
793 |
+
result['explanation'] = screener.generate_simple_explanation(
|
794 |
+
result['final_score'],
|
795 |
+
result['cross_encoder_score'],
|
796 |
+
result['bm25_score'],
|
797 |
+
result['skills']
|
798 |
+
)
|
799 |
+
|
800 |
+
# Store in session state
|
801 |
+
st.session_state.results = results
|
802 |
+
st.session_state.current_job_description = job_description
|
803 |
+
|
804 |
+
st.success(f"π Advanced pipeline complete! Found top {len(st.session_state.results)} candidates.")
|
805 |
+
|
806 |
+
except Exception as e:
|
807 |
+
st.error(f"β Error during analysis: {str(e)}")
|
808 |
|
809 |
# Display Results
|
810 |
if st.session_state.results:
|
|
|
966 |
st.session_state.resume_texts = []
|
967 |
st.session_state.file_names = []
|
968 |
st.session_state.results = []
|
|
|
969 |
st.session_state.current_job_description = ""
|
970 |
st.success("β
Resumes cleared!")
|
971 |
st.rerun()
|
|
|
975 |
st.session_state.resume_texts = []
|
976 |
st.session_state.file_names = []
|
977 |
st.session_state.results = []
|
|
|
978 |
st.session_state.current_job_description = ""
|
979 |
|
980 |
if torch.cuda.is_available():
|
|
|
988 |
st.markdown(
|
989 |
"""
|
990 |
<div style='text-align: center; color: #666;'>
|
991 |
+
π Powered by BAAI/bge-large-en-v1.5 & Qwen3-1.7B | Built with Streamlit
|
992 |
</div>
|
993 |
""",
|
994 |
unsafe_allow_html=True
|