Spaces:
Runtime error
Runtime error
| """ | |
| Word Document CV Generation Service | |
| Integrates with Office-Word-MCP-Server for professional Word resumes | |
| """ | |
| import os | |
| import json | |
| import logging | |
| from typing import Dict, Any, Optional, List | |
| from datetime import datetime | |
| import subprocess | |
| import requests | |
| from pathlib import Path | |
| from models.schemas import ResumeDraft, CoverLetterDraft, JobPosting | |
| logger = logging.getLogger(__name__) | |
| class WordCVGenerator: | |
| """Generate professional Word documents using MCP Server or python-docx""" | |
| def __init__(self): | |
| self.mcp_server_url = os.getenv("WORD_MCP_URL", "http://localhost:3001") | |
| self.templates = { | |
| "modern": "Modern ATS-friendly template", | |
| "executive": "Executive format with header", | |
| "creative": "Creative design with colors", | |
| "minimal": "Minimal clean design", | |
| "academic": "Academic CV format" | |
| } | |
| def create_resume_document( | |
| self, | |
| resume: ResumeDraft, | |
| job: Optional[JobPosting] = None, | |
| template: str = "modern", | |
| output_path: str = None | |
| ) -> str: | |
| """Create a Word document resume""" | |
| try: | |
| if not output_path: | |
| company_name = job.company.replace(' ', '_') if job else "general" | |
| output_path = f"resume_{company_name}_{datetime.now().strftime('%Y%m%d')}.docx" | |
| # Try MCP server first | |
| if self._use_mcp_server(resume, template, output_path): | |
| logger.info(f"Resume created via MCP: {output_path}") | |
| return output_path | |
| # Fallback to python-docx | |
| return self._create_with_python_docx(resume, job, template, output_path) | |
| except Exception as e: | |
| logger.error(f"Error creating Word resume: {e}") | |
| return None | |
| def create_cover_letter_document( | |
| self, | |
| cover_letter: CoverLetterDraft, | |
| job: JobPosting, | |
| template: str = "modern", | |
| output_path: str = None | |
| ) -> str: | |
| """Create a Word document cover letter""" | |
| try: | |
| if not output_path: | |
| company_name = job.company.replace(' ', '_') | |
| output_path = f"cover_letter_{company_name}_{datetime.now().strftime('%Y%m%d')}.docx" | |
| # Try MCP server first | |
| if self._use_mcp_server_cover(cover_letter, job, template, output_path): | |
| logger.info(f"Cover letter created via MCP: {output_path}") | |
| return output_path | |
| # Fallback to python-docx | |
| return self._create_cover_with_python_docx(cover_letter, job, template, output_path) | |
| except Exception as e: | |
| logger.error(f"Error creating Word cover letter: {e}") | |
| return None | |
| def _use_mcp_server(self, resume: ResumeDraft, template: str, output_path: str) -> bool: | |
| """Try to use MCP server for document generation""" | |
| try: | |
| # Create document | |
| response = self._call_mcp_tool("create_document", { | |
| "template": template | |
| }) | |
| if not response.get("success"): | |
| return False | |
| # Add header with contact info | |
| self._call_mcp_tool("add_header", { | |
| "name": resume.sections.get("name", ""), | |
| "email": resume.sections.get("email", ""), | |
| "phone": resume.sections.get("phone", ""), | |
| "linkedin": resume.sections.get("linkedin", "") | |
| }) | |
| # Add professional summary | |
| self._call_mcp_tool("add_section", { | |
| "title": "Professional Summary", | |
| "content": resume.sections.get("summary", "") | |
| }) | |
| # Add experience section | |
| experiences = resume.sections.get("experience", []) | |
| if experiences: | |
| self._call_mcp_tool("add_section", { | |
| "title": "Professional Experience" | |
| }) | |
| for exp in experiences: | |
| self._call_mcp_tool("add_experience", { | |
| "title": exp.get("title", ""), | |
| "company": exp.get("company", ""), | |
| "dates": exp.get("dates", ""), | |
| "bullets": exp.get("bullets", []) | |
| }) | |
| # Add skills section | |
| skills = resume.sections.get("skills", {}) | |
| if skills: | |
| self._call_mcp_tool("add_section", { | |
| "title": "Core Skills" | |
| }) | |
| for category, skill_list in skills.items(): | |
| if isinstance(skill_list, list): | |
| self._call_mcp_tool("add_skill_category", { | |
| "category": category, | |
| "skills": skill_list | |
| }) | |
| # Add education section | |
| education = resume.sections.get("education", []) | |
| if education: | |
| self._call_mcp_tool("add_section", { | |
| "title": "Education" | |
| }) | |
| for edu in education: | |
| self._call_mcp_tool("add_education", { | |
| "degree": edu.get("degree", ""), | |
| "school": edu.get("school", ""), | |
| "dates": edu.get("dates", ""), | |
| "details": edu.get("details", "") | |
| }) | |
| # Save document | |
| self._call_mcp_tool("save_document", { | |
| "file_path": output_path | |
| }) | |
| return True | |
| except Exception as e: | |
| logger.error(f"MCP server error: {e}") | |
| return False | |
| def _create_with_python_docx( | |
| self, | |
| resume: ResumeDraft, | |
| job: Optional[JobPosting], | |
| template: str, | |
| output_path: str | |
| ) -> str: | |
| """Create resume using python-docx as fallback""" | |
| try: | |
| from docx import Document | |
| from docx.shared import Pt, Inches, RGBColor | |
| from docx.enum.text import WD_ALIGN_PARAGRAPH | |
| from docx.enum.style import WD_STYLE_TYPE | |
| doc = Document() | |
| # Set margins | |
| sections = doc.sections | |
| for section in sections: | |
| section.top_margin = Inches(0.5) | |
| section.bottom_margin = Inches(0.5) | |
| section.left_margin = Inches(0.7) | |
| section.right_margin = Inches(0.7) | |
| # Header with name and contact | |
| header = doc.add_paragraph() | |
| header.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| name_run = header.add_run(resume.sections.get("name", "Professional")) | |
| name_run.font.size = Pt(20) | |
| name_run.font.bold = True | |
| # Contact info | |
| contact = doc.add_paragraph() | |
| contact.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| contact_text = [] | |
| if resume.sections.get("email"): | |
| contact_text.append(resume.sections["email"]) | |
| if resume.sections.get("phone"): | |
| contact_text.append(resume.sections["phone"]) | |
| if resume.sections.get("linkedin"): | |
| contact_text.append(resume.sections["linkedin"]) | |
| contact.add_run(" | ".join(contact_text)) | |
| # Professional Summary | |
| if resume.sections.get("summary"): | |
| doc.add_heading("Professional Summary", level=1) | |
| doc.add_paragraph(resume.sections["summary"]) | |
| # Professional Experience | |
| experiences = resume.sections.get("experience", []) | |
| if experiences: | |
| doc.add_heading("Professional Experience", level=1) | |
| for exp in experiences: | |
| # Job title and company | |
| exp_header = doc.add_paragraph() | |
| title_run = exp_header.add_run(f"{exp.get('title', '')} ") | |
| title_run.font.bold = True | |
| exp_header.add_run(f"| {exp.get('company', '')} | {exp.get('dates', '')}") | |
| # Bullets | |
| for bullet in exp.get("bullets", []): | |
| p = doc.add_paragraph(f"β’ {bullet}", style='List Bullet') | |
| p.paragraph_format.left_indent = Inches(0.5) | |
| # Skills | |
| skills = resume.sections.get("skills", {}) | |
| if skills: | |
| doc.add_heading("Core Skills", level=1) | |
| for category, skill_list in skills.items(): | |
| if isinstance(skill_list, list): | |
| p = doc.add_paragraph() | |
| p.add_run(f"{category}: ").bold = True | |
| p.add_run(", ".join(skill_list)) | |
| # Education | |
| education = resume.sections.get("education", []) | |
| if education: | |
| doc.add_heading("Education", level=1) | |
| for edu in education: | |
| edu_p = doc.add_paragraph() | |
| edu_p.add_run(f"{edu.get('degree', '')}").bold = True | |
| edu_p.add_run(f" | {edu.get('school', '')} | {edu.get('dates', '')}") | |
| # Save document | |
| doc.save(output_path) | |
| logger.info(f"Word resume created: {output_path}") | |
| return output_path | |
| except ImportError: | |
| logger.error("python-docx not installed") | |
| return None | |
| def _use_mcp_server_cover( | |
| self, | |
| cover_letter: CoverLetterDraft, | |
| job: JobPosting, | |
| template: str, | |
| output_path: str | |
| ) -> bool: | |
| """Try to use MCP server for cover letter generation""" | |
| try: | |
| # Create document | |
| response = self._call_mcp_tool("create_document", { | |
| "template": template | |
| }) | |
| if not response.get("success"): | |
| return False | |
| # Add cover letter content | |
| self._call_mcp_tool("add_cover_letter", { | |
| "recipient": job.company, | |
| "position": job.title, | |
| "content": cover_letter.text, | |
| "sender_name": cover_letter.sections.get("name", ""), | |
| "sender_email": cover_letter.sections.get("email", "") | |
| }) | |
| # Save document | |
| self._call_mcp_tool("save_document", { | |
| "file_path": output_path | |
| }) | |
| return True | |
| except Exception as e: | |
| logger.error(f"MCP server error for cover letter: {e}") | |
| return False | |
| def _create_cover_with_python_docx( | |
| self, | |
| cover_letter: CoverLetterDraft, | |
| job: JobPosting, | |
| template: str, | |
| output_path: str | |
| ) -> str: | |
| """Create cover letter using python-docx as fallback""" | |
| try: | |
| from docx import Document | |
| from docx.shared import Pt, Inches | |
| doc = Document() | |
| # Set margins | |
| sections = doc.sections | |
| for section in sections: | |
| section.top_margin = Inches(1) | |
| section.bottom_margin = Inches(1) | |
| section.left_margin = Inches(1) | |
| section.right_margin = Inches(1) | |
| # Date | |
| doc.add_paragraph(datetime.now().strftime("%B %d, %Y")) | |
| doc.add_paragraph() | |
| # Recipient | |
| doc.add_paragraph(f"Hiring Manager") | |
| doc.add_paragraph(job.company) | |
| doc.add_paragraph() | |
| # Position | |
| doc.add_paragraph(f"Re: {job.title}") | |
| doc.add_paragraph() | |
| # Cover letter body | |
| paragraphs = cover_letter.text.split('\n\n') | |
| for para in paragraphs: | |
| if para.strip(): | |
| doc.add_paragraph(para.strip()) | |
| # Signature | |
| doc.add_paragraph() | |
| doc.add_paragraph("Sincerely,") | |
| doc.add_paragraph(cover_letter.sections.get("name", "")) | |
| # Save document | |
| doc.save(output_path) | |
| logger.info(f"Word cover letter created: {output_path}") | |
| return output_path | |
| except ImportError: | |
| logger.error("python-docx not installed") | |
| return None | |
| def _call_mcp_tool(self, tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]: | |
| """Call MCP server tool""" | |
| try: | |
| response = requests.post( | |
| f"{self.mcp_server_url}/tools/{tool_name}", | |
| json=params, | |
| timeout=30 | |
| ) | |
| response.raise_for_status() | |
| return response.json() | |
| except requests.exceptions.RequestException as e: | |
| logger.error(f"MCP server call failed: {e}") | |
| return {"success": False, "error": str(e)} |