JobSearch / app.py
Adjoumani's picture
Update app.py
2f16005 verified
import pandas as pd
import streamlit as st
from jobspy import scrape_jobs
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
from openai import OpenAI
import json
import os
from datetime import datetime
from dotenv import load_dotenv
# Charger les variables d'environnement depuis un fichier .env
load_dotenv()
# Configuration de l'API OpenAI (ChatGPT) avec OpenRouter
openai = OpenAI(
base_url="https://openrouter.ai/api/v1",
api_key="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",
)
# Initialisation des variables de session
if 'global_jobs_df' not in st.session_state:
st.session_state.global_jobs_df = pd.DataFrame()
if 'selected_job' not in st.session_state:
st.session_state.selected_job = {}
# Fonction pour collecter les offres d'emploi
def collect_job_data(keywords, location, results_wanted=100):
try:
jobs = scrape_jobs(
site_name=[
'google',
],
search_term=keywords,
location=location,
results_wanted=results_wanted
)
return jobs
except Exception as e:
st.error(f"Erreur lors de la collecte des données: {e}")
return pd.DataFrame()
# Fonction pour enrichir les données d'emploi avec l'IA
def enrich_job_data(jobs_df):
if jobs_df.empty:
return jobs_df
# Vectorisation des descriptions pour l'analyse
vectorizer = TfidfVectorizer(max_features=1000)
job_vectors = vectorizer.fit_transform(jobs_df['description'].fillna(''))
# Extraction des compétences clés (version simplifiée)
jobs_df['key_skills'] = jobs_df['description'].apply(extract_skills_from_text)
# Ajout des vecteurs pour le matching
jobs_df['vector'] = list(job_vectors.toarray())
return jobs_df
# Extraction simplifiée des compétences
def extract_skills_from_text(text):
if not isinstance(text, str):
return []
common_skills = ["python", "java", "sql", "javascript", "machine learning",
"data analysis", "project management", "communication"]
return [skill for skill in common_skills if skill.lower() in text.lower()]
# Fonction de matching entre profil utilisateur et offres
def match_jobs_to_profile(jobs_df, user_profile):
if jobs_df.empty:
return jobs_df
# Créer un vecteur pour les préférences utilisateur
user_keywords = ' '.join([
user_profile.get('job_title', ''),
user_profile.get('skills', ''),
user_profile.get('interests', '')
])
vectorizer = TfidfVectorizer(max_features=1000)
all_texts = jobs_df['description'].fillna('').tolist() + [user_keywords]
vectors = vectorizer.fit_transform(all_texts)
user_vector = vectors[-1]
job_vectors = vectors[:-1]
# Calcul des scores de similarité
similarity_scores = cosine_similarity(user_vector, job_vectors).flatten()
jobs_df['match_score'] = similarity_scores
matched_jobs = jobs_df.sort_values('match_score', ascending=False).reset_index(drop=True)
return matched_jobs
# Génération de CV avec ChatGPT API
def generate_cv(job_details, user_profile):
try:
prompt = f"""
Génère un CV professionnel pour la candidature au poste suivant:
POSTE: {job_details.get('title', '')}
ENTREPRISE: {job_details.get('company', '')}
DESCRIPTION DU POSTE : {job_details.get('description', '')}
PROFIL DU CANDIDAT:
Nom: {user_profile.get('name', '')}
Formation: {user_profile.get('education', '')}
Expériences: {user_profile.get('experience', '')}
Compétences: {user_profile.get('skills', '')}
Format le CV de manière professionnelle et adapte-le spécifiquement pour ce poste.
"""
with st.spinner('Génération du CV en cours...'):
response = openai.chat.completions.create(
model="qwen/qwen2.5-vl-72b-instruct:free",
messages=[
{"role": "system", "content": "Tu es un expert en rédaction de CV qui adapte parfaitement les profils aux offres d'emploi."},
{"role": "user", "content": prompt}
],
max_tokens=1500
)
return response.choices[0].message.content
except Exception as e:
return f"Erreur lors de la génération du CV: {str(e)}"
# Génération de lettre de motivation avec ChatGPT API
def generate_cover_letter(job_details, user_profile):
try:
prompt = f"""
Génère une lettre de motivation personnalisée pour la candidature au poste suivant:
POSTE: {job_details.get('title', '')}
ENTREPRISE: {job_details.get('company', '')}
DESCRIPTION DU POSTE: {job_details.get('description', '')}
PROFIL DU CANDIDAT:
Nom: {user_profile.get('name', '')}
Formation: {user_profile.get('education', '')}
Expériences: {user_profile.get('experience', '')}
Compétences: {user_profile.get('skills', '')}
Motivation pour ce poste: {user_profile.get('motivation', '')}
La lettre doit être convaincante, professionnelle et adaptée spécifiquement à cette offre.
"""
with st.spinner('Génération de la lettre de motivation en cours...'):
response = openai.chat.completions.create(
model="qwen/qwen2.5-vl-72b-instruct:free",
messages=[
{"role": "system", "content": "Tu es un expert en rédaction de lettres de motivation persuasives et personnalisées."},
{"role": "user", "content": prompt}
],
max_tokens=1000
)
return response.choices[0].message.content
except Exception as e:
return f"Erreur lors de la génération de la lettre de motivation: {str(e)}"
# Fonction pour la recherche d'emploi
def search_jobs(keywords, location, job_title, skills, interests):
# Profil utilisateur basique
user_profile = {
'job_title': job_title,
'skills': skills,
'interests': interests
}
with st.spinner('Recherche des offres en cours...'):
# Collecte et enrichissement des données
jobs_df = collect_job_data(keywords, location)
enriched_jobs = enrich_job_data(jobs_df)
# Matching avec le profil utilisateur
matched_jobs = match_jobs_to_profile(enriched_jobs, user_profile)
# Stockage pour utilisation ultérieure
st.session_state.global_jobs_df = matched_jobs
return matched_jobs
# Fonction pour sélectionner une offre d'emploi
def select_job(job_index):
if st.session_state.global_jobs_df.empty or job_index >= len(st.session_state.global_jobs_df):
st.error("Offre d'emploi non trouvée.")
return
job = st.session_state.global_jobs_df.iloc[job_index]
st.session_state.selected_job = {
'title': job.get('title', ''),
'company': job.get('company', ''),
'location': job.get('location', ''),
'description': job.get('description', ''),
'date_posted': job.get('date_posted', ''),
'salary': job.get('salary', 'Non spécifié'),
'link': job.get('url', '#')
}
# Interface principale Streamlit
def main():
st.set_page_config(
page_title="Recherche d'Emploi IA",
page_icon="🔍",
layout="wide"
)
st.title("Recherche d'Emploi Intelligente")
# Navigation par onglets
tab1, tab2, tab3 = st.tabs(["Recherche", "Détails de l'offre", "Générer Documents"])
# Onglet Recherche
with tab1:
col1, col2 = st.columns([1, 1])
with col1:
st.subheader("Critères de recherche")
keywords = st.text_input("Mots-clés de recherche")
location = st.text_input("Lieu")
st.subheader("Votre profil")
job_title = st.text_input("Poste recherché")
skills = st.text_input("Compétences (séparées par des virgules)")
interests = st.text_input("Centres d'intérêt")
if st.button("Rechercher", type="primary"):
if not keywords or not location:
st.error("Veuillez saisir des mots-clés et un lieu pour la recherche.")
else:
results = search_jobs(keywords, location, job_title, skills, interests)
if results.empty:
st.error("Aucun résultat trouvé.")
with col2:
st.subheader("Résultats de recherche")
if not st.session_state.global_jobs_df.empty:
# Affichage des résultats sous forme de tableau
results_df = st.session_state.global_jobs_df.head(10)[['title', 'company', 'location']].copy()
results_df['match_score'] = st.session_state.global_jobs_df.head(10)['match_score'].apply(lambda x: f"{x*100:.1f}%")
results_df['actions'] = 'Voir détails'
# Utiliser st.dataframe avec sélection de ligne
selection = st.dataframe(
results_df,
column_config={
"title": "Titre du poste",
"company": "Entreprise",
"location": "Lieu",
"match_score": "Score de correspondance",
"actions": st.column_config.LinkColumn("Actions")
},
use_container_width=True,
hide_index=False
)
st.write("Sélectionnez une offre pour voir les détails")
job_index = st.number_input("ID de l'offre à consulter",
min_value=0,
max_value=len(st.session_state.global_jobs_df)-1 if not st.session_state.global_jobs_df.empty else 0,
step=1)
if st.button("Voir les détails"):
select_job(job_index)
st.switch_page("#détails-de-l'offre") # Navigation vers l'onglet détails
# Onglet Détails de l'offre
with tab2:
if st.session_state.selected_job:
job = st.session_state.selected_job
st.header(f"{job['title']} - {job['company']}")
col1, col2 = st.columns([1, 1])
with col1:
st.write(f"**Lieu:** {job['location']}")
with col2:
st.write(f"**Date de publication:** {job['date_posted']}")
st.write(f"**Salaire:** {job['salary']}")
st.subheader("Description du poste")
st.write(job['description'])
if job['link'] and job['link'] != '#':
st.link_button("Voir l'offre originale", job['link'])
else:
st.info("Veuillez d'abord sélectionner une offre d'emploi dans l'onglet Recherche.")
# Onglet Générer Documents
with tab3:
if not st.session_state.selected_job:
st.info("Veuillez d'abord sélectionner une offre d'emploi dans l'onglet Recherche.")
else:
st.subheader("Générer CV et Lettre de motivation")
st.write("Complétez votre profil pour générer les documents")
name = st.text_input("Nom complet")
education = st.text_input("Formation")
experience = st.text_area("Expériences professionnelles", height=150)
detailed_skills = st.text_area("Compétences détaillées", height=100)
motivation = st.text_area("Votre motivation pour ce poste", height=100)
col1, col2 = st.columns(2)
with col1:
if st.button("Générer CV", type="primary"):
if not name or not education or not experience or not detailed_skills:
st.error("Veuillez remplir tous les champs obligatoires.")
else:
user_profile = {
'name': name,
'education': education,
'experience': experience,
'skills': detailed_skills,
'motivation': motivation
}
cv = generate_cv(st.session_state.selected_job, user_profile)
st.session_state.cv = cv
with col2:
if st.button("Générer Lettre de motivation", type="primary"):
if not name or not education or not experience or not detailed_skills or not motivation:
st.error("Veuillez remplir tous les champs, y compris votre motivation.")
else:
user_profile = {
'name': name,
'education': education,
'experience': experience,
'skills': detailed_skills,
'motivation': motivation
}
cover_letter = generate_cover_letter(st.session_state.selected_job, user_profile)
st.session_state.cover_letter = cover_letter
# Affichage du CV généré
if 'cv' in st.session_state:
st.subheader("CV généré")
st.markdown(st.session_state.cv)
# Bouton pour télécharger le CV
st.download_button(
label="Télécharger le CV",
data=st.session_state.cv,
file_name="CV_généré.md",
mime="text/markdown"
)
# Affichage de la lettre de motivation générée
if 'cover_letter' in st.session_state:
st.subheader("Lettre de motivation générée")
st.markdown(st.session_state.cover_letter)
# Bouton pour télécharger la lettre de motivation
st.download_button(
label="Télécharger la lettre de motivation",
data=st.session_state.cover_letter,
file_name="Lettre_de_motivation.md",
mime="text/markdown"
)
if __name__ == "__main__":
main()