Skip to content

Commit

Permalink
✨ feat(resume): enhance style selection and management
Browse files Browse the repository at this point in the history
  - implement user-driven style selection process with fallback
  - improve readability and logging during style selection
  - introduce flexibility by reading style directly from file

♻️ refactor(style_manager): streamline style management logic
  - remove unused imports and functions
  - simplify style path retrieval
  - refine error handling in style management

🐛 fix(pdf_generation): correct style path retrieval logic
  - ensure style is selected before PDF generation
  - adjust style CSS reading in resume_generator

🔧 chore(logging): improve logging configuration and readability
  - standardize log messages across modules and remove Italian comments
  • Loading branch information
feder-cr committed Dec 5, 2024
1 parent 110e993 commit 0c8e49a
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 111 deletions.
62 changes: 40 additions & 22 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,6 @@ def create_resume_pdf_job_tailored(parameters: dict, llm_api_key: str):
plain_text_resume = file.read()

style_manager = StyleManager()
style_manager.choose_style()
questions = [inquirer.Text('job_url', message="Please enter the URL of the job description:")]
answers = inquirer.prompt(questions)
job_url = answers.get('job_url')
Expand All @@ -308,6 +307,7 @@ def create_resume_pdf_job_tailored(parameters: dict, llm_api_key: str):
resume_object=resume_object,
output_path=Path("data_folder/output"),
)
resume_facade.choose_style()
resume_facade.set_driver(driver)
resume_facade.link_to_job(job_url)
result_base64, suggested_name = resume_facade.create_resume_pdf_job_tailored()
Expand Down Expand Up @@ -350,59 +350,77 @@ def create_resume_pdf(parameters: dict, llm_api_key: str):
try:
logger.info("Generating a CV based on provided parameters.")

# Carica il resume in testo semplice
# Load the plain text resume
with open(parameters["uploads"]["plainTextResume"], "r", encoding="utf-8") as file:
plain_text_resume = file.read()

# Initialize StyleManager
style_manager = StyleManager()
style_manager.choose_style()
questions = [inquirer.Text('job_url', message="Please enter the URL of the job description:")]
answers = inquirer.prompt(questions)
job_url = answers.get('job_url')
available_styles = style_manager.get_styles()

if not available_styles:
logger.warning("No styles available. Proceeding without style selection.")
else:
# Present style choices to the user
choices = style_manager.format_choices(available_styles)
questions = [
inquirer.List(
"style",
message="Select a style for the resume:",
choices=choices,
)
]
style_answer = inquirer.prompt(questions)
if style_answer and "style" in style_answer:
selected_choice = style_answer["style"]
for style_name, (file_name, author_link) in available_styles.items():
if selected_choice.startswith(style_name):
style_manager.set_selected_style(style_name)
logger.info(f"Selected style: {style_name}")
break
else:
logger.warning("No style selected. Proceeding with default style.")

