Spaces:
Running
Running
import streamlit as st | |
import json | |
from mistralai import Mistral | |
# ----------------------- | |
# Config | |
# ----------------------- | |
EXIT_KEYWORDS = {"exit", "quit", "bye", "stop", "end"} | |
STORAGE_FILE = "candidates.json" | |
# Use Mistral hosted API (instead of local Ollama) | |
MODEL_NAME = "mistral-small" # can also try "mistral-medium" or "mistral-large" | |
# Init Mistral LLM | |
client = Mistral(api_key=st.secrets["MISTRAL_API_KEY"]) | |
# ----------------------- | |
# Helpers | |
# ----------------------- | |
def save_candidate(data): | |
"""Save candidate info to local JSON file.""" | |
try: | |
with open(STORAGE_FILE, "r") as f: | |
db = json.load(f) | |
except FileNotFoundError: | |
db = [] | |
db.append(data) | |
with open(STORAGE_FILE, "w") as f: | |
json.dump(db, f, indent=2) | |
def generate_questions(tech_stack): | |
"""Generate structured technical questions in ENGLISH using Mistral.""" | |
prompt = f""" | |
Candidate tech stack: {tech_stack}. | |
For each technology, generate exactly 3 short interview questions in ENGLISH. | |
Respond strictly in JSON format like this: | |
{{ | |
"Python": ["Question1", "Question2", "Question3"], | |
"Django": ["Question1", "Question2", "Question3"] | |
}} | |
No explanations, no extra text, only valid JSON. | |
""" | |
response = client.chat.complete( | |
model=MODEL_NAME, | |
messages=[{"role": "user", "content": prompt}], | |
temperature=0.2, | |
) | |
raw = response.choices[0].message["content"] | |
# Try to parse clean JSON | |
try: | |
return json.loads(raw) | |
except: | |
cleaned = raw.strip().split("```")[-1] | |
try: | |
return json.loads(cleaned) | |
except: | |
return {"Error": [raw]} | |
def chat_response(user_input): | |
"""Handles normal chat responses.""" | |
prompt = f""" | |
You are TalentScout Hiring Assistant. | |
Always answer clearly in English. | |
Do NOT repeat or rephrase the user question, only provide the best possible answer. | |
Question: {user_input} | |
Answer: | |
""" | |
response = client.chat.complete( | |
model=MODEL_NAME, | |
messages=[{"role": "user", "content": prompt}], | |
temperature=0.2, | |
) | |
return response.choices[0].message["content"] | |
def estimate_skill_levels(qa_history, tech_stack): | |
"""Estimate skill levels (Beginner / Intermediate / Expert) based on candidate answers.""" | |
answers = "\n".join( | |
f"Q: {q}\nA: {a}" | |
for role, msg in qa_history | |
if role == "You" | |
for q, a in [(msg, "")] | |
) | |
prompt = f""" | |
Candidate tech stack: {tech_stack} | |
Candidate interview answers: | |
{answers} | |
Based on the answers, estimate the candidate's skill level for each technology | |
in this stack. Use only these labels: Beginner, Intermediate, Expert. | |
Respond strictly in JSON format like this: | |
{{ | |
"Python": "Intermediate", | |
"Django": "Beginner" | |
}} | |
""" | |
response = client.chat.complete( | |
model=MODEL_NAME, | |
messages=[{"role": "user", "content": prompt}], | |
temperature=0.2, | |
) | |
raw = response.choices[0].message["content"] | |
try: | |
return json.loads(raw) | |
except: | |
return {"Error": raw} | |
# ----------------------- | |
# Streamlit UI | |
# ----------------------- | |
st.set_page_config(page_title="TalentScout Hiring Assistant", page_icon="π€") | |
st.title("π€ TalentScout Hiring Assistant") | |
st.write(""" | |
Welcome to the **TalentScout Hiring Assistant**. | |
I will: | |
- Collect your professional details (experience, skills, and career interests) | |
- Generate tailored **technical interview questions** based on your tech stack | |
- Conduct a structured Q&A session where you can respond to questions or ask clarifications | |
- Provide a skill-level estimation (Beginner / Intermediate / Expert) for each technology | |
- Maintain a transcript of the conversation for review | |
At any time, type **exit**, **quit**, or **end** to finish the interview. | |
""") | |
# Session state | |
if "qa_history" not in st.session_state: | |
st.session_state.qa_history = [] | |
if "candidate_info" not in st.session_state: | |
st.session_state.candidate_info = {} | |
if "chat_input" not in st.session_state: | |
st.session_state.chat_input = "" | |
if "skill_levels" not in st.session_state: | |
st.session_state.skill_levels = {} | |
# ----------------------- | |
# Candidate Info Form | |
# ----------------------- | |
with st.form("candidate_form"): | |
st.subheader("π Candidate Information") | |
name = st.text_input("Full Name") | |
email = st.text_input("Email Address") | |
phone = st.text_input("Phone Number") | |
exp = st.number_input("Years of Experience", min_value=0, step=1) | |
position = st.text_input("Desired Position(s)") | |
location = st.text_input("Current Location") | |
tech_stack = st.text_area("Tech Stack (e.g., Python, Django, MySQL)") | |
submitted = st.form_submit_button("Submit") | |
if submitted: | |
st.session_state.candidate_info = { | |
"name": name, | |
"email": email, | |
"phone": phone, | |
"experience": exp, | |
"position": position, | |
"location": location, | |
"tech_stack": tech_stack, | |
} | |
save_candidate(st.session_state.candidate_info) | |
st.success("β Information saved! Scroll down to see your technical questions.") | |
# ----------------------- | |
# Generate questions if candidate info available | |
# ----------------------- | |
if st.session_state.candidate_info.get("tech_stack"): | |
st.subheader("π§βπ» Technical Questions") | |
with st.spinner("Generating questions..."): | |
questions_dict = generate_questions(st.session_state.candidate_info["tech_stack"]) | |
if "Error" in questions_dict: | |
st.error("β οΈ Could not parse questions properly. Raw output:") | |
st.write(questions_dict["Error"]) | |
else: | |
for tech, qs in questions_dict.items(): | |
st.markdown(f"**{tech}**") | |
for i, q in enumerate(qs, 1): | |
st.write(f"{i}. {q}") | |
# ----------------------- | |
# Chatbox for conversation | |
# ----------------------- | |
st.subheader("π¬ Chat with Assistant") | |
def process_input(): | |
"""Handles user input when submitted.""" | |
user_input = st.session_state.chat_input.strip() | |
if not user_input: | |
return | |
cleaned = user_input.lower() | |
# β Exit keywords | |
if cleaned in EXIT_KEYWORDS: | |
st.session_state.qa_history.append(("You", user_input)) | |
st.session_state.qa_history.append(("Assistant", "π Thank you for your time! Weβll contact you with next steps.")) | |
st.session_state.chat_input = "" # clears input | |
return | |
# Otherwise β normal chat flow | |
with st.spinner("Assistant is typing..."): | |
final_response = chat_response(user_input) | |
st.session_state.qa_history.append(("You", user_input)) | |
st.session_state.qa_history.append(("Assistant", final_response)) | |
# Clear input box after processing | |
st.session_state.chat_input = "" | |
# Input box with callback | |
st.text_input( | |
"Your message:", | |
key="chat_input", | |
on_change=process_input, | |
) | |
# Clear conversation button | |
if st.button("ποΈ Clear Conversation"): | |
st.session_state.qa_history = [] | |
st.session_state.skill_levels = {} | |
st.success("Conversation cleared!") | |
# Display transcript | |
for role, msg in st.session_state.qa_history: | |
if role == "You": | |
st.markdown(f"**π§ {role}:** {msg}") | |
else: | |
st.markdown(f"**π€ {role}:** {msg}") | |
# ----------------------- | |
# Skill Level Estimation | |
# ----------------------- | |
if st.button("π Estimate Skill Levels"): | |
if st.session_state.qa_history and st.session_state.candidate_info.get("tech_stack"): | |
with st.spinner("Analyzing candidate answers..."): | |
st.session_state.skill_levels = estimate_skill_levels( | |
st.session_state.qa_history, | |
st.session_state.candidate_info["tech_stack"] | |
) | |
st.subheader("π Skill Level Estimation") | |
st.json(st.session_state.skill_levels) | |
else: | |
st.warning("β οΈ Please complete candidate details and provide some answers first.") | |