Noo88ear's picture
πŸš€ Initial deployment of Multi-Agent Job Application Assistant
7498f2c
"""
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)}