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