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()