Spaces:
Sleeping
Sleeping
Update src/app.py
Browse files- src/app.py +290 -283
src/app.py
CHANGED
|
@@ -59,207 +59,228 @@ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
|
|
| 59 |
from reportlab.lib.styles import getSampleStyleSheet
|
| 60 |
from reportlab.lib.units import inch
|
| 61 |
|
| 62 |
-
|
| 63 |
-
import
|
| 64 |
-
import
|
| 65 |
-
import
|
| 66 |
-
import
|
| 67 |
-
|
|
|
|
| 68 |
|
| 69 |
-
|
|
|
|
| 70 |
def __init__(self):
|
| 71 |
-
self.
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
# Debug mode
|
| 84 |
-
self.debug = True
|
| 85 |
-
|
| 86 |
-
if not self.api_key:
|
| 87 |
-
st.error("❌ OPENROUTER_API_KEY not found in environment variables!")
|
| 88 |
-
st.info("Please set your OpenRouter API key in the environment variables.")
|
| 89 |
-
if self.debug:
|
| 90 |
-
st.write("**Debug**: Checked environment variable 'OPENROUTER_API_KEY'")
|
| 91 |
|
| 92 |
-
def
|
| 93 |
-
"""
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
-
def
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
headers = {
|
| 111 |
-
"Authorization": f"Bearer {self.api_key}",
|
| 112 |
-
"Content-Type": "application/json",
|
| 113 |
-
"HTTP-Referer": "https://streamlit-resume-analyzer.com",
|
| 114 |
-
"X-Title": "AI Resume Analyzer"
|
| 115 |
}
|
|
|
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
"
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
try:
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
st.write(f"**Debug - Response Headers**: {dict(response.headers)}")
|
| 133 |
-
|
| 134 |
-
if response.status_code == 200:
|
| 135 |
-
result = response.json()
|
| 136 |
-
if 'choices' in result and len(result['choices']) > 0:
|
| 137 |
-
return f"✅ Connection successful: {result['choices'][0]['message']['content']}"
|
| 138 |
-
else:
|
| 139 |
-
return f"❌ Unexpected response format: {result}"
|
| 140 |
-
else:
|
| 141 |
-
error_text = response.text
|
| 142 |
-
return f"❌ Connection failed ({response.status_code}): {error_text}"
|
| 143 |
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
except Exception as e:
|
| 149 |
-
|
|
|
|
| 150 |
|
| 151 |
-
def
|
| 152 |
-
"""
|
| 153 |
-
if not self.
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
-
if
|
| 173 |
-
|
| 174 |
-
|
|
|
|
| 175 |
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
"
|
| 179 |
-
{"role": "system", "content": system_prompt},
|
| 180 |
-
{"role": "user", "content": prompt}
|
| 181 |
-
],
|
| 182 |
-
"max_tokens": max_tokens,
|
| 183 |
-
"temperature": 0.7,
|
| 184 |
-
"top_p": 0.9,
|
| 185 |
-
"frequency_penalty": 0.1
|
| 186 |
-
}
|
| 187 |
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
try:
|
| 195 |
-
|
| 196 |
-
|
| 197 |
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
|
| 204 |
-
#
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
return "Rate limit exceeded. Please wait a few minutes and try again."
|
| 212 |
-
|
| 213 |
-
elif response.status_code == 402:
|
| 214 |
-
return "This request requires payment. Please add credits to your OpenRouter account."
|
| 215 |
-
|
| 216 |
-
elif response.status_code == 401:
|
| 217 |
-
return "Invalid API key. Please check your OPENROUTER_API_KEY environment variable."
|
| 218 |
-
|
| 219 |
-
elif response.status_code == 400:
|
| 220 |
-
try:
|
| 221 |
-
error_data = response.json()
|
| 222 |
-
error_message = error_data.get('error', {}).get('message', 'Bad request')
|
| 223 |
-
if self.debug:
|
| 224 |
-
st.write(f"**Debug - 400 Error Details**: {error_data}")
|
| 225 |
-
return f"Bad request: {error_message}"
|
| 226 |
-
except:
|
| 227 |
-
return f"Bad request. Raw response: {response.text[:200]}"
|
| 228 |
-
|
| 229 |
-
elif response.status_code != 200:
|
| 230 |
-
try:
|
| 231 |
-
error_data = response.json()
|
| 232 |
-
error_message = error_data.get('error', {}).get('message', response.text[:200])
|
| 233 |
-
return f"API Error {response.status_code}: {error_message}"
|
| 234 |
-
except:
|
| 235 |
-
return f"API Error {response.status_code}: {response.text[:200]}"
|
| 236 |
|
| 237 |
-
# Success case
|
| 238 |
-
try:
|
| 239 |
-
result = response.json()
|
| 240 |
-
if self.debug:
|
| 241 |
-
st.write(f"**Debug - Response keys**: {list(result.keys())}")
|
| 242 |
-
|
| 243 |
-
if 'choices' in result and len(result['choices']) > 0:
|
| 244 |
-
self.daily_requests += 1
|
| 245 |
-
response_text = result['choices'][0]['message']['content'].strip()
|
| 246 |
-
if self.debug:
|
| 247 |
-
st.success(f"Response generated successfully! (Remaining: {self.max_daily_requests - self.daily_requests})")
|
| 248 |
-
return response_text
|
| 249 |
-
else:
|
| 250 |
-
return f"Error: Unexpected response format. Response keys: {list(result.keys())}"
|
| 251 |
-
except json.JSONDecodeError as e:
|
| 252 |
-
return f"Error: Invalid JSON response. Raw response: {response.text[:200]}"
|
| 253 |
-
|
| 254 |
-
except requests.exceptions.Timeout:
|
| 255 |
-
return "Request timed out after 30 seconds. Please try again with a shorter question."
|
| 256 |
-
except requests.exceptions.ConnectionError:
|
| 257 |
-
return "Connection error. Please check your internet connection and try again."
|
| 258 |
-
except requests.exceptions.RequestException as e:
|
| 259 |
-
return f"Request error: {str(e)}"
|
| 260 |
except Exception as e:
|
| 261 |
-
return f"
|
| 262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
# Download NLTK data if not already present
|
| 264 |
@st.cache_resource
|
| 265 |
def download_nltk_data():
|
|
@@ -336,7 +357,7 @@ def basic_grammar_check(text):
|
|
| 336 |
class ResumeAnalyzer:
|
| 337 |
def __init__(self):
|
| 338 |
self.nlp, self.grammar_tool = init_tools()
|
| 339 |
-
self.chatbot =
|
| 340 |
|
| 341 |
try:
|
| 342 |
self.stop_words = set(stopwords.words('english'))
|
|
@@ -685,6 +706,93 @@ class ResumeAnalyzer:
|
|
| 685 |
buffer.seek(0)
|
| 686 |
return buffer
|
| 687 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 688 |
def main():
|
| 689 |
st.set_page_config(
|
| 690 |
page_title="AI Resume Analyzer with Chatbot",
|
|
@@ -915,87 +1023,8 @@ def main():
|
|
| 915 |
|
| 916 |
# Chat Interface
|
| 917 |
st.header("💬 Chat with Resume Assistant")
|
|
|
|
| 918 |
|
| 919 |
-
# Show API status
|
| 920 |
-
if not os.getenv('OPENROUTER_API_KEY'):
|
| 921 |
-
st.error("❌ Chat feature unavailable: OPENROUTER_API_KEY not configured")
|
| 922 |
-
st.info("Please set your OpenRouter API key in environment variables to use the chat feature.")
|
| 923 |
-
else:
|
| 924 |
-
# Debugging information
|
| 925 |
-
with st.expander("🔧 Debug Information", expanded=False):
|
| 926 |
-
st.write("**API Key Status:**", "✅ Found" if os.getenv('OPENROUTER_API_KEY') else "❌ Missing")
|
| 927 |
-
if os.getenv('OPENROUTER_API_KEY'):
|
| 928 |
-
api_key = os.getenv('OPENROUTER_API_KEY')
|
| 929 |
-
st.write("**API Key Preview:**", f"{api_key[:10]}...{api_key[-4:]}" if len(api_key) > 14 else "Key too short")
|
| 930 |
-
st.write("**Model:**", analyzer.chatbot.model)
|
| 931 |
-
st.write("**Rate Limit Interval:**", f"{analyzer.chatbot.min_request_interval} seconds")
|
| 932 |
-
st.write("**Daily Requests Used:**", f"{analyzer.chatbot.daily_requests}/{analyzer.chatbot.max_daily_requests}")
|
| 933 |
-
|
| 934 |
-
st.info(f"🤖 Using model: {analyzer.chatbot.model}")
|
| 935 |
-
st.warning("⚠️ OpenRouter free tier limits: 50 requests/day, 6+ seconds between requests")
|
| 936 |
-
|
| 937 |
-
# Test API button
|
| 938 |
-
if st.button("🧪 Test API Connection", help="Send a simple test request to verify API is working"):
|
| 939 |
-
with st.spinner("Testing API connection..."):
|
| 940 |
-
test_response = analyzer.chatbot.generate_response(
|
| 941 |
-
"Just say 'API test successful' in exactly those words.",
|
| 942 |
-
""
|
| 943 |
-
)
|
| 944 |
-
st.write("**Test Response:**", test_response)
|
| 945 |
-
|
| 946 |
-
# Chat input
|
| 947 |
-
user_question = st.text_input(
|
| 948 |
-
"Ask about your resume:",
|
| 949 |
-
placeholder="e.g., How can I improve my resume for a data scientist role?",
|
| 950 |
-
key="chat_input",
|
| 951 |
-
help="Ask specific questions about your resume and get personalized advice"
|
| 952 |
-
)
|
| 953 |
-
|
| 954 |
-
col1, col2 = st.columns([1, 4])
|
| 955 |
-
with col1:
|
| 956 |
-
send_button = st.button("Send", type="primary")
|
| 957 |
-
with col2:
|
| 958 |
-
if st.button("Clear Chat"):
|
| 959 |
-
st.session_state.chat_history = []
|
| 960 |
-
st.rerun()
|
| 961 |
-
|
| 962 |
-
if send_button and user_question.strip():
|
| 963 |
-
# Add user message to history
|
| 964 |
-
st.session_state.chat_history.append(("user", user_question))
|
| 965 |
-
|
| 966 |
-
# Get AI response
|
| 967 |
-
with st.spinner("Getting AI response..."):
|
| 968 |
-
response = analyzer.chatbot.generate_response(
|
| 969 |
-
user_question,
|
| 970 |
-
st.session_state.resume_context
|
| 971 |
-
)
|
| 972 |
-
|
| 973 |
-
# Add AI response to history
|
| 974 |
-
st.session_state.chat_history.append(("assistant", response))
|
| 975 |
-
st.rerun()
|
| 976 |
-
|
| 977 |
-
# Display chat history
|
| 978 |
-
if st.session_state.chat_history:
|
| 979 |
-
st.subheader("Chat History")
|
| 980 |
-
|
| 981 |
-
# Display messages in reverse order (newest first) with a limit
|
| 982 |
-
chat_display_limit = 10 # Show last 10 messages
|
| 983 |
-
recent_messages = st.session_state.chat_history[-chat_display_limit:]
|
| 984 |
-
|
| 985 |
-
for role, message in reversed(recent_messages):
|
| 986 |
-
if role == "user":
|
| 987 |
-
st.markdown(f"**👤 You:** {message}")
|
| 988 |
-
else:
|
| 989 |
-
st.markdown(f"**🤖 Assistant:** {message}")
|
| 990 |
-
st.markdown("---")
|
| 991 |
-
|
| 992 |
-
if len(st.session_state.chat_history) > chat_display_limit:
|
| 993 |
-
st.info(f"Showing {chat_display_limit} most recent messages. Total messages: {len(st.session_state.chat_history)}")
|
| 994 |
-
|
| 995 |
-
except Exception as e:
|
| 996 |
-
st.error(f"Error during analysis: {str(e)}")
|
| 997 |
-
st.error("Please check your resume format and try again.")
|
| 998 |
-
|
| 999 |
else:
|
| 1000 |
st.error("❌ Could not extract text from the uploaded file. Please check the file format and try again.")
|
| 1001 |
|
|
@@ -1017,31 +1046,9 @@ def main():
|
|
| 1017 |
|
| 1018 |
**Supported Formats**: PDF, DOCX, TXT
|
| 1019 |
|
| 1020 |
-
**Chat Feature**:
|
| 1021 |
""")
|
| 1022 |
|
| 1023 |
-
# API Setup Instructions
|
| 1024 |
-
with st.expander("🔧 Setup Instructions for Chat Feature"):
|
| 1025 |
-
st.markdown("""
|
| 1026 |
-
To enable the AI chat assistant:
|
| 1027 |
-
|
| 1028 |
-
1. **Get an API key from OpenRouter**:
|
| 1029 |
-
- Visit https://openrouter.ai
|
| 1030 |
-
- Sign up for a free account
|
| 1031 |
-
- Get your API key from the dashboard
|
| 1032 |
-
|
| 1033 |
-
2. **Set the environment variable**:
|
| 1034 |
-
- Add `OPENROUTER_API_KEY=your_api_key_here` to your environment
|
| 1035 |
-
- For local development, you can use a `.env` file
|
| 1036 |
-
- For deployment, set it in your hosting platform's environment variables
|
| 1037 |
-
|
| 1038 |
-
3. **Free tier includes**:
|
| 1039 |
-
- Limited requests per hour
|
| 1040 |
-
- Access to free models like Llama 3.2
|
| 1041 |
-
- No credit card required
|
| 1042 |
-
|
| 1043 |
-
The app will work without the API key, but chat features will be disabled.
|
| 1044 |
-
""")
|
| 1045 |
|
| 1046 |
if __name__ == "__main__":
|
| 1047 |
main()
|
|
|
|
| 59 |
from reportlab.lib.styles import getSampleStyleSheet
|
| 60 |
from reportlab.lib.units import inch
|
| 61 |
|
| 62 |
+
import nltk
|
| 63 |
+
from nltk.tokenize import word_tokenize
|
| 64 |
+
from nltk.corpus import stopwords
|
| 65 |
+
from nltk.stem import WordNetLemmatizer
|
| 66 |
+
import re
|
| 67 |
+
from datetime import datetime
|
| 68 |
+
from typing import Dict, List
|
| 69 |
|
| 70 |
+
# Simple NLP processor
|
| 71 |
+
class SimpleNLPProcessor:
|
| 72 |
def __init__(self):
|
| 73 |
+
self.setup_nltk()
|
| 74 |
+
|
| 75 |
+
def setup_nltk(self):
|
| 76 |
+
try:
|
| 77 |
+
nltk.download('punkt', quiet=True)
|
| 78 |
+
nltk.download('stopwords', quiet=True)
|
| 79 |
+
nltk.download('wordnet', quiet=True)
|
| 80 |
+
self.stop_words = set(stopwords.words('english'))
|
| 81 |
+
self.lemmatizer = WordNetLemmatizer()
|
| 82 |
+
except:
|
| 83 |
+
self.stop_words = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'with'}
|
| 84 |
+
self.lemmatizer = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
+
def process_text(self, text: str) -> str:
|
| 87 |
+
"""Clean and process text for better context"""
|
| 88 |
+
try:
|
| 89 |
+
tokens = word_tokenize(text.lower())
|
| 90 |
+
except:
|
| 91 |
+
tokens = text.lower().split()
|
| 92 |
+
|
| 93 |
+
# Remove stopwords and short tokens
|
| 94 |
+
filtered_tokens = [token for token in tokens
|
| 95 |
+
if token not in self.stop_words and len(token) > 2]
|
| 96 |
+
|
| 97 |
+
# Lemmatize if available
|
| 98 |
+
if self.lemmatizer:
|
| 99 |
+
try:
|
| 100 |
+
filtered_tokens = [self.lemmatizer.lemmatize(token) for token in filtered_tokens]
|
| 101 |
+
except:
|
| 102 |
+
pass
|
| 103 |
+
|
| 104 |
+
# Return key terms (limit to avoid long prompts)
|
| 105 |
+
return ' '.join(filtered_tokens[:15])
|
| 106 |
+
|
| 107 |
+
# Simple memory management
|
| 108 |
+
class SimpleChatMemory:
|
| 109 |
+
def __init__(self):
|
| 110 |
+
if 'chat_history' not in st.session_state:
|
| 111 |
+
st.session_state.chat_history = []
|
| 112 |
|
| 113 |
+
def add_conversation(self, user_msg: str, bot_response: str):
|
| 114 |
+
conversation = {
|
| 115 |
+
'user': user_msg,
|
| 116 |
+
'bot': bot_response,
|
| 117 |
+
'timestamp': datetime.now().strftime("%H:%M:%S")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
}
|
| 119 |
+
st.session_state.chat_history.append(conversation)
|
| 120 |
|
| 121 |
+
# Keep only last 8 conversations to save memory
|
| 122 |
+
if len(st.session_state.chat_history) > 8:
|
| 123 |
+
st.session_state.chat_history = st.session_state.chat_history[-8:]
|
| 124 |
+
|
| 125 |
+
def get_recent_context(self, limit: int = 2) -> str:
|
| 126 |
+
"""Get recent conversation context"""
|
| 127 |
+
if not st.session_state.chat_history:
|
| 128 |
+
return ""
|
| 129 |
+
|
| 130 |
+
recent = st.session_state.chat_history[-limit:]
|
| 131 |
+
context_parts = []
|
| 132 |
+
for conv in recent:
|
| 133 |
+
context_parts.append(f"User asked: {conv['user'][:50]}...")
|
| 134 |
|
| 135 |
+
return " | ".join(context_parts) if context_parts else ""
|
| 136 |
+
|
| 137 |
+
# Main chatbot class
|
| 138 |
+
class SimpleCPUChatbot:
|
| 139 |
+
def __init__(self):
|
| 140 |
+
self.model_name = "distilgpt2" # Fast, CPU-friendly model
|
| 141 |
+
self.model = None
|
| 142 |
+
self.tokenizer = None
|
| 143 |
+
self.pipeline = None
|
| 144 |
+
self.nlp_processor = SimpleNLPProcessor()
|
| 145 |
+
self.memory = SimpleChatMemory()
|
| 146 |
+
self.is_loaded = False
|
| 147 |
+
|
| 148 |
+
@st.cache_resource
|
| 149 |
+
def load_model(_self):
|
| 150 |
+
"""Load the model (cached for efficiency)"""
|
| 151 |
try:
|
| 152 |
+
with st.spinner("Loading AI model (first time may take 2-3 minutes)..."):
|
| 153 |
+
# Load tokenizer
|
| 154 |
+
tokenizer = AutoTokenizer.from_pretrained(_self.model_name)
|
| 155 |
+
tokenizer.pad_token = tokenizer.eos_token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
+
# Load model with CPU optimization
|
| 158 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 159 |
+
_self.model_name,
|
| 160 |
+
torch_dtype=torch.float32, # Use float32 for CPU
|
| 161 |
+
low_cpu_mem_usage=True
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
# Create pipeline
|
| 165 |
+
text_generator = pipeline(
|
| 166 |
+
"text-generation",
|
| 167 |
+
model=model,
|
| 168 |
+
tokenizer=tokenizer,
|
| 169 |
+
device=-1, # CPU only
|
| 170 |
+
max_new_tokens=80,
|
| 171 |
+
do_sample=True,
|
| 172 |
+
temperature=0.7,
|
| 173 |
+
top_p=0.9,
|
| 174 |
+
pad_token_id=tokenizer.eos_token_id
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
return model, tokenizer, text_generator
|
| 178 |
except Exception as e:
|
| 179 |
+
st.error(f"Failed to load model: {str(e)}")
|
| 180 |
+
return None, None, None
|
| 181 |
|
| 182 |
+
def initialize(self):
|
| 183 |
+
"""Initialize the chatbot"""
|
| 184 |
+
if not self.is_loaded:
|
| 185 |
+
result = self.load_model()
|
| 186 |
+
if result[0] is not None:
|
| 187 |
+
self.model, self.tokenizer, self.pipeline = result
|
| 188 |
+
self.is_loaded = True
|
| 189 |
+
st.success("AI model loaded successfully!")
|
| 190 |
+
return True
|
| 191 |
+
else:
|
| 192 |
+
st.error("Failed to load AI model")
|
| 193 |
+
return False
|
| 194 |
+
return True
|
| 195 |
+
|
| 196 |
+
def create_prompt(self, user_input: str, resume_context: str = "") -> str:
|
| 197 |
+
"""Create a focused prompt for resume advice"""
|
| 198 |
+
# Process user input for key terms
|
| 199 |
+
key_terms = self.nlp_processor.process_text(user_input)
|
| 200 |
+
|
| 201 |
+
# Get conversation context
|
| 202 |
+
recent_context = self.memory.get_recent_context()
|
| 203 |
|
| 204 |
+
# Build prompt
|
| 205 |
+
prompt_parts = [
|
| 206 |
+
"You are a professional resume consultant. Give specific, helpful advice."
|
| 207 |
+
]
|
| 208 |
|
| 209 |
+
# Add resume context if available (limited)
|
| 210 |
+
if resume_context:
|
| 211 |
+
resume_excerpt = resume_context[:200] + "..." if len(resume_context) > 200 else resume_context
|
| 212 |
+
prompt_parts.append(f"Resume excerpt: {resume_excerpt}")
|
| 213 |
|
| 214 |
+
# Add conversation context
|
| 215 |
+
if recent_context:
|
| 216 |
+
prompt_parts.append(f"Previous topics: {recent_context}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
+
# Add key terms from current question
|
| 219 |
+
if key_terms:
|
| 220 |
+
prompt_parts.append(f"Focus areas: {key_terms}")
|
| 221 |
+
|
| 222 |
+
# Add the actual question
|
| 223 |
+
prompt_parts.append(f"Question: {user_input}")
|
| 224 |
+
prompt_parts.append("Advice:")
|
| 225 |
+
|
| 226 |
+
return " | ".join(prompt_parts)
|
| 227 |
+
|
| 228 |
+
def generate_response(self, user_input: str, resume_context: str = "") -> str:
|
| 229 |
+
"""Generate response using the loaded model"""
|
| 230 |
+
if not self.is_loaded:
|
| 231 |
+
return "Please initialize the AI model first by clicking 'Initialize AI'."
|
| 232 |
|
| 233 |
try:
|
| 234 |
+
# Create prompt
|
| 235 |
+
prompt = self.create_prompt(user_input, resume_context)
|
| 236 |
|
| 237 |
+
# Generate response
|
| 238 |
+
result = self.pipeline(
|
| 239 |
+
prompt,
|
| 240 |
+
max_new_tokens=60, # Keep responses concise
|
| 241 |
+
num_return_sequences=1,
|
| 242 |
+
temperature=0.7,
|
| 243 |
+
do_sample=True,
|
| 244 |
+
top_p=0.9
|
| 245 |
+
)
|
| 246 |
|
| 247 |
+
# Extract and clean response
|
| 248 |
+
generated_text = result[0]['generated_text']
|
| 249 |
+
response = generated_text.replace(prompt, "").strip()
|
| 250 |
|
| 251 |
+
# Clean up the response
|
| 252 |
+
response = self.clean_response(response, user_input)
|
| 253 |
+
|
| 254 |
+
# Add to memory
|
| 255 |
+
self.memory.add_conversation(user_input, response)
|
| 256 |
+
|
| 257 |
+
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
except Exception as e:
|
| 260 |
+
return f"Sorry, I encountered an error: {str(e)}. Please try a simpler question."
|
| 261 |
+
|
| 262 |
+
def clean_response(self, response: str, user_input: str) -> str:
|
| 263 |
+
"""Clean and improve the generated response"""
|
| 264 |
+
# Remove extra whitespace and newlines
|
| 265 |
+
response = re.sub(r'\s+', ' ', response).strip()
|
| 266 |
+
|
| 267 |
+
# Split into sentences and take first few good ones
|
| 268 |
+
sentences = [s.strip() for s in response.split('.') if s.strip()]
|
| 269 |
+
good_sentences = []
|
| 270 |
+
|
| 271 |
+
for sentence in sentences[:3]: # Max 3 sentences
|
| 272 |
+
if len(sentence) > 10 and not sentence.lower().startswith(('you are', 'i am', 'as a')):
|
| 273 |
+
good_sentences.append(sentence)
|
| 274 |
+
|
| 275 |
+
if good_sentences:
|
| 276 |
+
response = '. '.join(good_sentences)
|
| 277 |
+
if not response.endswith('.'):
|
| 278 |
+
response += '.'
|
| 279 |
+
else:
|
| 280 |
+
# Fallback response
|
| 281 |
+
response = "I'd be happy to help with your resume. Could you be more specific about what you need assistance with?"
|
| 282 |
+
|
| 283 |
+
return response
|
| 284 |
# Download NLTK data if not already present
|
| 285 |
@st.cache_resource
|
| 286 |
def download_nltk_data():
|
|
|
|
| 357 |
class ResumeAnalyzer:
|
| 358 |
def __init__(self):
|
| 359 |
self.nlp, self.grammar_tool = init_tools()
|
| 360 |
+
self.chatbot = SimpleCPUChatbot()
|
| 361 |
|
| 362 |
try:
|
| 363 |
self.stop_words = set(stopwords.words('english'))
|
|
|
|
| 706 |
buffer.seek(0)
|
| 707 |
return buffer
|
| 708 |
|
| 709 |
+
def create_simple_chat_interface(resume_context: str = ""):
|
| 710 |
+
"""Create simple chat interface for the resume analyzer"""
|
| 711 |
+
|
| 712 |
+
st.header("🤖 AI Resume Assistant")
|
| 713 |
+
|
| 714 |
+
# Initialize chatbot
|
| 715 |
+
if 'simple_chatbot' not in st.session_state:
|
| 716 |
+
st.session_state.simple_chatbot = SimpleCPUChatbot()
|
| 717 |
+
|
| 718 |
+
chatbot = st.session_state.simple_chatbot
|
| 719 |
+
|
| 720 |
+
# Model initialization
|
| 721 |
+
col1, col2 = st.columns([3, 1])
|
| 722 |
+
|
| 723 |
+
with col1:
|
| 724 |
+
st.info("Using DistilGPT2 - Fast CPU-only model (≈250MB download)")
|
| 725 |
+
|
| 726 |
+
with col2:
|
| 727 |
+
if st.button("Initialize AI", type="primary"):
|
| 728 |
+
chatbot.initialize()
|
| 729 |
+
|
| 730 |
+
# Chat interface
|
| 731 |
+
if chatbot.is_loaded:
|
| 732 |
+
st.success("✅ AI Ready")
|
| 733 |
+
|
| 734 |
+
# Sample questions
|
| 735 |
+
with st.expander("💡 Try asking"):
|
| 736 |
+
sample_questions = [
|
| 737 |
+
"How can I improve my resume?",
|
| 738 |
+
"What skills should I add?",
|
| 739 |
+
"How do I make it more ATS-friendly?",
|
| 740 |
+
"What's wrong with my experience section?"
|
| 741 |
+
]
|
| 742 |
+
for q in sample_questions:
|
| 743 |
+
if st.button(q, key=f"sample_{hash(q)}"):
|
| 744 |
+
st.session_state.current_question = q
|
| 745 |
+
|
| 746 |
+
# Chat input
|
| 747 |
+
user_question = st.text_input(
|
| 748 |
+
"Ask about your resume:",
|
| 749 |
+
value=st.session_state.get('current_question', ''),
|
| 750 |
+
placeholder="How can I improve my resume for tech jobs?",
|
| 751 |
+
key="chat_input"
|
| 752 |
+
)
|
| 753 |
+
|
| 754 |
+
# Send button and clear
|
| 755 |
+
col1, col2 = st.columns([1, 3])
|
| 756 |
+
with col1:
|
| 757 |
+
send_clicked = st.button("Send", type="primary")
|
| 758 |
+
with col2:
|
| 759 |
+
if st.button("Clear Chat"):
|
| 760 |
+
st.session_state.chat_history = []
|
| 761 |
+
if 'current_question' in st.session_state:
|
| 762 |
+
del st.session_state.current_question
|
| 763 |
+
st.experimental_rerun()
|
| 764 |
+
|
| 765 |
+
# Generate response
|
| 766 |
+
if send_clicked and user_question.strip():
|
| 767 |
+
with st.spinner("Thinking..."):
|
| 768 |
+
response = chatbot.generate_response(user_question, resume_context)
|
| 769 |
+
if 'current_question' in st.session_state:
|
| 770 |
+
del st.session_state.current_question
|
| 771 |
+
st.experimental_rerun()
|
| 772 |
+
|
| 773 |
+
# Display chat history
|
| 774 |
+
if st.session_state.chat_history:
|
| 775 |
+
st.subheader("💬 Conversation")
|
| 776 |
+
|
| 777 |
+
for conv in reversed(st.session_state.chat_history[-5:]): # Show last 5
|
| 778 |
+
st.markdown(f"**You:** {conv['user']}")
|
| 779 |
+
st.markdown(f"**AI:** {conv['bot']}")
|
| 780 |
+
st.caption(f"Time: {conv['timestamp']}")
|
| 781 |
+
st.divider()
|
| 782 |
+
|
| 783 |
+
else:
|
| 784 |
+
st.warning("Click 'Initialize AI' to start chatting")
|
| 785 |
+
|
| 786 |
+
with st.expander("ℹ️ About this AI"):
|
| 787 |
+
st.markdown("""
|
| 788 |
+
**Model**: DistilGPT2 (CPU-optimized)
|
| 789 |
+
**Size**: ~250MB download
|
| 790 |
+
**Speed**: 2-5 seconds per response
|
| 791 |
+
**Memory**: ~1GB RAM usage
|
| 792 |
+
|
| 793 |
+
This model runs entirely on your CPU and provides helpful resume advice.
|
| 794 |
+
First initialization will download the model files.
|
| 795 |
+
""")
|
| 796 |
def main():
|
| 797 |
st.set_page_config(
|
| 798 |
page_title="AI Resume Analyzer with Chatbot",
|
|
|
|
| 1023 |
|
| 1024 |
# Chat Interface
|
| 1025 |
st.header("💬 Chat with Resume Assistant")
|
| 1026 |
+
create_simple_chat_interface(st.session_state.get('resume_context', ''))
|
| 1027 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1028 |
else:
|
| 1029 |
st.error("❌ Could not extract text from the uploaded file. Please check the file format and try again.")
|
| 1030 |
|
|
|
|
| 1046 |
|
| 1047 |
**Supported Formats**: PDF, DOCX, TXT
|
| 1048 |
|
| 1049 |
+
**Chat Feature**: AI chat assistance
|
| 1050 |
""")
|
| 1051 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1052 |
|
| 1053 |
if __name__ == "__main__":
|
| 1054 |
main()
|