|
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 |
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
openai = OpenAI( |
|
base_url="https://openrouter.ai/api/v1", |
|
api_key="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", |
|
) |
|
|
|
|
|
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 = {} |
|
|
|
|
|
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() |
|
|
|
|
|
def enrich_job_data(jobs_df): |
|
if jobs_df.empty: |
|
return jobs_df |
|
|
|
|
|
vectorizer = TfidfVectorizer(max_features=1000) |
|
job_vectors = vectorizer.fit_transform(jobs_df['description'].fillna('')) |
|
|
|
|
|
jobs_df['key_skills'] = jobs_df['description'].apply(extract_skills_from_text) |
|
|
|
|
|
jobs_df['vector'] = list(job_vectors.toarray()) |
|
|
|
return jobs_df |
|
|
|
|
|
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()] |
|
|
|
|
|
def match_jobs_to_profile(jobs_df, user_profile): |
|
if jobs_df.empty: |
|
return jobs_df |
|
|
|
|
|
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] |
|
|
|
|
|
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 |
|
|
|
|
|
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)}" |
|
|
|
|
|
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)}" |
|
|
|
|
|
def search_jobs(keywords, location, job_title, skills, interests): |
|
|
|
user_profile = { |
|
'job_title': job_title, |
|
'skills': skills, |
|
'interests': interests |
|
} |
|
|
|
with st.spinner('Recherche des offres en cours...'): |
|
|
|
jobs_df = collect_job_data(keywords, location) |
|
enriched_jobs = enrich_job_data(jobs_df) |
|
|
|
|
|
matched_jobs = match_jobs_to_profile(enriched_jobs, user_profile) |
|
|
|
|
|
st.session_state.global_jobs_df = matched_jobs |
|
|
|
return matched_jobs |
|
|
|
|
|
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', '#') |
|
} |
|
|
|
|
|
def main(): |
|
st.set_page_config( |
|
page_title="Recherche d'Emploi IA", |
|
page_icon="🔍", |
|
layout="wide" |
|
) |
|
|
|
st.title("Recherche d'Emploi Intelligente") |
|
|
|
|
|
tab1, tab2, tab3 = st.tabs(["Recherche", "Détails de l'offre", "Générer Documents"]) |
|
|
|
|
|
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: |
|
|
|
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' |
|
|
|
|
|
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") |
|
|
|
|
|
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.") |
|
|
|
|
|
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 |
|
|
|
|
|
if 'cv' in st.session_state: |
|
st.subheader("CV généré") |
|
st.markdown(st.session_state.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" |
|
) |
|
|
|
|
|
if 'cover_letter' in st.session_state: |
|
st.subheader("Lettre de motivation générée") |
|
st.markdown(st.session_state.cover_letter) |
|
|
|
|
|
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() |