TestSpaces / generate_resume.py
Nayohn's picture
Add application file
5747165
from __future__ import annotations
import os
import json
import subprocess
import shutil
import platform
import sys
from pathlib import Path
from typing import Any, Dict
import uuid
from jinja2 import Environment, FileSystemLoader
from pydantic import ValidationError
from pydantic_model import Resume
# Import conditionnel de gradio pour éviter les erreurs si non installé
try:
import gradio as gr
except ImportError:
gr = None
PROJECT_ROOT = Path(__file__).parent
TEMPLATES_DIR = PROJECT_ROOT / "templates"
# Par défaut, toujours utiliser le template classic
TEMPLATE_NAME = "classic.tex.j2"
OUTPUT_DIR = PROJECT_ROOT / "build"
# Activer explicitement le serveur MCP via variable d'environnement (équivalent à mcp_server=True)
os.environ["GRADIO_MCP_SERVER"] = "True"
def escape_latex_special_chars(text: str) -> str:
"""Escape special LaTeX characters in text."""
if not isinstance(text, str):
return text
# Il faut traiter \ en premier pour éviter d'échapper les échappements
# Puis traiter les autres caractères spéciaux
text = text.replace('\\', r'\textbackslash{}')
latex_special_chars = {
'&': r'\&',
'%': r'\%',
'$': r'\$',
'#': r'\#',
'^': r'\textasciicircum{}',
'_': r'\_',
'{': r'\{',
'}': r'\}',
'~': r'\textasciitilde{}',
}
for char, escape in latex_special_chars.items():
text = text.replace(char, escape)
return text
def escape_resume_data(data: Dict[str, Any]) -> Dict[str, Any]:
"""Recursively escape LaTeX special characters in resume data."""
if isinstance(data, dict):
return {k: escape_resume_data(v) for k, v in data.items()}
elif isinstance(data, list):
return [escape_resume_data(item) for item in data]
elif isinstance(data, str):
return escape_latex_special_chars(data)
else:
return data
def render_tex(resume_data: Dict[str, Any], output_tex_path: Path, template_name: str = TEMPLATE_NAME) -> None:
try:
resume = Resume.model_validate(resume_data)
except ValidationError as e:
# En mode serveur (Gradio/MCP), il ne faut pas quitter le process
# Propager une erreur explicite pour que l'UI / MCP l'affiche correctement
raise ValueError(f"Validation Pydantic échouée: {e}") from e
env = Environment(
loader=FileSystemLoader(str(TEMPLATES_DIR)),
autoescape=False,
trim_blocks=True,
lstrip_blocks=True,
)
# Configure custom delimiters to avoid clashes with LaTeX
env.block_start_string = "<<%"
env.block_end_string = "%>>"
env.variable_start_string = "<<"
env.variable_end_string = ">>"
env.comment_start_string = "<#!"
env.comment_end_string = "!#>"
template = env.get_template(template_name)
# Échapper les caractères spéciaux LaTeX dans les données
escaped_data = escape_resume_data(resume.model_dump())
rendered = template.render(resume=escaped_data)
output_tex_path.parent.mkdir(parents=True, exist_ok=True)
output_tex_path.write_text(rendered, encoding="utf-8")
def run_tectonic(tex_path: Path, outdir: Path) -> None:
cmd = [
"tectonic",
"--outdir",
str(outdir),
str(tex_path),
]
subprocess.run(cmd, check=True)
def run_latexmk(tex_path: Path, outdir: Path, *, xelatex: bool = False) -> None:
# latexmk -pdf -synctex=1 -interaction=nonstopmode -output-directory=outdir tex
cmd = ["latexmk"]
if xelatex:
cmd.append("-xelatex")
else:
cmd.append("-pdf")
cmd += [
"-halt-on-error",
"-file-line-error",
"-synctex=1",
"-interaction=nonstopmode",
f"-output-directory={outdir}",
str(tex_path),
]
subprocess.run(cmd, check=True)
def compile_pdf(output_tex_path: Path, output_pdf_path: Path, engine_preference: str = "tectonic") -> None:
outdir = output_pdf_path.parent
outdir.mkdir(parents=True, exist_ok=True)
def tool_available(cmd: str) -> bool:
return shutil.which(cmd) is not None
def read_latex_log_tail() -> str:
try:
base = output_tex_path.stem
log_path = outdir / f"{base}.log"
if log_path.exists():
content = log_path.read_text(errors="ignore")
tail = "\n".join(content.splitlines()[-120:])
return tail
# tenter aussi le .xdv log de xelatex
xdv_path = outdir / f"{base}.xdv"
if xdv_path.exists():
return "(fichier .xdv présent, pas de .log généré)"
except Exception:
pass
return "(pas de log disponible)"
if engine_preference == "latexmk":
try:
# Sur Linux (HF Spaces), privilégier XeLaTeX pour éviter les soucis de polices (lmodern)
use_xelatex = platform.system() == "Linux"
run_latexmk(output_tex_path, outdir, xelatex=use_xelatex)
except (FileNotFoundError, subprocess.CalledProcessError) as e:
if tool_available("tectonic"):
run_tectonic(output_tex_path, outdir)
else:
log_tail = read_latex_log_tail()
raise RuntimeError(
"Compilation LaTeX échouée avec latexmk et aucun fallback 'tectonic' disponible.\n"
f"Commande: {e}\n\nDernières lignes du .log:\n{log_tail}"
) from e
else:
# default: try tectonic first, then fallback if available
try:
run_tectonic(output_tex_path, outdir)
except (FileNotFoundError, subprocess.CalledProcessError) as e:
if tool_available("latexmk"):
try:
run_latexmk(output_tex_path, outdir)
except subprocess.CalledProcessError as e2:
log_tail = read_latex_log_tail()
raise RuntimeError(
"Compilation LaTeX échouée avec tectonic puis latexmk.\n"
f"Erreur latexmk: {e2}\n\nDernières lignes du .log:\n{log_tail}"
) from e2
else:
raise RuntimeError(
f"Compilation LaTeX échouée avec tectonic et aucun fallback 'latexmk' disponible. "
f"Erreur tectonic: {e}"
) from e
if not output_pdf_path.exists():
raise RuntimeError(f"PDF introuvable après compilation: {output_pdf_path}")
@gr.mcp.prompt()
def resume_generation_system_prompt() -> str:
"""
System prompt complet pour gérer tout le workflow de génération de CV professionnel.
Ce prompt système fournit des instructions détaillées à l'assistant IA pour gérer
efficacement tout le processus de création de CV, de la collecte d'informations
à la génération du PDF final.
Returns:
str: Instructions système complètes pour l'assistant IA
"""
return """Tu es un assistant spécialisé dans l'onboarding pour la génération de CV. Ton rôle est de guider l'utilisateur pour remplir toutes les informations nécessaires à la création de son CV, de manière claire, structurée et conviviale.
## TES RÈGLES FONDAMENTALES
1. **Pose une seule question à la fois** - Ne jamais submerger l'utilisateur
2. **Fournis un exemple concret** pour chaque question afin de guider l'utilisateur
3. **Valide chaque réponse** et reformule la question si la réponse est vide ou invalide
4. **Ne fais jamais de supposition** sur l'utilisateur - Toujours demander et confirmer
5. **Utilise un ton convivial, simple et naturel** - Pas de jargon technique
## DÉMARRAGE OBLIGATOIRE
Commence TOUJOURS par cette question :
"Bonjour ! 👋 Je vais t'aider à créer ton CV professionnel.
Pour commencer, dis-moi : est-ce que tu crées ton **premier CV** ou tu veux **mettre à jour un CV existant** ?
Si c'est une mise à jour, peux-tu uploader ton ancien CV (fichier PDF, Word, etc.) pour que je puisse m'en inspirer ?"
## INFORMATIONS À COLLECTER (DANS CET ORDRE)
### 1. INFORMATIONS DE BASE
- **pdf_title**
- Question : "Quel titre veux-tu donner à ton CV ?"
- Exemple : "CV - Marie Dupont" ou "Candidature Développeur - Pierre Martin"
- **name** (sera automatiquement utilisé comme pdf_author)
- Question : "Quel est ton nom complet ?"
- Exemple : "Marie Dupont" ou "Pierre Martin"
- **location**
- Question : "Où habites-tu actuellement ?"
- Exemple : "Paris, France" ou "Lyon, France"
- **email**
- Question : "Quelle est ton adresse email professionnelle ?"
- Exemple : "marie.dupont@gmail.com"
- **phone**
- Question : "Quel est ton numéro de téléphone (avec l'indicatif pays) ?"
- Exemple : "+33 6 12 34 56 78" ou "06 12 34 56 78"
### 2. LIENS PROFESSIONNELS (OPTIONNELS)
- **website_url** et **website_label**
- Question : "As-tu un site web personnel ou portfolio ? Si oui, donne-moi l'URL complète."
- Exemple : "https://marie-dupont.fr" → label sera "marie-dupont.fr"
- **linkedin_url** et **linkedin_handle**
- Question : "As-tu un profil LinkedIn ? Si oui, donne-moi le lien complet."
- Exemple : "https://linkedin.com/in/marie-dupont" → handle sera "marie-dupont"
- **github_url** et **github_handle**
- Question : "As-tu un profil GitHub ? Si oui, donne-moi le lien complet."
- Exemple : "https://github.com/marie-dupont" → handle sera "marie-dupont"
### 3. CONTENU DU CV
- **intro_paragraphs**
- Question : "Écris une ou deux phrases qui te décrivent professionnellement."
- Exemple : "Développeuse web avec 3 ans d'expérience, passionnée par les technologies modernes et l'innovation."
- **quick_guide_items**
- Question : "Liste 3 à 5 de tes principales compétences ou points forts."
- Exemple : "Développement web, Gestion d'équipe, React/Node.js, Problem solving"
- **education**
- Question : "Parle-moi de ta formation. Pour chaque diplôme, indique : le nom du diplôme, l'école/université, les dates (format AAAA-AAAA), et éventuellement des mentions ou points importants."
- Exemple : "Master en Informatique à l'École Polytechnique (2020-2022), spécialité IA, mention Bien"
- ⚠️ **FORMAT DATES OBLIGATOIRE** : Toujours utiliser des tirets pour séparer les années (ex: "2020-2022", "2015-2018")
- **experience**
- Question : "Décris ton expérience professionnelle. Pour chaque poste : nom de l'entreprise, ton poste, lieu, dates (format Mois AAAA-Mois AAAA), et tes principales réalisations avec des chiffres si possible."
- Exemple : "Développeur chez Google, Paris (Janvier 2022-présent) : Développé 3 nouvelles fonctionnalités, réduit les bugs de 40%"
- ⚠️ **FORMAT DATES OBLIGATOIRE** : Utiliser des tirets et des espaces (ex: "Janvier 2022-Décembre 2024", "Avril-Juin 2022")
- **publications** (si applicable)
- Question : "As-tu publié des articles, recherches ou communications ? Si oui, donne-moi les détails."
- Exemple : "Article sur l'IA publié en 2023 dans la revue TechReview"
- **projects**
- Question : "Quels sont tes projets personnels ou professionnels marquants ? Avec liens GitHub si possible."
- Exemple : "Application mobile de gestion de tâches (React Native) - 1000+ téléchargements"
- **languages**
- Question : "Quels langages de programmation ou langues maîtrises-tu ?"
- Exemple : "Python, JavaScript, TypeScript, Anglais, Espagnol"
- **technologies**
- Question : "Quelles technologies, frameworks ou outils utilises-tu ?"
- Exemple : "React, Node.js, Docker, AWS, PostgreSQL"
## FINALISATION
Une fois toutes les informations collectées :
1. **Présente un récapitulatif clair** de toutes les informations
2. **Demande confirmation** : "Ces informations sont-elles correctes ?"
3. **Si confirmé**, appelle la fonction `generate_resume_pdf` avec tous les paramètres
4. **Présente le résultat** : "Voici ton CV ! Tu peux le télécharger et me dire s'il faut ajuster quelque chose."
## GESTION DES RÉPONSES
- **Si réponse vide** : "Cette information est importante pour ton CV. Peux-tu me donner cette information ?"
- **Si réponse invalide** : "Je n'ai pas bien compris. Peux-tu reformuler ? Voici un exemple : [exemple]"
- **Si l'utilisateur veut passer** : "D'accord, on peut laisser ça vide pour l'instant. On pourra l'ajouter plus tard si tu veux."
## FORMATS JSON POUR LES PARAMÈTRES
Quand tu collectes des informations complexes, formate-les correctement :
- **Listes simples** : ["item1", "item2", "item3"]
- **Éducation** : [{"degree": "Master", "institution": "École", "date_range": "2020-2022", "field_of_study": "Informatique", "highlights": ["Mention Bien"]}]
- **Expérience** : [{"company": "Google", "role": "Développeur", "location": "Paris", "date_range": "Janvier 2022-présent", "highlights": ["Réalisation 1", "Impact 2"]}]
⚠️ **RÈGLES CRITIQUES POUR LES DATES** :
- **TOUJOURS** utiliser des tirets pour séparer les périodes : "2020-2022", "Avril-Juin 2022"
- **JAMAIS** coller les dates : ❌ "20202022" ✅ "2020-2022"
- Pour l'éducation : Format "AAAA-AAAA" (ex: "2015-2018")
- Pour l'expérience : Format "Mois AAAA-Mois AAAA" (ex: "Janvier 2022-Décembre 2024")
Commence maintenant par la question de démarrage !"""
def validate_json_parameter(param_name: str, param_value: str) -> list:
"""
Validate and parse a JSON parameter.
Args:
param_name (str): Name of the parameter for error reporting
param_value (str): JSON string to validate
Returns:
list: Parsed JSON data
Raises:
ValueError: If JSON is invalid
"""
try:
parsed = json.loads(param_value)
if not isinstance(parsed, list):
raise ValueError(f"Parameter '{param_name}' must be a JSON array")
return parsed
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in parameter '{param_name}': {str(e)}")
def generate_resume_pdf(
pdf_title: str,
pdf_author: str,
name: str,
location: str,
email: str,
phone: str,
last_updated_text: str = "",
website_url: str = "",
website_label: str = "",
linkedin_url: str = "",
linkedin_handle: str = "",
github_url: str = "",
github_handle: str = "",
intro_paragraphs: str = "[]",
quick_guide_items: str = "[]",
education: str = "[]",
experience: str = "[]",
publications: str = "[]",
projects: str = "[]",
languages: str = "[]",
technologies: str = "[]"
) -> str:
"""
Generate a professional PDF resume from structured data.
Creates a LaTeX-based PDF resume. All JSON parameters must be valid JSON strings.
Args:
pdf_title (str): PDF document title (e.g., "John Doe Resume")
pdf_author (str): Author name for PDF metadata (e.g., "John Doe")
name (str): Full name (e.g., "John Doe")
location (str): Current location (e.g., "Paris, France")
email (str): Email address (e.g., "john.doe@example.com")
phone (str): Phone with country code (e.g., "+33 1 23 45 67 89")
last_updated_text (str): Optional update text (e.g., "Updated September 2024") or ""
website_url (str): Website URL (e.g., "https://johndoe.dev") or ""
website_label (str): Website display text (e.g., "johndoe.dev") or ""
linkedin_url (str): LinkedIn URL (e.g., "https://linkedin.com/in/johndoe") or ""
linkedin_handle (str): LinkedIn username (e.g., "johndoe") or ""
github_url (str): GitHub URL (e.g., "https://github.com/johndoe") or ""
github_handle (str): GitHub username (e.g., "johndoe") or ""
intro_paragraphs (str): JSON array of strings. Example: ["I am a software engineer with 5+ years experience.", "Passionate about AI and machine learning."]
quick_guide_items (str): JSON array of key skills. Example: ["Python Expert", "Team Leadership", "Agile Methodologies"]
education (str): JSON array of objects. Example: [{"degree": "Master of Science", "institution": "MIT", "date_range": "2018-2020", "field_of_study": "Computer Science", "highlights": ["GPA: 3.9/4.0"]}]. IMPORTANT: date_range must use format "YYYY-YYYY" (e.g., "2015-2018", NOT "20152018")
experience (str): JSON array of objects. Example: [{"company": "Google", "role": "Senior Engineer", "location": "Mountain View, CA", "date_range": "January 2020-Present", "highlights": ["Led team of 5 engineers", "Increased system performance by 40%"]}]. IMPORTANT: date_range must use format "Month YYYY-Month YYYY" with proper separators
publications (str): JSON array of objects. Example: [{"title": "AI in Production", "authors": ["John Doe", "Jane Smith"], "date": "2023", "doi": "10.1000/xyz123"}] or "[]"
projects (str): JSON array of objects. Example: [{"title": "E-commerce Platform", "repo_url": "https://github.com/johndoe/ecommerce", "repo_label": "github.com/johndoe/ecommerce", "highlights": ["Built with React and Node.js", "Handles 10k+ users"]}]
languages (str): JSON array of programming languages. Example: ["Python", "JavaScript", "TypeScript", "Go"]
technologies (str): JSON array of tools/frameworks. Example: ["React", "Node.js", "Docker", "AWS", "PostgreSQL"]
Returns:
str: File path to the generated PDF resume
"""
try:
# Valider et parser tous les paramètres JSON
intro_paragraphs_list = validate_json_parameter("intro_paragraphs", intro_paragraphs)
quick_guide_items_list = validate_json_parameter("quick_guide_items", quick_guide_items)
education_list = validate_json_parameter("education", education)
experience_list = validate_json_parameter("experience", experience)
publications_list = validate_json_parameter("publications", publications)
projects_list = validate_json_parameter("projects", projects)
languages_list = validate_json_parameter("languages", languages)
technologies_list = validate_json_parameter("technologies", technologies)
# Construire le dictionnaire Resume à partir des paramètres
resume_data = {
"meta": {
"pdf_title": pdf_title,
"pdf_author": pdf_author,
"last_updated_text": last_updated_text if last_updated_text else None
},
"header": {
"name": name,
"location": location,
"email": email,
"phone": phone,
"website_url": website_url if website_url else None,
"website_label": website_label if website_label else None,
"linkedin_url": linkedin_url if linkedin_url else None,
"linkedin_handle": linkedin_handle if linkedin_handle else None,
"github_url": github_url if github_url else None,
"github_handle": github_handle if github_handle else None
},
"intro_paragraphs": intro_paragraphs_list,
"quick_guide_items": quick_guide_items_list,
"education": education_list,
"experience": experience_list,
"publications": publications_list,
"projects": projects_list,
"technologies_section": {
"languages": languages_list,
"technologies": technologies_list
}
}
# Valider avec le modèle Pydantic
resume = Resume.model_validate(resume_data)
# Générer le PDF
base_name = uuid.uuid4().hex
out_dir = OUTPUT_DIR
out_dir.mkdir(parents=True, exist_ok=True)
out_tex = out_dir / f"{base_name}.tex"
out_pdf = out_dir / f"{base_name}.pdf"
render_tex(resume.model_dump(), out_tex, template_name=TEMPLATE_NAME)
preferred_engine = "latexmk" if platform.system() == "Linux" else "tectonic"
compile_pdf(out_tex, out_pdf, engine_preference=preferred_engine)
# Retourner DIRECTEMENT le chemin pour que Gradio génère l'URL publique
return str(out_pdf)
except (ValueError, ValidationError) as e:
# Pour les erreurs, lever une exception que Gradio peut gérer
raise ValueError(str(e))
except Exception as err: # noqa: BLE001
# Pour toutes les autres erreurs
raise ValueError(f"Unexpected error: {str(err)}")
def launch_gradio(server_host: str = "127.0.0.1", server_port: int = 7860) -> None:
"""Lance une application Gradio (UI + MCP Server).
L'UI propose: zone de texte JSON (schéma `Resume`) → bouton → PDF.\
Le serveur MCP est activé via `mcp_server=True` conformément à la doc
officielle Gradio [Building an MCP Server with Gradio](https://www.gradio.app/guides/building-mcp-server-with-gradio).
"""
if gr is None:
print("Gradio n'est pas installé. Ajoutez 'gradio' à pyproject.toml.")
raise ImportError("Gradio n'est pas disponible")
sample_path = PROJECT_ROOT / "example_inputs" / "sample.json"
sample_value = "{}"
if sample_path.exists():
try:
sample_value = sample_path.read_text(encoding="utf-8")
except Exception: # noqa: BLE001
pass
with gr.Blocks(title="Générateur de CV PDF (LaTeX)") as demo:
gr.Markdown("## Générateur de CV PDF avec champs structurés\nTemplate: `classic.tex.j2`.")
with gr.Accordion("Mode d'emploi", open=True):
gr.Markdown(
"- Remplissez les champs ci-dessous pour créer votre CV.\n"
"- Les champs marqués * sont obligatoires.\n"
"- Les listes (éducation, expérience, etc.) doivent être au format JSON.\n"
"- Cliquez sur 'Générer le PDF' pour créer votre CV.\n"
)
with gr.Tab("Informations de base"):
with gr.Row():
with gr.Column():
ui_pdf_title = gr.Textbox(label="Titre du PDF *", value="Mon CV")
ui_pdf_author = gr.Textbox(label="Auteur du PDF *", value="")
ui_last_updated_text = gr.Textbox(label="Dernière mise à jour", value="", placeholder="ex: Septembre 2024")
with gr.Column():
ui_name = gr.Textbox(label="Nom complet *", value="")
ui_location = gr.Textbox(label="Localisation *", value="", placeholder="ex: Paris, France")
ui_email = gr.Textbox(label="Email *", value="", placeholder="ex: nom@example.com")
ui_phone = gr.Textbox(label="Téléphone *", value="", placeholder="ex: +33 1 23 45 67 89")
with gr.Tab("Liens et réseaux sociaux"):
with gr.Row():
with gr.Column():
ui_website_url = gr.Textbox(label="URL du site web", value="", placeholder="https://monsite.com")
ui_website_label = gr.Textbox(label="Label du site web", value="", placeholder="monsite.com")
ui_linkedin_url = gr.Textbox(label="URL LinkedIn", value="", placeholder="https://linkedin.com/in/...")
ui_linkedin_handle = gr.Textbox(label="Handle LinkedIn", value="", placeholder="monprofil")
with gr.Column():
ui_github_url = gr.Textbox(label="URL GitHub", value="", placeholder="https://github.com/...")
ui_github_handle = gr.Textbox(label="Handle GitHub", value="", placeholder="monprofil")
with gr.Tab("Contenu du CV"):
ui_intro_paragraphs = gr.Textbox(
label="Paragraphes d'introduction (JSON)",
value='["Ingénieur passionné avec 5 ans d\'expérience"]',
lines=3,
placeholder='["Paragraphe 1", "Paragraphe 2"]'
)
ui_quick_guide_items = gr.Textbox(
label="Points clés (JSON)",
value='["Expert en Python", "Gestion d\'équipe"]',
lines=3,
placeholder='["Point 1", "Point 2"]'
)
with gr.Row():
with gr.Column():
ui_education = gr.Textbox(
label="Éducation (JSON)",
value='[]',
lines=5,
placeholder='[{"degree": "Master", "institution": "Université", "date_range": "2020-2022", "field_of_study": "Informatique", "highlights": []}]'
)
ui_experience = gr.Textbox(
label="Expérience (JSON)",
value='[]',
lines=5,
placeholder='[{"company": "Entreprise", "role": "Développeur", "location": "Paris", "date_range": "2022-2024", "highlights": ["Réalisation X", "Amélioration Y"]}]'
)
with gr.Column():
ui_publications = gr.Textbox(
label="Publications (JSON)",
value='[]',
lines=3,
placeholder='[{"date": "2024", "title": "Article", "authors": ["Moi"], "doi_url": null, "doi_label": null}]'
)
ui_projects = gr.Textbox(
label="Projets (JSON)",
value='[]',
lines=3,
placeholder='[{"title": "Projet", "repo_url": "https://github.com/...", "repo_label": "github.com/...", "highlights": []}]'
)
with gr.Tab("Compétences"):
with gr.Row():
ui_languages = gr.Textbox(
label="Langages de programmation (JSON)",
value='["Python", "JavaScript", "Java"]',
lines=2,
placeholder='["Python", "JavaScript", "Java"]'
)
ui_technologies = gr.Textbox(
label="Technologies (JSON)",
value='["React", "Docker", "AWS"]',
lines=2,
placeholder='["React", "Docker", "AWS"]'
)
with gr.Row():
generate_btn = gr.Button("Générer le PDF", variant="primary", size="lg")
pdf_file = gr.File(label="PDF généré", file_count="single")
def _on_click_structured(
pdf_title, pdf_author, name, location, email, phone,
last_updated_text, website_url, website_label,
linkedin_url, linkedin_handle, github_url, github_handle,
intro_paragraphs, quick_guide_items, education, experience,
publications, projects, languages, technologies
) -> str:
"""Génère le PDF avec les champs structurés."""
try:
return generate_resume_pdf(
pdf_title, pdf_author, name, location, email, phone,
last_updated_text, website_url, website_label,
linkedin_url, linkedin_handle, github_url, github_handle,
intro_paragraphs, quick_guide_items, education, experience,
publications, projects, languages, technologies
)
except Exception as err: # noqa: BLE001
raise gr.Error(str(err))
# Connecter l'interface utilisateur
generate_btn.click(
_on_click_structured,
inputs=[
ui_pdf_title, ui_pdf_author, ui_name, ui_location, ui_email, ui_phone,
ui_last_updated_text, ui_website_url, ui_website_label,
ui_linkedin_url, ui_linkedin_handle, ui_github_url, ui_github_handle,
ui_intro_paragraphs, ui_quick_guide_items, ui_education, ui_experience,
ui_publications, ui_projects, ui_languages, ui_technologies
],
outputs=[pdf_file],
api_name=False
)
# Section for API endpoints (hidden from UI but accessible for MCP)
with gr.Tab("API Endpoints", visible=False):
# API: generate_resume_pdf - Main resume generation tool
with gr.Row():
with gr.Column():
# Required fields
api_pdf_title = gr.Textbox(label="pdf_title", value="Professional Resume")
api_pdf_author = gr.Textbox(label="pdf_author", value="John Doe")
api_name = gr.Textbox(label="name", value="John Doe")
api_location = gr.Textbox(label="location", value="Paris, France")
api_email = gr.Textbox(label="email", value="john.doe@example.com")
api_phone = gr.Textbox(label="phone", value="+33 1 23 45 67 89")
# Optional fields
api_last_updated_text = gr.Textbox(label="last_updated_text", value="")
api_website_url = gr.Textbox(label="website_url", value="")
api_website_label = gr.Textbox(label="website_label", value="")
api_linkedin_url = gr.Textbox(label="linkedin_url", value="")
api_linkedin_handle = gr.Textbox(label="linkedin_handle", value="")
api_github_url = gr.Textbox(label="github_url", value="")
api_github_handle = gr.Textbox(label="github_handle", value="")
with gr.Column():
# JSON array fields with comprehensive examples for LLMs
api_intro_paragraphs = gr.Textbox(
label="intro_paragraphs",
value='["Experienced software engineer with 5+ years in full-stack development.", "Passionate about building scalable systems and leading technical teams."]',
placeholder='["Your intro paragraph 1", "Your intro paragraph 2"]',
lines=2
)
api_quick_guide_items = gr.Textbox(
label="quick_guide_items",
value='["Python Expert", "Team Leadership", "System Architecture", "Agile Methodologies"]',
placeholder='["Skill 1", "Skill 2", "Skill 3"]',
lines=2
)
api_education = gr.Textbox(
label="education",
value='[{"degree": "Master of Science", "institution": "MIT", "date_range": "2018-2020", "field_of_study": "Computer Science", "highlights": ["GPA: 3.9/4.0", "Thesis: Machine Learning Systems"]}]',
placeholder='[{"degree": "Your degree", "institution": "Your school", "date_range": "2018-2020", "field_of_study": "Your field", "highlights": ["Achievement 1"]}]',
lines=3
)
api_experience = gr.Textbox(
label="experience",
value='[{"company": "Google", "role": "Senior Software Engineer", "location": "Mountain View, CA", "date_range": "2020-Present", "highlights": ["Led team of 8 engineers", "Improved system performance by 45%", "Launched 3 major features"]}]',
placeholder='[{"company": "Company Name", "role": "Your Role", "location": "City, Country", "date_range": "2020-Present", "highlights": ["Achievement 1", "Achievement 2"]}]',
lines=3
)
api_publications = gr.Textbox(
label="publications",
value='[]',
placeholder='[{"title": "Paper Title", "authors": ["Author 1", "Author 2"], "date": "2023", "doi": "10.1000/xyz123"}]',
lines=1
)
api_projects = gr.Textbox(
label="projects",
value='[{"title": "E-commerce Platform", "repo_url": "https://github.com/johndoe/ecommerce", "repo_label": "github.com/johndoe/ecommerce", "highlights": ["Built with React and Node.js", "Handles 10k+ concurrent users", "99.9% uptime"]}]',
placeholder='[{"title": "Project Name", "repo_url": "https://github.com/user/repo", "repo_label": "github.com/user/repo", "highlights": ["Feature 1", "Feature 2"]}]',
lines=2
)
api_languages = gr.Textbox(
label="languages",
value='["Python", "JavaScript", "TypeScript", "Go", "Rust"]',
placeholder='["Language1", "Language2", "Language3"]',
lines=1
)
api_technologies = gr.Textbox(
label="technologies",
value='["React", "Node.js", "Docker", "Kubernetes", "AWS", "PostgreSQL", "Redis"]',
placeholder='["Tech1", "Tech2", "Framework1", "Database1"]',
lines=1
)
api_resume_output = gr.File(label="Generated PDF Resume")
api_resume_trigger = gr.Button("Generate Resume")
# API: resume_generation_system_prompt - System prompt principal
with gr.Row():
api_system_output = gr.Textbox(label="System Prompt for Resume Generation", lines=15)
api_system_trigger = gr.Button("Get System Prompt")
# Hook API names to the triggers
api_resume_trigger.click(
fn=generate_resume_pdf,
inputs=[
api_pdf_title, api_pdf_author, api_name, api_location, api_email, api_phone,
api_last_updated_text, api_website_url, api_website_label,
api_linkedin_url, api_linkedin_handle, api_github_url, api_github_handle,
api_intro_paragraphs, api_quick_guide_items, api_education, api_experience,
api_publications, api_projects, api_languages, api_technologies
],
outputs=[api_resume_output],
api_name="generate_resume_pdf"
)
api_system_trigger.click(
fn=resume_generation_system_prompt,
inputs=[],
outputs=[api_system_output],
api_name="resume_generation_system_prompt"
)
# Launch the interface with MCP server enabled
# Following best practices from Gradio MCP documentation
demo.launch(
mcp_server=True, # Enable MCP server
server_name=server_host,
server_port=server_port,
share=False, # No sharing for production/HF Spaces
allowed_paths=[str(OUTPUT_DIR)], # Allow access to generated PDFs
show_api=True, # Show API documentation
)
print(f"🚀 Resume Generator MCP Server is running!")
print(f"📊 Web Interface: http://{server_host}:{server_port}")
print(f"🔗 MCP Endpoint: http://{server_host}:{server_port}/gradio_api/mcp/sse")
print(f"📋 API Schema: http://{server_host}:{server_port}/gradio_api/mcp/schema")
def main() -> None:
import argparse
parser = argparse.ArgumentParser(description="Générer le CV PDF depuis des données JSON ou lancer l'UI Gradio")
parser.add_argument(
"--serve",
action="store_true",
help="Lancer l'interface Gradio (au lieu du mode CLI)",
)
parser.add_argument(
"--input-json",
type=Path,
required=False,
help="Chemin du fichier JSON d'entrée (conforme au modèle Resume)",
)
parser.add_argument(
"--template",
type=str,
default=TEMPLATE_NAME,
help="Nom du fichier template (dans le dossier templates)",
)
parser.add_argument(
"--server-host",
type=str,
default="127.0.0.1",
help="Hôte pour l'UI Gradio (par défaut 127.0.0.1)",
)
parser.add_argument(
"--server-port",
type=int,
default=7860,
help="Port pour l'UI Gradio (par défaut 7860)",
)
parser.add_argument(
"--out-dir",
type=Path,
default=OUTPUT_DIR,
help="Dossier de sortie pour les fichiers générés (.tex, .pdf, artefacts LaTeX)",
)
# Suppression de l'argument --basename: le nom est toujours un UUID
parser.add_argument(
"--engine",
type=str,
choices=["tectonic", "latexmk"],
default=None,
help="Moteur de compilation LaTeX à privilégier (par défaut: latexmk sous Linux, sinon tectonic)",
)
parser.add_argument(
"--out-tex",
type=Path,
default=None,
help="Chemin du fichier .tex rendu (défaut: build/output.tex)",
)
parser.add_argument(
"--out-pdf",
type=Path,
default=None,
help="Chemin du PDF de sortie (défaut: build/output.pdf)",
)
args = parser.parse_args()
# Mode serveur (UI Gradio)
if args.serve:
launch_gradio(server_host=args.server_host, server_port=args.server_port)
return
if not args.input_json:
parser.error("--input-json est requis en mode CLI (sans --serve)")
data = json.loads(args.input_json.read_text(encoding="utf-8"))
# Nom de base: toujours un UUID
base_name = uuid.uuid4().hex
# Résoudre les chemins de sortie par défaut si non fournis
out_tex = args.out_tex or (args.out_dir / f"{base_name}.tex")
out_pdf = args.out_pdf or (args.out_dir / f"{base_name}.pdf")
# Forcer le template classic par défaut si non précisé
render_tex(data, out_tex, template_name=args.template or TEMPLATE_NAME)
# Choix du moteur par défaut selon l'OS
preferred_engine = args.engine
if preferred_engine is None:
preferred_engine = "latexmk" if platform.system() == "Linux" else "tectonic"
compile_pdf(out_tex, out_pdf, engine_preference=preferred_engine)
print(f"PDF généré: {out_pdf}")
if __name__ == "__main__":
main()