|
|
import os |
|
|
import sys |
|
|
import json |
|
|
import time |
|
|
import gradio as gr |
|
|
import google.generativeai as genai |
|
|
|
|
|
from intent_detector import detect_intent |
|
|
from rag_utils import retrieve_profiles, retrieve_jobs |
|
|
|
|
|
|
|
|
API_KEY = os.getenv("GOOGLE_API_KEY") |
|
|
if not API_KEY: |
|
|
raise ValueError("Error: Set GOOGLE_API_KEY environment variable before running.") |
|
|
genai.configure(api_key=API_KEY) |
|
|
|
|
|
|
|
|
SYSTEM_PROMPT = """ |
|
|
You are a helpful AI assistant that can perform three main tasks: |
|
|
|
|
|
1. ONBOARD – Help professionals create their profile by asking questions ONE AT A TIME in a natural conversation flow |
|
|
2. SEARCH – Help find job opportunities based on user requirements |
|
|
3. POST – Help clients create structured job postings |
|
|
|
|
|
ONBOARDING CONVERSATION FLOW: |
|
|
When onboarding a user, ask questions in this specific order, ONE QUESTION AT A TIME: |
|
|
1. First, ask for their full name |
|
|
2. Then ask what their current role/job title is |
|
|
3. Then ask about their key skills (programming languages, technologies, etc.) |
|
|
4. Then ask about their years of experience |
|
|
5. Then ask about their preferred location or if they want remote work |
|
|
6. Finally, summarize their profile and confirm if everything looks correct |
|
|
|
|
|
IMPORTANT ONBOARDING RULES: |
|
|
- Ask ONLY ONE question at a time |
|
|
- Wait for the user's response before asking the next question |
|
|
- Be conversational and friendly |
|
|
- If they provide multiple pieces of information at once, acknowledge what they shared and ask for what's still missing |
|
|
- Keep questions simple and clear |
|
|
- After collecting all information, show a summary and ask for confirmation |
|
|
|
|
|
SEARCH BEHAVIOR: |
|
|
For job searches, retrieve relevant listings and present them clearly with title, company, and key details. |
|
|
|
|
|
POST BEHAVIOR: |
|
|
For job postings, help create structured JSON format job listings. |
|
|
|
|
|
Always be helpful, conversational, and ask follow-up questions when needed. |
|
|
""" |
|
|
|
|
|
class ChatbotState: |
|
|
"""Manages the conversation state for onboarding flow.""" |
|
|
|
|
|
def __init__(self): |
|
|
self.reset() |
|
|
|
|
|
def reset(self): |
|
|
"""Reset the conversation state.""" |
|
|
self.mode = None |
|
|
self.onboarding_step = 0 |
|
|
self.user_data = {} |
|
|
self.onboarding_complete = False |
|
|
|
|
|
def is_onboarding(self): |
|
|
"""Check if currently in onboarding mode.""" |
|
|
return self.mode == "onboarding" and not self.onboarding_complete |
|
|
|
|
|
def get_next_onboarding_question(self): |
|
|
"""Get the next question in the onboarding flow.""" |
|
|
questions = [ |
|
|
"What's your full name?", |
|
|
"What's your current role or job title?", |
|
|
"What are your key skills? (For example: Python, React, project management, etc.)", |
|
|
"How many years of experience do you have in your field?", |
|
|
"What's your preferred work location? (Or would you prefer remote work?)" |
|
|
] |
|
|
|
|
|
if self.onboarding_step < len(questions): |
|
|
return questions[self.onboarding_step] |
|
|
return None |
|
|
|
|
|
def save_onboarding_data(self, field, value): |
|
|
"""Save user response to onboarding data.""" |
|
|
fields = ["name", "role", "skills", "experience", "location"] |
|
|
if self.onboarding_step < len(fields): |
|
|
self.user_data[fields[self.onboarding_step]] = value |
|
|
|
|
|
def get_onboarding_summary(self): |
|
|
"""Generate a summary of collected onboarding data.""" |
|
|
summary = "Here's your profile summary:\n" |
|
|
summary += f"• Name: {self.user_data.get('name', 'Not provided')}\n" |
|
|
summary += f"• Role: {self.user_data.get('role', 'Not provided')}\n" |
|
|
summary += f"• Skills: {self.user_data.get('skills', 'Not provided')}\n" |
|
|
summary += f"• Experience: {self.user_data.get('experience', 'Not provided')}\n" |
|
|
summary += f"• Location: {self.user_data.get('location', 'Not provided')}\n" |
|
|
summary += "\nDoes this look correct? (yes/no)" |
|
|
return summary |
|
|
|
|
|
def save_user_profile(user_data, filename="data/user_profiles.jsonl"): |
|
|
"""Save user profile to file.""" |
|
|
os.makedirs(os.path.dirname(filename), exist_ok=True) |
|
|
|
|
|
|
|
|
user_id = f"user_{int(time.time())}" |
|
|
|
|
|
profile_entry = { |
|
|
"id": user_id, |
|
|
"name": user_data.get("name", ""), |
|
|
"role": user_data.get("role", ""), |
|
|
"skills": user_data.get("skills", "").split(", ") if user_data.get("skills") else [], |
|
|
"experience": user_data.get("experience", ""), |
|
|
"location": user_data.get("location", ""), |
|
|
"timestamp": time.time() |
|
|
} |
|
|
|
|
|
try: |
|
|
with open(filename, "a", encoding="utf-8") as f: |
|
|
f.write(json.dumps(profile_entry) + "\n") |
|
|
return True |
|
|
except Exception as e: |
|
|
print(f"Error saving profile: {e}") |
|
|
return False |
|
|
|
|
|
|
|
|
chatbot_state = ChatbotState() |
|
|
model = None |
|
|
|
|
|
def initialize_model(model_name="gemini-1.5-flash"): |
|
|
"""Initialize the Gemini model.""" |
|
|
global model |
|
|
model = genai.GenerativeModel(model_name) |
|
|
return model |
|
|
|
|
|
def process_message(message, history, model_name): |
|
|
"""Process user message and return bot response.""" |
|
|
global chatbot_state, model |
|
|
|
|
|
if model is None: |
|
|
initialize_model(model_name) |
|
|
|
|
|
try: |
|
|
|
|
|
if chatbot_state.is_onboarding(): |
|
|
|
|
|
if chatbot_state.onboarding_step < 5: |
|
|
|
|
|
chatbot_state.save_onboarding_data(None, message) |
|
|
chatbot_state.onboarding_step += 1 |
|
|
|
|
|
|
|
|
if chatbot_state.onboarding_step < 5: |
|
|
next_question = chatbot_state.get_next_onboarding_question() |
|
|
return f"Great! {next_question}" |
|
|
else: |
|
|
|
|
|
summary = chatbot_state.get_onboarding_summary() |
|
|
return summary |
|
|
|
|
|
elif chatbot_state.onboarding_step == 5: |
|
|
if message.lower() in ["yes", "y", "correct", "looks good"]: |
|
|
|
|
|
if save_user_profile(chatbot_state.user_data): |
|
|
response = "Perfect! Your profile has been saved successfully. You're now onboarded and ready to search for jobs! 🎉" |
|
|
else: |
|
|
response = "Your profile information has been recorded. You're now onboarded! 🎉" |
|
|
|
|
|
chatbot_state.onboarding_complete = True |
|
|
chatbot_state.mode = None |
|
|
return response |
|
|
|
|
|
elif message.lower() in ["no", "n", "incorrect", "wrong"]: |
|
|
chatbot_state.reset() |
|
|
chatbot_state.mode = "onboarding" |
|
|
chatbot_state.onboarding_step = 0 |
|
|
return "No problem! Let's start over. What's your full name?" |
|
|
|
|
|
else: |
|
|
return "Please answer 'yes' if the information is correct, or 'no' if you'd like to start over." |
|
|
|
|
|
|
|
|
intent = detect_intent(message, model=model_name) |
|
|
print(f"[Debug] Detected intent: {intent}") |
|
|
|
|
|
|
|
|
if intent == "ONBOARD": |
|
|
|
|
|
chatbot_state.reset() |
|
|
chatbot_state.mode = "onboarding" |
|
|
|
|
|
|
|
|
first_question = chatbot_state.get_next_onboarding_question() |
|
|
return f"I'd be happy to help you create your professional profile! Let's start with some basic information.\n\n{first_question}" |
|
|
|
|
|
elif intent == "SEARCH": |
|
|
|
|
|
context_block = None |
|
|
job_results = retrieve_jobs(message, top_k=3) |
|
|
|
|
|
if job_results: |
|
|
lines = [] |
|
|
for idx, job in enumerate(job_results, start=1): |
|
|
lines.append(f"{idx}) {job['text']}") |
|
|
block_text = "\n".join(lines) |
|
|
context_block = f"Relevant job listings:\n{block_text}" |
|
|
print(f"[Debug] Found {len(job_results)} relevant jobs") |
|
|
else: |
|
|
print("[Debug] No relevant jobs found") |
|
|
|
|
|
|
|
|
search_prompt = f""" |
|
|
{SYSTEM_PROMPT} |
|
|
|
|
|
{"Context Information: " + context_block if context_block else "No relevant jobs found in the database."} |
|
|
|
|
|
User is looking for jobs: {message} |
|
|
|
|
|
Please provide a helpful response about job opportunities. If jobs were found, present them clearly. If no jobs were found, provide helpful guidance on how they might refine their search. |
|
|
""" |
|
|
|
|
|
response = model.generate_content(search_prompt) |
|
|
return response.text |
|
|
|
|
|
elif intent == "POST": |
|
|
|
|
|
post_prompt = f""" |
|
|
{SYSTEM_PROMPT} |
|
|
|
|
|
User wants to create a job posting: {message} |
|
|
|
|
|
Help them create a structured job posting. Ask for any missing information needed to create a complete job post (title, description, required skills, budget, timeline, etc.). |
|
|
""" |
|
|
|
|
|
response = model.generate_content(post_prompt) |
|
|
return response.text |
|
|
|
|
|
else: |
|
|
|
|
|
general_prompt = f""" |
|
|
{SYSTEM_PROMPT} |
|
|
|
|
|
User message: {message} |
|
|
|
|
|
Respond helpfully. If they seem to want to get started with onboarding, job searching, or job posting, guide them appropriately. |
|
|
""" |
|
|
|
|
|
response = model.generate_content(general_prompt) |
|
|
return response.text |
|
|
|
|
|
except Exception as e: |
|
|
return f"Sorry, I encountered an error: {str(e)}. Please try again!" |
|
|
|
|
|
def reset_conversation(): |
|
|
"""Reset the conversation state.""" |
|
|
global chatbot_state |
|
|
chatbot_state.reset() |
|
|
return [], "" |
|
|
|
|
|
def get_welcome_message(): |
|
|
"""Get the initial welcome message.""" |
|
|
return """👋 **Welcome to AI Job Assistant!** |
|
|
|
|
|
I can help you with: |
|
|
• 🎯 **Creating your professional profile** - Let me guide you through a quick onboarding |
|
|
• 🔍 **Finding job opportunities** - Search for jobs that match your skills |
|
|
• 📝 **Creating job postings** - Help employers post structured job listings |
|
|
|
|
|
What would you like to do today?""" |
|
|
|
|
|
|
|
|
custom_css = """ |
|
|
.gradio-container { |
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
|
} |
|
|
|
|
|
.chat-message { |
|
|
padding: 1rem; |
|
|
margin: 0.5rem 0; |
|
|
border-radius: 1rem; |
|
|
max-width: 80%; |
|
|
} |
|
|
|
|
|
.user-message { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
color: white; |
|
|
margin-left: auto; |
|
|
} |
|
|
|
|
|
.bot-message { |
|
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); |
|
|
color: white; |
|
|
} |
|
|
|
|
|
/* Dark mode support */ |
|
|
.dark .chat-message { |
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); |
|
|
} |
|
|
|
|
|
.gradio-chatbot { |
|
|
height: 500px; |
|
|
} |
|
|
|
|
|
/* Custom button styles */ |
|
|
.primary-button { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
border: none; |
|
|
color: white; |
|
|
padding: 0.5rem 1rem; |
|
|
border-radius: 0.5rem; |
|
|
font-weight: 500; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.primary-button:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); |
|
|
} |
|
|
""" |
|
|
|
|
|
def create_interface(): |
|
|
"""Create the Gradio interface.""" |
|
|
|
|
|
with gr.Blocks( |
|
|
css=custom_css, |
|
|
theme=gr.themes.Soft( |
|
|
primary_hue="blue", |
|
|
secondary_hue="purple", |
|
|
neutral_hue="slate", |
|
|
), |
|
|
title="AI Job Assistant" |
|
|
) as interface: |
|
|
|
|
|
gr.HTML(""" |
|
|
<div style="text-align: center; padding: 2rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 1rem; margin-bottom: 2rem;"> |
|
|
<h1 style="margin: 0; font-size: 2.5rem; font-weight: bold;">🤖 AI Job Assistant</h1> |
|
|
<p style="margin: 0.5rem 0 0 0; font-size: 1.2rem; opacity: 0.9;">Your intelligent career companion</p> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=4): |
|
|
chatbot = gr.Chatbot( |
|
|
value=[{"role": "assistant", "content": get_welcome_message()}], |
|
|
height=500, |
|
|
show_label=False, |
|
|
container=True, |
|
|
avatar_images=("👤", "🤖"), |
|
|
type="messages" |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
msg = gr.Textbox( |
|
|
placeholder="Type your message here...", |
|
|
show_label=False, |
|
|
scale=4, |
|
|
container=False |
|
|
) |
|
|
send_btn = gr.Button( |
|
|
"Send", |
|
|
variant="primary", |
|
|
scale=1, |
|
|
elem_classes=["primary-button"] |
|
|
) |
|
|
|
|
|
with gr.Column(scale=1, min_width=250): |
|
|
gr.HTML(""" |
|
|
<div style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; padding: 1.5rem; border-radius: 1rem; margin-bottom: 1rem;"> |
|
|
<h3 style="margin: 0 0 1rem 0;">🚀 Quick Actions</h3> |
|
|
<div style="font-size: 0.9rem; line-height: 1.6;"> |
|
|
<div style="margin-bottom: 0.8rem;"> |
|
|
<strong>💼 Get Started:</strong><br> |
|
|
"I want to create my profile" |
|
|
</div> |
|
|
<div style="margin-bottom: 0.8rem;"> |
|
|
<strong>🔍 Find Jobs:</strong><br> |
|
|
"Show me Python developer jobs" |
|
|
</div> |
|
|
<div> |
|
|
<strong>📝 Post Job:</strong><br> |
|
|
"I want to post a job opening" |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
model_selector = gr.Dropdown( |
|
|
choices=["gemini-1.5-flash", "gemini-1.5-pro"], |
|
|
value="gemini-1.5-flash", |
|
|
label="🧠 AI Model", |
|
|
info="Choose your preferred AI model" |
|
|
) |
|
|
|
|
|
clear_btn = gr.Button( |
|
|
"🗑️ Clear Chat", |
|
|
variant="secondary", |
|
|
size="sm" |
|
|
) |
|
|
|
|
|
gr.HTML(""" |
|
|
<div style="background: rgba(102, 126, 234, 0.1); padding: 1rem; border-radius: 0.5rem; margin-top: 1rem;"> |
|
|
<h4 style="margin: 0 0 0.5rem 0; color: #667eea;">ℹ️ Tips</h4> |
|
|
<ul style="margin: 0; padding-left: 1.2rem; font-size: 0.85rem; color: #666;"> |
|
|
<li>Be specific about your skills and preferences</li> |
|
|
<li>Use natural language - I understand context!</li> |
|
|
<li>Ask follow-up questions anytime</li> |
|
|
</ul> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
def respond(message, chat_history, model_name): |
|
|
if not message.strip(): |
|
|
return chat_history, "" |
|
|
|
|
|
bot_message = process_message(message, chat_history, model_name) |
|
|
chat_history.append({"role": "user", "content": message}) |
|
|
chat_history.append({"role": "assistant", "content": bot_message}) |
|
|
return chat_history, "" |
|
|
|
|
|
def clear_chat(): |
|
|
reset_conversation() |
|
|
return [{"role": "assistant", "content": get_welcome_message()}], "" |
|
|
|
|
|
|
|
|
msg.submit(respond, [msg, chatbot, model_selector], [chatbot, msg]) |
|
|
send_btn.click(respond, [msg, chatbot, model_selector], [chatbot, msg]) |
|
|
clear_btn.click(clear_chat, None, [chatbot, msg]) |
|
|
|
|
|
|
|
|
interface.load(lambda: gr.update(focus=True), None, msg) |
|
|
|
|
|
return interface |
|
|
|
|
|
def main(): |
|
|
"""Main function to launch the Gradio interface.""" |
|
|
|
|
|
|
|
|
chosen_model = sys.argv[1] if len(sys.argv) > 1 else "gemini-1.5-flash" |
|
|
initialize_model(chosen_model) |
|
|
|
|
|
|
|
|
interface = create_interface() |
|
|
|
|
|
print("🚀 Launching AI Job Assistant...") |
|
|
print("🌐 Opening in your default browser...") |
|
|
|
|
|
interface.launch( |
|
|
server_name="0.0.0.0", |
|
|
server_port=7860, |
|
|
share=False, |
|
|
show_error=True |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
main() |