Spaces:
Sleeping
Sleeping
Update src/app.py
Browse files- src/app.py +209 -327
src/app.py
CHANGED
|
@@ -60,27 +60,20 @@ from reportlab.lib.styles import getSampleStyleSheet
|
|
| 60 |
from reportlab.lib.units import inch
|
| 61 |
|
| 62 |
# Claude Chatbot Class
|
| 63 |
-
import time
|
| 64 |
-
|
| 65 |
class ClaudeChatbot:
|
| 66 |
def __init__(self):
|
| 67 |
self.api_key = os.getenv('OPENROUTER_API_KEY')
|
| 68 |
self.base_url = "https://openrouter.ai/api/v1/chat/completions"
|
| 69 |
|
| 70 |
-
#
|
| 71 |
-
|
| 72 |
-
self.model = "meta-llama/llama-3.2-3b-instruct:free" # Free alternative
|
| 73 |
-
# Other free options:
|
| 74 |
-
# "microsoft/phi-3-mini-128k-instruct:free"
|
| 75 |
-
# "huggingface/zephyr-7b-beta:free"
|
| 76 |
-
# "mistralai/mistral-7b-instruct:free"
|
| 77 |
|
| 78 |
if not self.api_key:
|
| 79 |
st.error("β OPENROUTER_API_KEY not found in environment variables!")
|
| 80 |
st.info("Please set your OpenRouter API key in the environment variables.")
|
| 81 |
|
| 82 |
def generate_response(self, prompt, context="", max_tokens=1500):
|
| 83 |
-
"""Generate response using free models via OpenRouter"""
|
| 84 |
if not self.api_key:
|
| 85 |
return "Error: API key not configured"
|
| 86 |
|
|
@@ -126,11 +119,7 @@ class ClaudeChatbot:
|
|
| 126 |
try:
|
| 127 |
response = requests.post(self.base_url, headers=headers, json=data, timeout=30)
|
| 128 |
|
| 129 |
-
|
| 130 |
-
st.warning("Rate limit hit. Waiting 60 seconds...")
|
| 131 |
-
time.sleep(60)
|
| 132 |
-
response = requests.post(self.base_url, headers=headers, json=data, timeout=30)
|
| 133 |
-
|
| 134 |
if response.status_code == 402:
|
| 135 |
return "Error: This model requires payment. Please switch to a free model or add credits to your OpenRouter account."
|
| 136 |
|
|
@@ -146,7 +135,7 @@ class ClaudeChatbot:
|
|
| 146 |
if e.response.status_code == 402:
|
| 147 |
return "Error: Payment required. This model is not free. Please use a free model or add credits."
|
| 148 |
elif e.response.status_code == 429:
|
| 149 |
-
return "Error:
|
| 150 |
else:
|
| 151 |
return f"HTTP Error: {e.response.status_code} - {str(e)}"
|
| 152 |
except requests.exceptions.RequestException as e:
|
|
@@ -283,6 +272,18 @@ class ResumeAnalyzer:
|
|
| 283 |
"business process", "gap analysis", "user stories", "workflow", "project management"],
|
| 284 |
"Full Stack Developer": ["html", "css", "javascript", "react", "angular", "vue", "node.js", "express",
|
| 285 |
"mongodb", "postgresql", "rest api", "graphql", "version control", "responsive design"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
}
|
| 287 |
|
| 288 |
# Common skills database
|
|
@@ -630,19 +631,6 @@ def main():
|
|
| 630 |
# Initialize analyzer
|
| 631 |
try:
|
| 632 |
analyzer = ResumeAnalyzer()
|
| 633 |
-
# Add ML/AI Engineer to job keywords
|
| 634 |
-
analyzer.job_keywords["Machine Learning Engineer"] = [
|
| 635 |
-
"python", "tensorflow", "pytorch", "scikit-learn", "pandas", "numpy", "machine learning",
|
| 636 |
-
"deep learning", "neural networks", "computer vision", "nlp", "data science", "algorithms",
|
| 637 |
-
"statistics", "linear algebra", "calculus", "regression", "classification", "clustering",
|
| 638 |
-
"feature engineering", "model deployment", "mlops", "docker", "kubernetes", "aws", "gcp"
|
| 639 |
-
]
|
| 640 |
-
analyzer.job_keywords["AI Engineer"] = [
|
| 641 |
-
"artificial intelligence", "machine learning", "deep learning", "neural networks", "python",
|
| 642 |
-
"tensorflow", "pytorch", "computer vision", "nlp", "natural language processing", "opencv",
|
| 643 |
-
"transformers", "bert", "gpt", "reinforcement learning", "generative ai", "llm", "chatbot",
|
| 644 |
-
"model optimization", "ai ethics", "edge ai", "quantization", "onnx", "tensorrt"
|
| 645 |
-
]
|
| 646 |
except Exception as e:
|
| 647 |
st.error(f"Error initializing analyzer: {str(e)}")
|
| 648 |
return
|
|
@@ -652,15 +640,15 @@ def main():
|
|
| 652 |
job_roles = list(analyzer.job_keywords.keys())
|
| 653 |
selected_role = st.sidebar.selectbox("Select Target Job Role:", job_roles)
|
| 654 |
|
| 655 |
-
# Initialize session state for chat
|
| 656 |
if "chat_history" not in st.session_state:
|
| 657 |
st.session_state.chat_history = []
|
| 658 |
if "resume_context" not in st.session_state:
|
| 659 |
st.session_state.resume_context = ""
|
| 660 |
if "show_chat" not in st.session_state:
|
| 661 |
st.session_state.show_chat = False
|
| 662 |
-
if "
|
| 663 |
-
st.session_state.
|
| 664 |
|
| 665 |
# File upload section
|
| 666 |
st.header("π Upload Your Resume")
|
|
@@ -723,29 +711,20 @@ def main():
|
|
| 723 |
with col2:
|
| 724 |
# Generate persona summary
|
| 725 |
persona_summary = analyzer.generate_persona_summary(text, sections)
|
| 726 |
-
st.subheader("
|
| 727 |
-
st.
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
st.pyplot(fig)
|
| 741 |
-
except Exception as e:
|
| 742 |
-
st.warning("Could not generate word cloud. Showing top words instead.")
|
| 743 |
-
word_freq = Counter(preprocessed_tokens)
|
| 744 |
-
top_words = word_freq.most_common(20)
|
| 745 |
-
|
| 746 |
-
words_df = pd.DataFrame(top_words, columns=['Word', 'Frequency'])
|
| 747 |
-
fig = px.bar(words_df, x='Word', y='Frequency', title='Top 20 Words')
|
| 748 |
-
st.plotly_chart(fig)
|
| 749 |
|
| 750 |
with tab2:
|
| 751 |
st.header("Skills Analysis")
|
|
@@ -753,100 +732,70 @@ def main():
|
|
| 753 |
col1, col2 = st.columns(2)
|
| 754 |
|
| 755 |
with col1:
|
| 756 |
-
st.subheader("
|
| 757 |
if tech_skills:
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
st.write(skills_text)
|
| 761 |
-
|
| 762 |
-
if len(tech_skills) > 5:
|
| 763 |
-
skills_df = pd.DataFrame({
|
| 764 |
-
'Skill': tech_skills[:10],
|
| 765 |
-
'Count': [1] * len(tech_skills[:10])
|
| 766 |
-
})
|
| 767 |
-
fig = px.pie(skills_df, values='Count', names='Skill',
|
| 768 |
-
title='Technical Skills Distribution')
|
| 769 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 770 |
-
else:
|
| 771 |
-
skills_df = pd.DataFrame({
|
| 772 |
-
'Skill': tech_skills,
|
| 773 |
-
'Count': [1] * len(tech_skills)
|
| 774 |
-
})
|
| 775 |
-
fig = px.bar(skills_df, x='Skill', y='Count',
|
| 776 |
-
title='Technical Skills Found')
|
| 777 |
-
fig.update_xaxis(tickangle=45)
|
| 778 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 779 |
else:
|
| 780 |
-
st.
|
| 781 |
-
st.info("π‘ Consider adding technical skills relevant to your field")
|
| 782 |
|
| 783 |
with col2:
|
| 784 |
-
st.subheader("
|
| 785 |
if soft_skills:
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
st.write(skills_text)
|
| 789 |
-
|
| 790 |
-
if len(soft_skills) > 3:
|
| 791 |
-
soft_df = pd.DataFrame({
|
| 792 |
-
'Skill': soft_skills[:8],
|
| 793 |
-
'Count': [1] * len(soft_skills[:8])
|
| 794 |
-
})
|
| 795 |
-
fig = px.bar(soft_df, x='Skill', y='Count',
|
| 796 |
-
title='Soft Skills Found',
|
| 797 |
-
color='Skill')
|
| 798 |
-
fig.update_xaxis(tickangle=45)
|
| 799 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 800 |
else:
|
| 801 |
-
st.
|
| 802 |
-
st.info("π‘ Consider highlighting soft skills like leadership, communication, teamwork")
|
| 803 |
|
| 804 |
# Role-specific keyword analysis
|
| 805 |
-
st.subheader(f"
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
st.
|
| 810 |
-
st.info(f"Match Percentage: {match_percentage:.1f}%")
|
| 811 |
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 819 |
|
| 820 |
with tab3:
|
| 821 |
st.header("Section Breakdown")
|
| 822 |
|
| 823 |
for section_name, section_content in sections.items():
|
| 824 |
if section_content:
|
| 825 |
-
with st.expander(f"
|
| 826 |
-
st.
|
| 827 |
-
f"{section_name.title()} Content",
|
| 828 |
-
section_content,
|
| 829 |
-
height=200,
|
| 830 |
-
key=f"section_{section_name}"
|
| 831 |
-
)
|
| 832 |
-
|
| 833 |
-
# Section-specific analysis
|
| 834 |
-
word_count = len(section_content.split())
|
| 835 |
-
st.metric(f"{section_name.title()} Word Count", word_count)
|
| 836 |
-
|
| 837 |
-
if section_name == "experience":
|
| 838 |
-
# Analyze experience section
|
| 839 |
-
years_mentioned = len(re.findall(r'\b(19|20)\d{2}\b', section_content))
|
| 840 |
-
companies_mentioned = len(re.findall(r'\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b', section_content))
|
| 841 |
-
st.metric("Years/Dates Mentioned", years_mentioned)
|
| 842 |
-
st.metric("Potential Companies", companies_mentioned)
|
| 843 |
-
|
| 844 |
-
elif section_name == "education":
|
| 845 |
-
# Analyze education section
|
| 846 |
-
degrees = re.findall(r'\b(bachelor|master|phd|degree|diploma|certificate)\b', section_content.lower())
|
| 847 |
-
st.metric("Degrees/Certificates Found", len(degrees))
|
| 848 |
else:
|
| 849 |
st.warning(f"β οΈ {section_name.title()} section not found or empty")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 850 |
|
| 851 |
with tab4:
|
| 852 |
st.header("ATS Analysis")
|
|
@@ -854,106 +803,60 @@ def main():
|
|
| 854 |
col1, col2 = st.columns(2)
|
| 855 |
|
| 856 |
with col1:
|
| 857 |
-
st.
|
| 858 |
-
st.metric("Overall ATS Score", f"{ats_score}/100")
|
| 859 |
|
| 860 |
-
# ATS
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
gauge = {
|
| 868 |
-
'axis': {'range': [None, 100]},
|
| 869 |
-
'bar': {'color': "darkblue"},
|
| 870 |
-
'steps': [
|
| 871 |
-
{'range': [0, 50], 'color': "lightgray"},
|
| 872 |
-
{'range': [50, 70], 'color': "yellow"},
|
| 873 |
-
{'range': [70, 100], 'color': "green"}
|
| 874 |
-
],
|
| 875 |
-
'threshold': {
|
| 876 |
-
'line': {'color': "red", 'width': 4},
|
| 877 |
-
'thickness': 0.75,
|
| 878 |
-
'value': 90
|
| 879 |
-
}
|
| 880 |
-
}
|
| 881 |
-
))
|
| 882 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 883 |
|
| 884 |
with col2:
|
| 885 |
-
|
| 886 |
-
st.
|
| 887 |
-
|
| 888 |
-
# Combined score
|
| 889 |
-
combined_score = (ats_score + match_percentage) / 2
|
| 890 |
-
st.metric("Combined Score", f"{combined_score:.1f}/100")
|
| 891 |
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
st.success("π Excellent! Your resume is well-optimized")
|
| 895 |
-
elif combined_score >= 60:
|
| 896 |
-
st.warning("π Good, but room for improvement")
|
| 897 |
else:
|
| 898 |
-
st.
|
|
|
|
|
|
|
|
|
|
| 899 |
|
| 900 |
-
#
|
| 901 |
-
st.subheader("
|
| 902 |
-
|
| 903 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 904 |
|
| 905 |
-
|
| 906 |
-
st.
|
| 907 |
-
for i, issue in enumerate(grammar_issues[:10]): # Show first 10 issues
|
| 908 |
-
if hasattr(issue, 'message'):
|
| 909 |
-
st.write(f"β’ {issue.message}")
|
| 910 |
-
else:
|
| 911 |
-
st.write(f"β’ {str(issue)}")
|
| 912 |
-
|
| 913 |
-
if len(grammar_issues) > 10:
|
| 914 |
-
st.info(f"... and {len(grammar_issues) - 10} more issues")
|
| 915 |
-
else:
|
| 916 |
-
st.success("β
No major grammar issues detected!")
|
| 917 |
|
| 918 |
with tab5:
|
| 919 |
-
st.header("
|
| 920 |
|
| 921 |
-
# Get
|
| 922 |
if os.getenv('OPENROUTER_API_KEY'):
|
| 923 |
-
st.
|
| 924 |
-
with st.spinner("Getting AI analysis from Claude..."):
|
| 925 |
claude_analysis = analyzer.get_claude_analysis(
|
| 926 |
text, sections, selected_role, ats_score, match_percentage
|
| 927 |
)
|
| 928 |
-
|
|
|
|
|
|
|
| 929 |
else:
|
| 930 |
-
st.
|
| 931 |
-
|
| 932 |
-
# Basic recommendations
|
| 933 |
-
st.subheader("π Quick Recommendations")
|
| 934 |
-
recommendations = []
|
| 935 |
|
| 936 |
-
|
| 937 |
-
recommendations.append("πΉ Improve ATS compatibility by adding more bullet points and clear section headers")
|
| 938 |
-
|
| 939 |
-
if match_percentage < 60:
|
| 940 |
-
recommendations.append(f"πΉ Add more {selected_role}-specific keywords to improve role match")
|
| 941 |
-
|
| 942 |
-
if len(tech_skills) < 5:
|
| 943 |
-
recommendations.append("πΉ Include more technical skills relevant to your field")
|
| 944 |
-
|
| 945 |
-
if not sections.get('projects'):
|
| 946 |
-
recommendations.append("πΉ Consider adding a projects section to showcase your work")
|
| 947 |
-
|
| 948 |
-
if len(text.split()) < 300:
|
| 949 |
-
recommendations.append("πΉ Expand your resume content - it seems too brief")
|
| 950 |
-
elif len(text.split()) > 800:
|
| 951 |
-
recommendations.append("πΉ Consider condensing your resume - it might be too lengthy")
|
| 952 |
-
|
| 953 |
-
for rec in recommendations:
|
| 954 |
-
st.markdown(rec)
|
| 955 |
-
|
| 956 |
-
# PDF Report Generation
|
| 957 |
st.subheader("π Download Report")
|
| 958 |
if st.button("Generate PDF Report"):
|
| 959 |
try:
|
|
@@ -964,135 +867,114 @@ def main():
|
|
| 964 |
|
| 965 |
st.download_button(
|
| 966 |
label="π₯ Download PDF Report",
|
| 967 |
-
data=pdf_buffer,
|
| 968 |
file_name=f"resume_analysis_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf",
|
| 969 |
mime="application/pdf"
|
| 970 |
)
|
| 971 |
except Exception as e:
|
| 972 |
st.error(f"Error generating PDF: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 973 |
|
| 974 |
-
#
|
| 975 |
-
st.
|
| 976 |
-
st.header("π€ AI Assistant Chat")
|
| 977 |
|
| 978 |
# Toggle chat visibility
|
| 979 |
-
|
| 980 |
-
if chat_toggle:
|
| 981 |
st.session_state.show_chat = not st.session_state.show_chat
|
| 982 |
|
| 983 |
if st.session_state.show_chat:
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
with st.chat_message(chat["role"]):
|
| 1003 |
-
st.markdown(chat["content"])
|
| 1004 |
-
|
| 1005 |
-
# Suggested questions
|
| 1006 |
-
st.subheader("π‘ Suggested Questions")
|
| 1007 |
-
col1, col2, col3, col4 = st.columns(4)
|
| 1008 |
-
|
| 1009 |
-
with col1:
|
| 1010 |
-
if st.button("How can I improve my resume?", key="improve_btn"):
|
| 1011 |
-
prompt = "Based on my resume analysis, how can I improve my resume to get better ATS scores and job match rates?"
|
| 1012 |
-
with st.chat_message("user"):
|
| 1013 |
-
st.markdown(prompt)
|
| 1014 |
-
st.session_state.chat_history.append({"role": "user", "content": prompt})
|
| 1015 |
-
|
| 1016 |
-
with st.chat_message("assistant"):
|
| 1017 |
-
with st.spinner("Analyzing your resume..."):
|
| 1018 |
-
response = analyzer.chatbot.generate_response(prompt, analysis_context + st.session_state.resume_context)
|
| 1019 |
-
st.markdown(response)
|
| 1020 |
-
st.session_state.chat_history.append({"role": "assistant", "content": response})
|
| 1021 |
-
|
| 1022 |
-
with col2:
|
| 1023 |
-
if st.button("What skills should I add?", key="skills_btn"):
|
| 1024 |
-
prompt = f"What specific technical and soft skills should I add to my resume for a {selected_role} position based on my current analysis?"
|
| 1025 |
-
with st.chat_message("user"):
|
| 1026 |
-
st.markdown(prompt)
|
| 1027 |
-
st.session_state.chat_history.append({"role": "user", "content": prompt})
|
| 1028 |
-
|
| 1029 |
-
with st.chat_message("assistant"):
|
| 1030 |
-
with st.spinner("Analyzing skill gaps..."):
|
| 1031 |
-
response = analyzer.chatbot.generate_response(prompt, analysis_context + st.session_state.resume_context)
|
| 1032 |
-
st.markdown(response)
|
| 1033 |
-
st.session_state.chat_history.append({"role": "assistant", "content": response})
|
| 1034 |
-
|
| 1035 |
-
with col3:
|
| 1036 |
-
if st.button("Format suggestions?", key="format_btn"):
|
| 1037 |
-
prompt = "What formatting improvements can I make to my resume to make it more ATS-friendly and visually appealing?"
|
| 1038 |
-
with st.chat_message("user"):
|
| 1039 |
-
st.markdown(prompt)
|
| 1040 |
-
st.session_state.chat_history.append({"role": "user", "content": prompt})
|
| 1041 |
-
|
| 1042 |
-
with st.chat_message("assistant"):
|
| 1043 |
-
with st.spinner("Analyzing format..."):
|
| 1044 |
-
response = analyzer.chatbot.generate_response(prompt, analysis_context + st.session_state.resume_context)
|
| 1045 |
-
st.markdown(response)
|
| 1046 |
-
st.session_state.chat_history.append({"role": "assistant", "content": response})
|
| 1047 |
-
|
| 1048 |
-
with col4:
|
| 1049 |
-
if st.button("ποΈ Clear Chat", key="clear_btn"):
|
| 1050 |
-
st.session_state.chat_history = []
|
| 1051 |
-
st.rerun()
|
| 1052 |
-
|
| 1053 |
-
# Chat input - MAIN FIX: Handle chat without multiple responses
|
| 1054 |
-
user_prompt = st.chat_input("Ask me anything about your resume...")
|
| 1055 |
|
| 1056 |
-
if
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
st.markdown(user_prompt)
|
| 1060 |
-
st.session_state.chat_history.append({"role": "user", "content": user_prompt})
|
| 1061 |
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
st.
|
| 1071 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1072 |
except Exception as e:
|
| 1073 |
st.error(f"Error during analysis: {str(e)}")
|
|
|
|
|
|
|
| 1074 |
else:
|
| 1075 |
-
st.error("Could not extract text from the uploaded file. Please
|
| 1076 |
-
|
| 1077 |
-
# Footer
|
| 1078 |
-
st.markdown("---")
|
| 1079 |
-
st.markdown("### π Tips for Better Resume Analysis")
|
| 1080 |
-
st.markdown("""
|
| 1081 |
-
- **Upload clear, well-formatted documents** for better text extraction
|
| 1082 |
-
- **Select the appropriate job role** to get relevant keyword matching
|
| 1083 |
-
- **Use the AI Assistant** to get personalized advice
|
| 1084 |
-
- **Download the PDF report** for offline reference
|
| 1085 |
-
- **Check multiple job roles** to see how your resume performs across different positions
|
| 1086 |
-
""")
|
| 1087 |
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
|
|
|
|
|
|
|
|
|
| 1091 |
st.markdown("""
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1096 |
""")
|
| 1097 |
|
| 1098 |
if __name__ == "__main__":
|
|
|
|
| 60 |
from reportlab.lib.units import inch
|
| 61 |
|
| 62 |
# Claude Chatbot Class
|
|
|
|
|
|
|
| 63 |
class ClaudeChatbot:
|
| 64 |
def __init__(self):
|
| 65 |
self.api_key = os.getenv('OPENROUTER_API_KEY')
|
| 66 |
self.base_url = "https://openrouter.ai/api/v1/chat/completions"
|
| 67 |
|
| 68 |
+
# Using free models on OpenRouter
|
| 69 |
+
self.model = "meta-llama/llama-3.2-3b-instruct:free"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
if not self.api_key:
|
| 72 |
st.error("β OPENROUTER_API_KEY not found in environment variables!")
|
| 73 |
st.info("Please set your OpenRouter API key in the environment variables.")
|
| 74 |
|
| 75 |
def generate_response(self, prompt, context="", max_tokens=1500):
|
| 76 |
+
"""Generate response using free models via OpenRouter - No rate limiting"""
|
| 77 |
if not self.api_key:
|
| 78 |
return "Error: API key not configured"
|
| 79 |
|
|
|
|
| 119 |
try:
|
| 120 |
response = requests.post(self.base_url, headers=headers, json=data, timeout=30)
|
| 121 |
|
| 122 |
+
# Handle payment required error
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
if response.status_code == 402:
|
| 124 |
return "Error: This model requires payment. Please switch to a free model or add credits to your OpenRouter account."
|
| 125 |
|
|
|
|
| 135 |
if e.response.status_code == 402:
|
| 136 |
return "Error: Payment required. This model is not free. Please use a free model or add credits."
|
| 137 |
elif e.response.status_code == 429:
|
| 138 |
+
return "Error: Too many requests. Please try again in a moment."
|
| 139 |
else:
|
| 140 |
return f"HTTP Error: {e.response.status_code} - {str(e)}"
|
| 141 |
except requests.exceptions.RequestException as e:
|
|
|
|
| 272 |
"business process", "gap analysis", "user stories", "workflow", "project management"],
|
| 273 |
"Full Stack Developer": ["html", "css", "javascript", "react", "angular", "vue", "node.js", "express",
|
| 274 |
"mongodb", "postgresql", "rest api", "graphql", "version control", "responsive design"],
|
| 275 |
+
"Machine Learning Engineer": [
|
| 276 |
+
"python", "tensorflow", "pytorch", "scikit-learn", "pandas", "numpy", "machine learning",
|
| 277 |
+
"deep learning", "neural networks", "computer vision", "nlp", "data science", "algorithms",
|
| 278 |
+
"statistics", "linear algebra", "calculus", "regression", "classification", "clustering",
|
| 279 |
+
"feature engineering", "model deployment", "mlops", "docker", "kubernetes", "aws", "gcp"
|
| 280 |
+
],
|
| 281 |
+
"AI Engineer": [
|
| 282 |
+
"artificial intelligence", "machine learning", "deep learning", "neural networks", "python",
|
| 283 |
+
"tensorflow", "pytorch", "computer vision", "nlp", "natural language processing", "opencv",
|
| 284 |
+
"transformers", "bert", "gpt", "reinforcement learning", "generative ai", "llm", "chatbot",
|
| 285 |
+
"model optimization", "ai ethics", "edge ai", "quantization", "onnx", "tensorrt"
|
| 286 |
+
]
|
| 287 |
}
|
| 288 |
|
| 289 |
# Common skills database
|
|
|
|
| 631 |
# Initialize analyzer
|
| 632 |
try:
|
| 633 |
analyzer = ResumeAnalyzer()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
except Exception as e:
|
| 635 |
st.error(f"Error initializing analyzer: {str(e)}")
|
| 636 |
return
|
|
|
|
| 640 |
job_roles = list(analyzer.job_keywords.keys())
|
| 641 |
selected_role = st.sidebar.selectbox("Select Target Job Role:", job_roles)
|
| 642 |
|
| 643 |
+
# Initialize session state for chat
|
| 644 |
if "chat_history" not in st.session_state:
|
| 645 |
st.session_state.chat_history = []
|
| 646 |
if "resume_context" not in st.session_state:
|
| 647 |
st.session_state.resume_context = ""
|
| 648 |
if "show_chat" not in st.session_state:
|
| 649 |
st.session_state.show_chat = False
|
| 650 |
+
if "waiting_for_response" not in st.session_state:
|
| 651 |
+
st.session_state.waiting_for_response = False
|
| 652 |
|
| 653 |
# File upload section
|
| 654 |
st.header("π Upload Your Resume")
|
|
|
|
| 711 |
with col2:
|
| 712 |
# Generate persona summary
|
| 713 |
persona_summary = analyzer.generate_persona_summary(text, sections)
|
| 714 |
+
st.subheader("AI-Generated Persona")
|
| 715 |
+
st.write(persona_summary)
|
| 716 |
+
|
| 717 |
+
# Overall scores
|
| 718 |
+
overall_score = (ats_score + match_percentage) / 2
|
| 719 |
+
st.metric("Overall Score", f"{overall_score:.1f}/100")
|
| 720 |
+
|
| 721 |
+
# Score interpretation
|
| 722 |
+
if overall_score >= 80:
|
| 723 |
+
st.success("π Excellent! Your resume is well-optimized.")
|
| 724 |
+
elif overall_score >= 60:
|
| 725 |
+
st.warning("β οΈ Good, but there's room for improvement.")
|
| 726 |
+
else:
|
| 727 |
+
st.error("β Needs significant improvement.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 728 |
|
| 729 |
with tab2:
|
| 730 |
st.header("Skills Analysis")
|
|
|
|
| 732 |
col1, col2 = st.columns(2)
|
| 733 |
|
| 734 |
with col1:
|
| 735 |
+
st.subheader("Technical Skills Found")
|
| 736 |
if tech_skills:
|
| 737 |
+
for skill in tech_skills:
|
| 738 |
+
st.write(f"β
{skill}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 739 |
else:
|
| 740 |
+
st.write("No technical skills detected. Consider adding relevant technical skills.")
|
|
|
|
| 741 |
|
| 742 |
with col2:
|
| 743 |
+
st.subheader("Soft Skills Found")
|
| 744 |
if soft_skills:
|
| 745 |
+
for skill in soft_skills:
|
| 746 |
+
st.write(f"β
{skill}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 747 |
else:
|
| 748 |
+
st.write("No soft skills detected. Consider highlighting your interpersonal abilities.")
|
|
|
|
| 749 |
|
| 750 |
# Role-specific keyword analysis
|
| 751 |
+
st.subheader(f"Keywords for {selected_role}")
|
| 752 |
+
col1, col2 = st.columns(2)
|
| 753 |
+
|
| 754 |
+
with col1:
|
| 755 |
+
st.metric("Match Percentage", f"{match_percentage:.1f}%")
|
|
|
|
| 756 |
|
| 757 |
+
if match_percentage >= 70:
|
| 758 |
+
st.success("π― Great keyword match!")
|
| 759 |
+
elif match_percentage >= 50:
|
| 760 |
+
st.warning("β οΈ Moderate keyword match")
|
| 761 |
+
else:
|
| 762 |
+
st.error("β Low keyword match")
|
| 763 |
+
|
| 764 |
+
with col2:
|
| 765 |
+
st.write("**Found Keywords:**")
|
| 766 |
+
if found_keywords:
|
| 767 |
+
for keyword in found_keywords:
|
| 768 |
+
st.write(f"β
{keyword}")
|
| 769 |
+
else:
|
| 770 |
+
st.write("No role-specific keywords found.")
|
| 771 |
+
|
| 772 |
+
# Missing keywords
|
| 773 |
+
missing_keywords = [kw for kw in analyzer.job_keywords[selected_role]
|
| 774 |
+
if kw not in found_keywords]
|
| 775 |
+
if missing_keywords:
|
| 776 |
+
st.subheader("Suggested Keywords to Add")
|
| 777 |
+
for keyword in missing_keywords[:10]: # Show top 10
|
| 778 |
+
st.write(f"β {keyword}")
|
| 779 |
|
| 780 |
with tab3:
|
| 781 |
st.header("Section Breakdown")
|
| 782 |
|
| 783 |
for section_name, section_content in sections.items():
|
| 784 |
if section_content:
|
| 785 |
+
with st.expander(f"π {section_name.title()} Section"):
|
| 786 |
+
st.write(section_content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
else:
|
| 788 |
st.warning(f"β οΈ {section_name.title()} section not found or empty")
|
| 789 |
+
|
| 790 |
+
# Section recommendations
|
| 791 |
+
st.subheader("Section Recommendations")
|
| 792 |
+
missing_sections = [name for name, content in sections.items() if not content]
|
| 793 |
+
if missing_sections:
|
| 794 |
+
st.write("**Missing or Empty Sections:**")
|
| 795 |
+
for section in missing_sections:
|
| 796 |
+
st.write(f"β {section.title()}")
|
| 797 |
+
else:
|
| 798 |
+
st.success("β
All major sections are present!")
|
| 799 |
|
| 800 |
with tab4:
|
| 801 |
st.header("ATS Analysis")
|
|
|
|
| 803 |
col1, col2 = st.columns(2)
|
| 804 |
|
| 805 |
with col1:
|
| 806 |
+
st.metric("ATS Score", f"{ats_score}/100")
|
|
|
|
| 807 |
|
| 808 |
+
# ATS Score interpretation
|
| 809 |
+
if ats_score >= 80:
|
| 810 |
+
st.success("π€ Excellent ATS compatibility!")
|
| 811 |
+
elif ats_score >= 60:
|
| 812 |
+
st.warning("β οΈ Good ATS compatibility")
|
| 813 |
+
else:
|
| 814 |
+
st.error("β Poor ATS compatibility")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 815 |
|
| 816 |
with col2:
|
| 817 |
+
# Grammar check
|
| 818 |
+
st.subheader("Grammar Check")
|
| 819 |
+
grammar_issues = analyzer.grammar_check(text)
|
|
|
|
|
|
|
|
|
|
| 820 |
|
| 821 |
+
if len(grammar_issues) == 0:
|
| 822 |
+
st.success("β
No grammar issues detected!")
|
|
|
|
|
|
|
|
|
|
| 823 |
else:
|
| 824 |
+
st.warning(f"β οΈ {len(grammar_issues)} potential issues found")
|
| 825 |
+
with st.expander("View Grammar Issues"):
|
| 826 |
+
for issue in grammar_issues[:10]: # Show first 10
|
| 827 |
+
st.write(f"β’ {issue.message}")
|
| 828 |
|
| 829 |
+
# ATS Tips
|
| 830 |
+
st.subheader("ATS Optimization Tips")
|
| 831 |
+
ats_tips = [
|
| 832 |
+
"Use standard section headings (Experience, Education, Skills)",
|
| 833 |
+
"Include relevant keywords naturally throughout your resume",
|
| 834 |
+
"Use bullet points for easy scanning",
|
| 835 |
+
"Avoid images, tables, and complex formatting",
|
| 836 |
+
"Use standard fonts (Arial, Calibri, Times New Roman)",
|
| 837 |
+
"Save as PDF to preserve formatting",
|
| 838 |
+
"Include contact information at the top"
|
| 839 |
+
]
|
| 840 |
|
| 841 |
+
for tip in ats_tips:
|
| 842 |
+
st.write(f"π‘ {tip}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 843 |
|
| 844 |
with tab5:
|
| 845 |
+
st.header("Comprehensive Analysis & Suggestions")
|
| 846 |
|
| 847 |
+
# Get Claude analysis if API is available
|
| 848 |
if os.getenv('OPENROUTER_API_KEY'):
|
| 849 |
+
with st.spinner("Getting AI-powered analysis..."):
|
|
|
|
| 850 |
claude_analysis = analyzer.get_claude_analysis(
|
| 851 |
text, sections, selected_role, ats_score, match_percentage
|
| 852 |
)
|
| 853 |
+
|
| 854 |
+
st.subheader("π€ AI-Powered Analysis")
|
| 855 |
+
st.write(claude_analysis)
|
| 856 |
else:
|
| 857 |
+
st.warning("β οΈ AI analysis not available. Please set OPENROUTER_API_KEY.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 858 |
|
| 859 |
+
# Generate PDF report
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 860 |
st.subheader("π Download Report")
|
| 861 |
if st.button("Generate PDF Report"):
|
| 862 |
try:
|
|
|
|
| 867 |
|
| 868 |
st.download_button(
|
| 869 |
label="π₯ Download PDF Report",
|
| 870 |
+
data=pdf_buffer.getvalue(),
|
| 871 |
file_name=f"resume_analysis_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf",
|
| 872 |
mime="application/pdf"
|
| 873 |
)
|
| 874 |
except Exception as e:
|
| 875 |
st.error(f"Error generating PDF: {str(e)}")
|
| 876 |
+
|
| 877 |
+
# Manual recommendations
|
| 878 |
+
st.subheader("π Quick Recommendations")
|
| 879 |
+
recommendations = []
|
| 880 |
+
|
| 881 |
+
if ats_score < 70:
|
| 882 |
+
recommendations.extend([
|
| 883 |
+
"Add more bullet points to improve readability",
|
| 884 |
+
"Ensure contact information is clearly visible",
|
| 885 |
+
"Use standard section headings"
|
| 886 |
+
])
|
| 887 |
+
|
| 888 |
+
if match_percentage < 60:
|
| 889 |
+
recommendations.append(f"Include more {selected_role}-specific keywords")
|
| 890 |
+
|
| 891 |
+
if not tech_skills:
|
| 892 |
+
recommendations.append("Add a dedicated technical skills section")
|
| 893 |
+
|
| 894 |
+
if not sections.get('projects'):
|
| 895 |
+
recommendations.append("Consider adding a projects section")
|
| 896 |
+
|
| 897 |
+
for i, rec in enumerate(recommendations, 1):
|
| 898 |
+
st.write(f"{i}. {rec}")
|
| 899 |
|
| 900 |
+
# Chat Interface
|
| 901 |
+
st.header("π¬ Chat with Resume Assistant")
|
|
|
|
| 902 |
|
| 903 |
# Toggle chat visibility
|
| 904 |
+
if st.button("π€ Ask Questions About Your Resume"):
|
|
|
|
| 905 |
st.session_state.show_chat = not st.session_state.show_chat
|
| 906 |
|
| 907 |
if st.session_state.show_chat:
|
| 908 |
+
# Model selection
|
| 909 |
+
available_models = analyzer.chatbot.get_available_free_models()
|
| 910 |
+
selected_model = st.selectbox(
|
| 911 |
+
"Choose AI Model:",
|
| 912 |
+
available_models,
|
| 913 |
+
index=0 if analyzer.chatbot.model in available_models else 0
|
| 914 |
+
)
|
| 915 |
+
|
| 916 |
+
if selected_model != analyzer.chatbot.model:
|
| 917 |
+
analyzer.chatbot.switch_model(selected_model)
|
| 918 |
+
|
| 919 |
+
# Chat input
|
| 920 |
+
if not st.session_state.waiting_for_response:
|
| 921 |
+
user_question = st.text_input(
|
| 922 |
+
"Ask about your resume:",
|
| 923 |
+
placeholder="e.g., How can I improve my resume for a data scientist role?",
|
| 924 |
+
key="chat_input"
|
| 925 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 926 |
|
| 927 |
+
if st.button("Send") and user_question:
|
| 928 |
+
st.session_state.waiting_for_response = True
|
| 929 |
+
st.session_state.chat_history.append(("user", user_question))
|
|
|
|
|
|
|
| 930 |
|
| 931 |
+
with st.spinner("Getting AI response..."):
|
| 932 |
+
response = analyzer.chatbot.generate_response(
|
| 933 |
+
user_question,
|
| 934 |
+
st.session_state.resume_context
|
| 935 |
+
)
|
| 936 |
+
|
| 937 |
+
st.session_state.chat_history.append(("assistant", response))
|
| 938 |
+
st.session_state.waiting_for_response = False
|
| 939 |
+
st.rerun()
|
| 940 |
+
|
| 941 |
+
# Display chat history
|
| 942 |
+
if st.session_state.chat_history:
|
| 943 |
+
st.subheader("Chat History")
|
| 944 |
+
for role, message in st.session_state.chat_history[-6:]: # Show last 6 messages
|
| 945 |
+
if role == "user":
|
| 946 |
+
st.write(f"**You:** {message}")
|
| 947 |
+
else:
|
| 948 |
+
st.write(f"**Assistant:** {message}")
|
| 949 |
+
|
| 950 |
+
if st.button("Clear Chat History"):
|
| 951 |
+
st.session_state.chat_history = []
|
| 952 |
+
st.rerun()
|
| 953 |
+
|
| 954 |
except Exception as e:
|
| 955 |
st.error(f"Error during analysis: {str(e)}")
|
| 956 |
+
st.error("Please check your resume format and try again.")
|
| 957 |
+
|
| 958 |
else:
|
| 959 |
+
st.error("β Could not extract text from the uploaded file. Please check the file format and try again.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 960 |
|
| 961 |
+
else:
|
| 962 |
+
# Show instructions when no file is uploaded
|
| 963 |
+
st.info("π Please upload your resume to get started!")
|
| 964 |
+
|
| 965 |
+
# Show sample analysis
|
| 966 |
+
with st.expander("π What you'll get:", expanded=True):
|
| 967 |
st.markdown("""
|
| 968 |
+
**Our AI-powered resume analyzer provides:**
|
| 969 |
+
|
| 970 |
+
- **π Comprehensive Scoring**: ATS compatibility and role-specific matching scores
|
| 971 |
+
- **π― Skills Analysis**: Technical and soft skills identification
|
| 972 |
+
- **π Section Breakdown**: Detailed analysis of each resume section
|
| 973 |
+
- **π ATS Optimization**: Tips to improve applicant tracking system compatibility
|
| 974 |
+
- **π€ AI Chat Assistant**: Ask questions and get personalized advice
|
| 975 |
+
- **π PDF Report**: Downloadable analysis report
|
| 976 |
+
|
| 977 |
+
**Supported Formats**: PDF, DOCX, TXT
|
| 978 |
""")
|
| 979 |
|
| 980 |
if __name__ == "__main__":
|