# Initialize the Resume Generator
resume_generator = ResumeGenerator()
resume_object = Resume(plain_text_resume)
driver = init_browser()
resume_generator.set_resume_object(resume_object)
resume_facade = ResumeFacade(

# Create the ResumeFacade
resume_facade = ResumeFacade(
api_key=llm_api_key,
style_manager=style_manager,
resume_generator=resume_generator,
resume_object=resume_object,
output_path=Path("data_folder/output"),
)
resume_facade.set_driver(driver)
resume_facade.link_to_job(job_url)
result_base64, suggested_name = resume_facade.create_resume_pdf()
result_base64 = resume_facade.create_resume_pdf()

# Decodifica Base64 in dati binari
# Decode Base64 to binary data
try:
pdf_data = base64.b64decode(result_base64)
except base64.binascii.Error as e:
logger.error("Error decoding Base64: %s", e)
raise

# Definisci il percorso della cartella di output utilizzando `suggested_name`
output_dir = Path(parameters["outputFileDirectory"]) / suggested_name
# Define the output directory using `suggested_name`
output_dir = Path(parameters["outputFileDirectory"])

# Crea la cartella se non esiste
try:
output_dir.mkdir(parents=True, exist_ok=True)
logger.info(f"Cartella di output creata o già esistente: {output_dir}")
except IOError as e:
logger.error("Error creating output directory: %s", e)
raise

output_path = output_dir / "resume.pdf"
# Write the PDF file
output_path = output_dir / "resume_base.pdf"
try:
with open(output_path, "wb") as file:
file.write(pdf_data)
logger.info(f"CV salvato in: {output_path}")
logger.info(f"Resume saved at: {output_path}")
except IOError as e:
logger.error("Error writing file: %s", e)
raise
except Exception as e:
logger.exception(f"An error occurred while creating the CV: {e}")
raise


def handle_inquiries(selected_actions: List[str], parameters: dict, llm_api_key: str):
"""
Expand Down
4 changes: 3 additions & 1 deletion src/libs/resume_and_cover_builder/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ def __init__(self):
<link href="https://fonts.googleapis.com/css2?family=Barlow:wght@400;600&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Barlow:wght@400;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" />
<link rel="stylesheet" href="file://$style_path">
<style>
$style_css
</style>
</head>
<body>
$body
Expand Down
12 changes: 5 additions & 7 deletions src/libs/resume_and_cover_builder/resume_facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,10 @@ def create_resume_pdf_job_tailored(self) -> tuple[bytes, str]:
Returns:
tuple: A tuple containing the PDF content as bytes and the unique filename.
"""
style_path = self.style_manager.get_style_path()
if self.selected_style is None:
raise ValueError("You must choose a style before generating the PDF.")

style_path = self.style_manager.get_style_path(self.selected_style)


html_resume = self.resume_generator.create_resume_job_description_text(style_path, self.job.description)

Expand All @@ -130,16 +130,14 @@ def create_resume_pdf(self) -> tuple[bytes, str]:
Returns:
tuple: A tuple containing the PDF content as bytes and the unique filename.
"""

if self.selected_style is None:
style_path = self.style_manager.get_style_path()
if style_path is None:
raise ValueError("You must choose a style before generating the PDF.")

style_path = self.style_manager.get_style_path(self.selected_style)
html_resume = self.resume_generator.create_resume(style_path)
suggested_name = hashlib.md5(self.job.link.encode()).hexdigest()[:10]
result = HTML_to_PDF(html_resume, self.driver)
self.driver.quit()
return result, suggested_name
return result

def create_cover_letter(self) -> tuple[bytes, str]:
"""
Expand Down
22 changes: 20 additions & 2 deletions src/libs/resume_and_cover_builder/resume_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,25 @@ def set_resume_object(self, resume_object):


def _create_resume(self, gpt_answerer: Any, style_path):
# Imposta il resume nell'oggetto gpt_answerer
gpt_answerer.set_resume(self.resume_object)

# Leggi il template HTML
template = Template(global_config.html_template)
return template.substitute(body=gpt_answerer.generate_html_resume(), style_path=style_path)

try:
with open(style_path, "r") as f:
style_css = f.read() # Correzione: chiama il metodo `read` con le parentesi
except FileNotFoundError:
raise ValueError(f"Il file di stile non è stato trovato nel percorso: {style_path}")
except Exception as e:
raise RuntimeError(f"Errore durante la lettura del file CSS: {e}")

# Genera l'HTML del resume
body_html = gpt_answerer.generate_html_resume()

# Applica i contenuti al template
return template.substitute(body=body_html, style_css=style_css)

def create_resume(self, style_path):
strings = load_module(global_config.STRINGS_MODULE_RESUME_PATH, global_config.STRINGS_MODULE_NAME)
Expand All @@ -41,7 +57,9 @@ def create_cover_letter_job_description(self, style_path: str, job_description_t
gpt_answerer.set_job_description_from_text(job_description_text)
cover_letter_html = gpt_answerer.generate_cover_letter()
template = Template(global_config.html_template)
return template.substitute(body=cover_letter_html, style_path=style_path)
with open(style_path, "r") as f:
style_css = f.read()
return template.substitute(body=cover_letter_html, style_css=style_css)



Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from libs.resume_and_cover_builder.template_base import *
from src.libs.resume_and_cover_builder.template_base import prompt_header_template, prompt_education_template, prompt_working_experience_template, prompt_projects_template, prompt_achievements_template, prompt_certifications_template, prompt_additional_skills_template

prompt_header = """
Act as an HR expert and resume writer specializing in ATS-friendly resumes. Your task is to create a professional and polished header for the resume. The header should:
Expand Down
116 changes: 39 additions & 77 deletions src/libs/resume_and_cover_builder/style_manager.py
Original file line number Diff line number Diff line change
@@ -1,126 +1,88 @@
# src/ai_hawk/libs/resume_and_cover_builder/style_manager.py
import os
from pathlib import Path
from typing import Dict, List, Tuple, Optional
import inquirer
import webbrowser
import sys
import logging

# Configura il logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
# Configure logging
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s")


class StyleManager:

def __init__(self):
self.styles_directory: Optional[Path] = None
self.selected_style: Optional[str] = None
current_file = Path(__file__).resolve()
# Salire di 4 livelli per raggiungere la radice del progetto
project_root = current_file.parent.parent.parent.parent # Adatta se la struttura cambia

# Imposta la directory degli stili in modo robusto
project_root = current_file.parent.parent.parent.parent
self.styles_directory = project_root / "src" / "libs" / "resume_and_cover_builder" / "resume_style"
logging.debug(f"Project root determinato come: {project_root}")
logging.debug(f"Directory degli stili impostata su: {self.styles_directory}")

logging.debug(f"Project root determined as: {project_root}")
logging.debug(f"Styles directory set to: {self.styles_directory}")

def get_styles(self) -> Dict[str, Tuple[str, str]]:
"""
Ottiene gli stili disponibili nella directory degli stili.
Retrieve the available styles from the styles directory.
Returns:
Dict[str, Tuple[str, str]]: Un dizionario che mappa i nomi degli stili ai loro file e link degli autori.
Dict[str, Tuple[str, str]]: A dictionary mapping style names to their file names and author links.
"""
styles_to_files = {}
if not self.styles_directory:
logging.warning("Directory degli stili non impostata.")
logging.warning("Styles directory is not set.")
return styles_to_files
logging.debug(f"Leggendo la directory degli stili: {self.styles_directory}")
logging.debug(f"Reading styles directory: {self.styles_directory}")
try:
files = [f for f in self.styles_directory.iterdir() if f.is_file()]
logging.debug(f"Files trovati: {[f.name for f in files]}")
logging.debug(f"Files found: {[f.name for f in files]}")
for file_path in files:
logging.debug(f"Processando file: {file_path}")
with file_path.open('r', encoding='utf-8') as file:
logging.debug(f"Processing file: {file_path}")
with file_path.open("r", encoding="utf-8") as file:
first_line = file.readline().strip()
logging.debug(f"Prima linea del file {file_path.name}: {first_line}")
logging.debug(f"First line of file {file_path.name}: {first_line}")
if first_line.startswith("/*") and first_line.endswith("*/"):
content = first_line[2:-2].strip()
if '$' in content:
style_name, author_link = content.split('$', 1)
if "$" in content:
style_name, author_link = content.split("$", 1)
style_name = style_name.strip()
author_link = author_link.strip()
styles_to_files[style_name] = (file_path.name, author_link)
logging.info(f"Aggiunto stile: {style_name} da {author_link}")
logging.info(f"Added style: {style_name} by {author_link}")
except FileNotFoundError:
logging.error(f"Directory {self.styles_directory} non trovata.")
logging.error(f"Directory {self.styles_directory} not found.")
except PermissionError:
logging.error(f"Permesso negato per accedere a {self.styles_directory}.")
logging.error(f"Permission denied for accessing {self.styles_directory}.")
except Exception as e:
logging.error(f"Errore imprevisto durante la lettura degli stili: {e}")
logging.error(f"Unexpected error while reading styles: {e}")
return styles_to_files

def format_choices(self, styles_to_files: Dict[str, Tuple[str, str]]) -> List[str]:
"""
Format the style choices for the user.
Format the style choices for user presentation.
Args:
styles_to_files (Dict[str, Tuple[str, str]]): A dictionary mapping style names to their file names and author links.
Returns:
List[str]: A list of formatted style choices.
"""
return [f"{style_name} (style author -> {author_link})" for style_name, (file_name, author_link) in styles_to_files.items()]

def get_style_path(self) -> Path:
def set_selected_style(self, selected_style: str):
"""
Get the path to the selected style.
Directly set the selected style.
Args:
selected_style (str): The selected style.
Returns:
Path: a Path object representing the path to the selected style file.
selected_style (str): The name of the style to select.
"""
styles = self.get_styles()
if self.selected_style not in styles:
raise ValueError(f"Style '{self.selected_style}' not found.")
file_name, _ = styles[self.selected_style]
return self.styles_directory / file_name
self.selected_style = selected_style
logging.info(f"Selected style set to: {self.selected_style}")

def choose_style(self) -> Optional[str]:
def get_style_path(self) -> Optional[Path]:
"""
Prompt the user to select a style using inquirer.
Get the path to the selected style.
Returns:
Optional[str]: The name of the selected style, or None if selection was canceled.
Path: A Path object representing the path to the selected style file, or None if not found.
"""
styles = self.get_styles()
if not styles:
logging.warning("Nessuno stile disponibile per la selezione.")
return None

final_style_choice = "Crea il tuo stile di resume in CSS"
formatted_choices = self.format_choices(styles)
formatted_choices.append(final_style_choice)

questions = [
inquirer.List(
'selected_style',
message="Quale stile vorresti adottare?",
choices=formatted_choices
)
]

answers = inquirer.prompt(questions)
if answers and 'selected_style' in answers:
selected_display = answers['selected_style']
if selected_display == final_style_choice:
tutorial_url = "https://github.com/feder-cr/lib_resume_builder_AIHawk/blob/main/how_to_contribute/web_designer.md"
logging.info("\nApro il tutorial nel tuo browser...")
webbrowser.open(tutorial_url)
sys.exit(0)
else:
# Estrai il nome dello stile dal formato "style_name (style author -> author_link)"
style_name = selected_display.split(' (')[0]
logging.info(f"Hai selezionato lo stile: {style_name}")
self.selected_style = style_name
return style_name
else:
logging.warning("Selezione annullata.")
try:
styles = self.get_styles()
if self.selected_style not in styles:
raise ValueError(f"Style '{self.selected_style}' not found.")
file_name, _ = styles[self.selected_style]
return self.styles_directory / file_name
except Exception as e:
logging.error(f"Error retrieving selected style: {e}")
return None
4 changes: 3 additions & 1 deletion src/utils/chrome_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def chrome_browser_options():
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--ignore-certificate-errors")
options.add_argument("--disable-extensions")
options.add_argument("--disable-gpu")
options.add_argument("--disable-gpu") # Opzionale, utile in alcuni ambienti
options.add_argument("window-size=1200x800")
options.add_argument("--disable-background-timer-throttling")
options.add_argument("--disable-backgrounding-occluded-windows")
Expand All @@ -29,6 +29,8 @@ def chrome_browser_options():
options.add_argument("--disable-animations")
options.add_argument("--disable-cache")
options.add_argument("--incognito")
options.add_argument("--allow-file-access-from-files") # Consente l'accesso ai file locali
options.add_argument("--disable-web-security") # Disabilita la sicurezza web
logger.debug("Using Chrome in incognito mode")

return options
Expand Down

0 comments on commit 0c8e49a

Please sign in to comment.