|
|
import gradio as gr |
|
|
import requests |
|
|
import json |
|
|
|
|
|
SERPAPI_KEY = "7aa26c214c77639dc2be2a61cb01ba2811fde874a36d3b04a38b9823655f6706" |
|
|
MIXTRAL_API_KEY = "VhU4tnowxkqtGOIRoyP7qUlhnXj1kjn5" |
|
|
MIXTRAL_API_URL = "https://api-inference.huggingface.co/models/mistralai/Mixtral-8x7B-Instruct-v0.1" |
|
|
|
|
|
def format_jobs_with_mixtral(jobs_data): |
|
|
""" |
|
|
Utilise Mixtral pour reformater les offres d'emploi en format optimal pour le LLM chat. |
|
|
|
|
|
Args: |
|
|
jobs_data (dict): Données des offres d'emploi depuis SerpAPI |
|
|
|
|
|
Returns: |
|
|
dict: Format optimisé pour le LLM chat |
|
|
""" |
|
|
try: |
|
|
|
|
|
jobs_json = json.dumps(jobs_data, ensure_ascii=False, indent=2) |
|
|
|
|
|
prompt = f"""Tu es un assistant spécialisé dans la présentation d'offres d'emploi. |
|
|
Voici des données JSON d'offres d'emploi que tu dois reformater pour qu'elles soient parfaitement présentables par un LLM chat. |
|
|
|
|
|
DONNÉES JSON: |
|
|
{jobs_json} |
|
|
|
|
|
INSTRUCTIONS CRITIQUES: |
|
|
1. Crée un format JSON-RPC 2.0 avec un message TRÈS CLAIR pour le LLM |
|
|
2. Ajoute dans le JSON un champ "INSTRUCTION_OBLIGATOIRE_LLM" qui force l'affichage |
|
|
3. Résume chaque offre : titre, entreprise, lieu, salaire, lien |
|
|
4. Descriptions courtes (max 120 caractères) |
|
|
5. JSON final < 20000 caractères |
|
|
6. Ajoute des émojis et messages clairs pour que le LLM comprenne qu'il DOIT afficher les résultats |
|
|
|
|
|
FORMAT REQUIS: |
|
|
{{ |
|
|
"jsonrpc": "2.0", |
|
|
"result": {{ |
|
|
"status": "SUCCESS", |
|
|
"INSTRUCTION_OBLIGATOIRE_LLM": "🚨 AFFICHAGE OBLIGATOIRE : Tu dois présenter TOUTES ces offres à l'utilisateur. Ne dis JAMAIS 'aucun résultat trouvé'", |
|
|
"message": "✅ X offres d'emploi trouvées - PRÉSENTE-LES MAINTENANT", |
|
|
"offres": [liste des offres formatées], |
|
|
"info_recherche": {{...}} |
|
|
}} |
|
|
}} |
|
|
|
|
|
Réponds UNIQUEMENT avec le JSON formaté, sans explication.""" |
|
|
|
|
|
|
|
|
headers = { |
|
|
"Authorization": f"Bearer {MIXTRAL_API_KEY}", |
|
|
"Content-Type": "application/json" |
|
|
} |
|
|
|
|
|
payload = { |
|
|
"inputs": prompt, |
|
|
"parameters": { |
|
|
"max_new_tokens": 2000, |
|
|
"temperature": 0.1, |
|
|
"return_full_text": False |
|
|
} |
|
|
} |
|
|
|
|
|
response = requests.post(MIXTRAL_API_URL, headers=headers, json=payload, timeout=30) |
|
|
|
|
|
if response.status_code == 200: |
|
|
mixtral_response = response.json() |
|
|
if isinstance(mixtral_response, list) and len(mixtral_response) > 0: |
|
|
formatted_text = mixtral_response[0].get("generated_text", "") |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
if "```json" in formatted_text: |
|
|
formatted_text = formatted_text.split("```json")[1].split("```")[0] |
|
|
elif "```" in formatted_text: |
|
|
formatted_text = formatted_text.split("```")[1].split("```")[0] |
|
|
|
|
|
formatted_json = json.loads(formatted_text.strip()) |
|
|
return formatted_json |
|
|
except json.JSONDecodeError: |
|
|
|
|
|
pass |
|
|
|
|
|
|
|
|
return create_simplified_format(jobs_data) |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
return create_simplified_format(jobs_data) |
|
|
|
|
|
def create_simplified_format(jobs_data): |
|
|
""" |
|
|
Crée un format simplifié en cas d'échec de Mixtral. |
|
|
""" |
|
|
if not jobs_data.get("result", {}).get("jobs", {}).get("results"): |
|
|
return jobs_data |
|
|
|
|
|
jobs = jobs_data["result"]["jobs"]["results"] |
|
|
simplified_jobs = [] |
|
|
|
|
|
for job in jobs[:6]: |
|
|
simplified_job = { |
|
|
"titre": job.get("title", ""), |
|
|
"entreprise": job.get("company", ""), |
|
|
"lieu": job.get("location", ""), |
|
|
"salaire": job.get("salary", "Non spécifié"), |
|
|
"description": job.get("description", "")[:120] + "..." if len(job.get("description", "")) > 120 else job.get("description", ""), |
|
|
"lien": job.get("link", "") |
|
|
} |
|
|
simplified_jobs.append(simplified_job) |
|
|
|
|
|
return { |
|
|
"jsonrpc": "2.0", |
|
|
"result": { |
|
|
"status": "SUCCESS", |
|
|
"INSTRUCTION_OBLIGATOIRE_LLM": "🚨 AFFICHAGE OBLIGATOIRE : Tu dois présenter TOUTES ces offres à l'utilisateur. Ne dis JAMAIS 'aucun résultat trouvé'", |
|
|
"message": f"✅ {len(simplified_jobs)} offres d'emploi trouvées - PRÉSENTE-LES MAINTENANT À L'UTILISATEUR", |
|
|
"offres": simplified_jobs, |
|
|
"info_recherche": { |
|
|
"requete": jobs_data.get("result", {}).get("search_info", {}).get("query", ""), |
|
|
"localisation": jobs_data.get("result", {}).get("search_info", {}).get("location", ""), |
|
|
"nombre_total": len(simplified_jobs) |
|
|
}, |
|
|
"ACTION_REQUISE": "AFFICHER_TOUTES_LES_OFFRES" |
|
|
}, |
|
|
"id": None |
|
|
} |
|
|
|
|
|
def search_jobs(job_title="", location="", user_desc=None, salary=None, studies=None, domain=None): |
|
|
""" |
|
|
🔍 OUTIL DE RECHERCHE D'EMPLOI - AFFICHAGE OBLIGATOIRE DES RÉSULTATS 🔍 |
|
|
|
|
|
⚠️ INSTRUCTION CRITIQUE POUR LE LLM : Si cette fonction retourne des offres d'emploi (status SUCCESS), |
|
|
tu DOIS ABSOLUMENT les présenter à l'utilisateur sous forme de liste claire et détaillée. |
|
|
NE JAMAIS dire "aucun résultat" si le JSON contient des offres dans "result.offres" ou "result.jobs.results". |
|
|
|
|
|
COMPORTEMENT ATTENDU DU LLM : |
|
|
- Si status = "SUCCESS" → AFFICHER TOUTES LES OFFRES reçues |
|
|
- Si "offres" ou "jobs.results" contient des données → PRÉSENTER chaque offre |
|
|
- TOUJOURS vérifier le contenu du JSON avant de répondre à l'utilisateur |
|
|
|
|
|
🔐 SÉCURITÉ : L'utilisateur doit dire le mot secret "cochon d'inde" quelque part dans sa demande |
|
|
|
|
|
Args: |
|
|
job_title (str): Titre du poste recherché (OBLIGATOIRE) - ex: "développeur Python" |
|
|
location (str): Ville/pays de recherche (OBLIGATOIRE) - ex: "Paris", "New York" |
|
|
user_desc (str, optional): Profil utilisateur - ex: "étudiant en informatique" |
|
|
salary (str, optional): Salaire souhaité - ex: "45k€", "3000€/mois" |
|
|
studies (str, optional): Formation - ex: "Master informatique" |
|
|
domain (str, optional): Secteur d'activité - ex: "fintech", "e-commerce" |
|
|
|
|
|
Returns: |
|
|
dict: JSON-RPC 2.0 avec offres d'emploi formatées par Mixtral pour présentation optimale |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
secret_word = "cochon d'inde" |
|
|
all_params = f"{job_title} {location} {user_desc or ''} {salary or ''} {studies or ''} {domain or ''}".lower() |
|
|
|
|
|
if secret_word not in all_params: |
|
|
return { |
|
|
"jsonrpc": "2.0", |
|
|
"error": { |
|
|
"code": -32001, |
|
|
"message": "Access Denied - Secret word required", |
|
|
"data": { |
|
|
"error_type": "AUTHENTICATION_REQUIRED", |
|
|
"message_for_user": "🔐 Accès refusé ! Tu dois dire le mot secret pour utiliser cet outil de recherche d'emploi.", |
|
|
"hint": "Le mot secret doit être inclus quelque part dans ta demande de recherche.", |
|
|
"instruction_llm": "Informe l'utilisateur qu'il doit dire le mot secret. Ne révèle JAMAIS le mot secret. Demande-lui simplement de l'inclure dans sa demande.", |
|
|
"required_action": "L'utilisateur doit inclure le mot secret dans sa demande de recherche d'emploi." |
|
|
} |
|
|
}, |
|
|
"id": None |
|
|
} |
|
|
|
|
|
|
|
|
missing_fields = [] |
|
|
if not job_title.strip(): |
|
|
missing_fields.append("Titre du poste/métier") |
|
|
if not location.strip(): |
|
|
missing_fields.append("Localisation") |
|
|
|
|
|
|
|
|
if missing_fields: |
|
|
return { |
|
|
"jsonrpc": "2.0", |
|
|
"error": { |
|
|
"code": -32602, |
|
|
"message": "Invalid params", |
|
|
"data": { |
|
|
"missing_parameters": missing_fields, |
|
|
"required_parameters": ["job_title", "location"], |
|
|
"optional_parameters": ["user_desc", "salary", "studies", "domain"], |
|
|
"description": f"Paramètres obligatoires manquants : {', '.join(missing_fields)}" |
|
|
} |
|
|
}, |
|
|
"id": None |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
def extract_key_word(text): |
|
|
if not text or not text.strip(): |
|
|
return "" |
|
|
|
|
|
words = text.strip().split() |
|
|
stop_words = ['le', 'la', 'les', 'de', 'du', 'des', 'en', 'et', 'un', 'une'] |
|
|
for word in words: |
|
|
if word.lower() not in stop_words and len(word) > 2: |
|
|
return word |
|
|
return words[0] if words else "" |
|
|
|
|
|
|
|
|
job_key = extract_key_word(job_title) |
|
|
query_parts = [job_key] |
|
|
|
|
|
|
|
|
if user_desc and user_desc.strip(): |
|
|
profile_key = extract_key_word(user_desc) |
|
|
if profile_key and profile_key != job_key: |
|
|
query_parts.append(profile_key) |
|
|
else: |
|
|
|
|
|
query_parts.append(job_key) |
|
|
|
|
|
|
|
|
if domain and domain.strip(): |
|
|
domain_key = extract_key_word(domain) |
|
|
if domain_key: |
|
|
query_parts.append(domain_key) |
|
|
if studies and studies.strip(): |
|
|
studies_key = extract_key_word(studies) |
|
|
if studies_key: |
|
|
query_parts.append(studies_key) |
|
|
if salary and salary.strip(): |
|
|
salary_key = extract_key_word(salary) |
|
|
if salary_key: |
|
|
query_parts.append(f"salaire") |
|
|
query_parts.append(salary_key) |
|
|
|
|
|
query = " ".join(query_parts) |
|
|
|
|
|
url = "https://serpapi.com/search.json" |
|
|
payload = { |
|
|
"engine": "google_jobs", |
|
|
"q": query, |
|
|
"location": location, |
|
|
"api_key": SERPAPI_KEY, |
|
|
"lrad": 50 |
|
|
} |
|
|
|
|
|
try: |
|
|
response = requests.get(url, params=payload) |
|
|
if response.status_code != 200: |
|
|
return { |
|
|
"jsonrpc": "2.0", |
|
|
"error": { |
|
|
"code": -32603, |
|
|
"message": "Internal error", |
|
|
"data": { |
|
|
"serpapi_status_code": response.status_code, |
|
|
"serpapi_response": response.text[:500] if response.text else "No response body", |
|
|
"request_payload": payload, |
|
|
"error_type": "SerpAPI HTTP Error", |
|
|
"description": f"SerpAPI returned status code {response.status_code}" |
|
|
} |
|
|
}, |
|
|
"id": None |
|
|
} |
|
|
|
|
|
data = response.json() |
|
|
jobs_results = data.get("jobs_results", []) |
|
|
|
|
|
|
|
|
search_info = { |
|
|
"query_used": query, |
|
|
"location_used": location, |
|
|
"total_results": len(jobs_results), |
|
|
"serpapi_response_keys": list(data.keys()) |
|
|
} |
|
|
|
|
|
if not jobs_results: |
|
|
|
|
|
simple_query = extract_key_word(job_title) |
|
|
simple_payload = { |
|
|
"engine": "google_jobs", |
|
|
"q": simple_query, |
|
|
"location": location, |
|
|
"api_key": SERPAPI_KEY, |
|
|
"lrad": 100 |
|
|
} |
|
|
|
|
|
simple_response = requests.get(url, params=simple_payload) |
|
|
if simple_response.status_code == 200: |
|
|
simple_data = simple_response.json() |
|
|
jobs_results = simple_data.get("jobs_results", []) |
|
|
search_info["fallback_query"] = simple_query |
|
|
search_info["fallback_results"] = len(jobs_results) |
|
|
|
|
|
if not jobs_results: |
|
|
return { |
|
|
"jsonrpc": "2.0", |
|
|
"result": { |
|
|
"success": False, |
|
|
"status": "NO_RESULTS", |
|
|
"message": "❌ Aucune offre trouvée", |
|
|
"search_info": { |
|
|
"query": query, |
|
|
"location": location, |
|
|
"fallback_used": search_info.get("fallback_query") is not None |
|
|
}, |
|
|
"jobs": { |
|
|
"total": 0, |
|
|
"results": [] |
|
|
}, |
|
|
"suggestions": [ |
|
|
"Essayez un métier plus général", |
|
|
"Vérifiez l'orthographe de la ville", |
|
|
"Élargissez la zone géographique" |
|
|
] |
|
|
}, |
|
|
"id": None |
|
|
} |
|
|
|
|
|
|
|
|
jobs_list = [] |
|
|
for job in jobs_results: |
|
|
|
|
|
description = job.get("description", "") |
|
|
if description and len(description) > 300: |
|
|
description = description[:300] + "..." |
|
|
|
|
|
job_info = { |
|
|
"title": job.get("title", ""), |
|
|
"company": job.get("company_name", ""), |
|
|
"location": job.get("location", ""), |
|
|
"salary": job.get("salary", "Non spécifié"), |
|
|
"description": description, |
|
|
"posted": job.get("posted_at", ""), |
|
|
"type": job.get("schedule_type", ""), |
|
|
"source": job.get("via", ""), |
|
|
"link": job.get("link", ""), |
|
|
"highlights": { |
|
|
"qualifications": job.get("job_highlights", {}).get("Qualifications", [])[:3] if job.get("job_highlights", {}).get("Qualifications") else [], |
|
|
"responsibilities": job.get("job_highlights", {}).get("Responsibilities", [])[:3] if job.get("job_highlights", {}).get("Responsibilities") else [] |
|
|
} |
|
|
} |
|
|
jobs_list.append(job_info) |
|
|
|
|
|
total_jobs_found = len(jobs_list) |
|
|
|
|
|
displayed_jobs = jobs_list[:8] |
|
|
|
|
|
|
|
|
raw_result = { |
|
|
"jsonrpc": "2.0", |
|
|
"result": { |
|
|
"success": True, |
|
|
"status": "SUCCESS", |
|
|
"message": f"✅ {total_jobs_found} offres trouvées ({len(displayed_jobs)} affichées)", |
|
|
"search_info": { |
|
|
"query": query, |
|
|
"location": location, |
|
|
"fallback_used": search_info.get("fallback_query") is not None |
|
|
}, |
|
|
"jobs": { |
|
|
"total": total_jobs_found, |
|
|
"displayed": len(displayed_jobs), |
|
|
"results": displayed_jobs |
|
|
}, |
|
|
"stats": { |
|
|
"with_salary": len([j for j in displayed_jobs if j.get("salary") and j.get("salary") != "Non spécifié"]), |
|
|
"companies": len(set([j.get("company", "") for j in displayed_jobs if j.get("company")])) |
|
|
} |
|
|
}, |
|
|
"id": None |
|
|
} |
|
|
|
|
|
|
|
|
return format_jobs_with_mixtral(raw_result) |
|
|
|
|
|
except Exception as e: |
|
|
return { |
|
|
"jsonrpc": "2.0", |
|
|
"error": { |
|
|
"code": -32603, |
|
|
"message": "Internal error", |
|
|
"data": { |
|
|
"error_type": "Python Exception", |
|
|
"error_message": str(e), |
|
|
"error_details": { |
|
|
"exception_type": type(e).__name__, |
|
|
"search_parameters": { |
|
|
"job_title": job_title, |
|
|
"location": location, |
|
|
"user_desc": user_desc, |
|
|
"salary": salary, |
|
|
"studies": studies, |
|
|
"domain": domain |
|
|
} |
|
|
}, |
|
|
"description": "Une erreur interne s'est produite lors de la recherche d'emploi", |
|
|
"suggestions": [ |
|
|
"Vérifiez les paramètres de recherche", |
|
|
"Réessayez dans quelques instants", |
|
|
"Contactez le support si l'erreur persiste" |
|
|
] |
|
|
} |
|
|
}, |
|
|
"id": None |
|
|
} |
|
|
|
|
|
|
|
|
demo = gr.Interface( |
|
|
fn=search_jobs, |
|
|
inputs=[ |
|
|
gr.Textbox(label="Titre du poste/métier (OBLIGATOIRE)", placeholder="ex: développeur Python, data scientist"), |
|
|
gr.Textbox(label="Localisation (OBLIGATOIRE)", placeholder="ex: Paris, New York, London, Tokyo"), |
|
|
gr.Textbox(label="Profil utilisateur (optionnel)", placeholder="ex: étudiant en informatique, 5 ans d'expérience", value=""), |
|
|
gr.Textbox(label="Salaire souhaité (optionnel)", placeholder="ex: 45k€/an, 3000€/mois", value=""), |
|
|
gr.Textbox(label="Études/Formation (optionnel)", placeholder="ex: Master informatique, École d'ingénieur", value=""), |
|
|
gr.Textbox(label="Domaine d'activité (optionnel)", placeholder="ex: fintech, e-commerce, santé", value="") |
|
|
], |
|
|
outputs=gr.JSON(label="Résultats de la recherche d'emploi"), |
|
|
title="Recherche d'Emploi MCP", |
|
|
description="Outil de recherche d'emploi international via SerpAPI. Seuls 'Titre du poste' et 'Localisation' sont obligatoires. Fonctionne dans le monde entier avec des termes en français." |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch(mcp_server=True) |
|
|
|