root commited on
Commit
19f7d68
Β·
1 Parent(s): da61f37
Files changed (1) hide show
  1. app.py +60 -206
app.py CHANGED
@@ -59,10 +59,7 @@ with st.sidebar:
59
 
60
  # LLM Settings
61
  st.subheader("LLM Settings")
62
- use_llm_explanations = st.checkbox("Generate AI Explanations", value=True)
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
- if 'explanations_generated' not in st.session_state:
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
- # Load Qwen models with error handling
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
- # First button: Find top K candidates (fast ranking)
839
- col1, col2 = st.columns([1, 1])
840
-
841
- with col1:
842
- if st.button("πŸš€ Advanced Pipeline Analysis",
843
- disabled=not (job_description and st.session_state.resume_texts),
844
- type="primary",
845
- help="Run the complete 5-stage advanced pipeline"):
846
- if len(st.session_state.resume_texts) == 0:
847
- st.error("❌ Please upload resumes first!")
848
- elif not job_description.strip():
849
- st.error("❌ Please enter a job description!")
850
- else:
851
- with st.spinner("πŸš€ Running Advanced Pipeline Analysis..."):
852
- try:
853
- # Run the advanced pipeline
854
- pipeline_results = screener.advanced_pipeline_ranking(
855
- st.session_state.resume_texts, job_description, final_top_k=top_k
856
- )
857
-
858
- # Prepare results for display
859
- results = []
860
-
861
- for rank, result_data in enumerate(pipeline_results, 1):
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
- st.success(f"πŸ€– AI explanations generated for all {len(st.session_state.results)} candidates!")
 
940
 
941
- except Exception as e:
942
- st.error(f"❌ Error generating explanations: {str(e)}")
943
-
944
- elif st.session_state.results and st.session_state.explanations_generated:
945
- st.info("βœ… AI explanations already generated!")
946
-
947
- elif st.session_state.results and not use_llm_explanations:
948
- st.info("πŸ’‘ Enable 'Generate AI Explanations' in sidebar to use this feature")
949
-
950
- elif st.session_state.results and not st.session_state.qwen3_model:
951
- st.warning("⚠️ LLM model not available. Check your Hugging Face token.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-14B | Built with Streamlit
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