Skip to content

Commit

Permalink
Merge pull request #24 from AliAfshar7/add-editor-cv
Browse files Browse the repository at this point in the history
add CV editor
  • Loading branch information
mohammadi-com authored Jan 5, 2025
2 parents 9389c44 + 5ba157b commit c3ddf83
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 44 deletions.
30 changes: 28 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def main():
resume_template = st.selectbox("Select Resume Template", [template.value for template in ResumeTemplate], index=2)
job_description = st.text_area("Enter Job Description", height=200)
resume = st.text_area("Enter Resume", height=200)
tab1, tab2, tab3 = st.tabs(["Generate Resume", "Generate Cover Letter", "Answer application questions"])
tab1, tab2, tab3, tab4 = st.tabs(["Generate Resume", "Generate Cover Letter", "Answer application questions", "CV Editor"])


job_data = {
Expand All @@ -51,7 +51,9 @@ def main():

if st.button("Generate Resume"):
result = app.call_api("generate-latex-resume-save", job_data)
st.success(f"Resume saved: {result['path']}")
# session_state shows that whether latex code is present or not, as in the cv_editor part, we want to edit the latex code
st.session_state['latex_code'] = result['latex_code']
st.success(f"Resume saved: {result['pdf_file_path']}")


with tab2:
Expand All @@ -66,6 +68,30 @@ def main():
if st.button("Generate answer"):
result = app.call_api("answer-application-questions", job_data)
st.text_area("Generated Answer", value=result, height=400)

with tab4:
# There should be a latex code to display, as the purpose of this section is to edit the latex code
if 'latex_code' not in st.session_state:
st.session_state['latex_code'] = ""

st.header("LaTeX Editor")
edited_latex = st.text_area("Edit LaTeX Code", value=st.session_state['latex_code'], height=400)
if st.button("Save Final PDF"):
save_data = {
"latex": {
"latex_code":edited_latex
},
"tailoring_options": {
"ai_model":ai_model,
"resume_template": resume_template
}
}
result = app.call_api("save-latex-resume", save_data)
if result.get('path'):
st.success(f"PDF saved at: {result['path']}")
else:
st.error("An error occured. Please try again.")


if __name__ == "__main__":
main()
43 changes: 26 additions & 17 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import os

from datetime import datetime
from fastapi import FastAPI
import openai_wrapper
from models.templates import john_doe_resume, john_doe_legal_authorization, john_doe_preferences
from models.templates import john_doe_resume, john_doe_legal_authorization, john_doe_preferences, Template_Details
from models.tailoring_options import TailoringOptions
from models.job import Job
from models.profile import Profile, Resume
from utils import generate_pdf
from utils import save_pdf, generate_pdf_from_latex
from log import logger
from models.question import Question

from models.latex import Latex

app = FastAPI()

Expand Down Expand Up @@ -52,15 +52,6 @@ def generate_tailored_plain_coverletter(job: Job, profile: Profile = Profile(Res
"""
return openai_wrapper.create_tailored_plain_coverletter(profile.resume.text, job.description, tailoring_options.ai_model)

@app.post("/generate-tailored-latex-resume")
def generate_tailored_latex_resume(job: Job, profile: Profile = Profile(Resume(john_doe_resume)), tailoring_options: TailoringOptions = TailoringOptions()) -> str:
"""
Gets resume and job description in plain text and returns tailored resume as a latex
"""
tailored_plain_resume = generate_tailored_plain_resume(job, profile, tailoring_options)
_, trimed_tailored_resume = openai_wrapper.covert_plain_resume_to_latex(tailored_plain_resume, tailoring_options.ai_model, tailoring_options.resume_template)
return trimed_tailored_resume

@app.post("/generate-latex-resume-save")
def generate_tailored_latex_resume_save(job: Job, profile: Profile = Profile(Resume(john_doe_resume)), tailoring_options: TailoringOptions = TailoringOptions()):
"""
Expand All @@ -71,11 +62,16 @@ def generate_tailored_latex_resume_save(job: Job, profile: Profile = Profile(Res
f"Give the name of the company that this job description is for. As the output just give the name, nothing else. Job description: {job.description}"
) # Since this is a simple task we use the cheapest ai

pdf_path = generate_pdf(company_name, tailored_plain_resume, tailoring_options)

current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
pdf_path = f'./Resumes/{current_time}_{company_name}'
os.makedirs(pdf_path, exist_ok=True)
latex_compiler_response, latex_code = openai_wrapper.covert_plain_resume_to_latex(current_time, company_name, tailored_plain_resume, tailoring_options.ai_model, tailoring_options.resume_template)
pdf_file_path = save_pdf(pdf_path, latex_compiler_response.content)

return {
"success": f"Generated resume saved at here: {pdf_path}",
"path": os.path.abspath(pdf_path)
"success": f"Generated resume saved at here: {pdf_file_path}",
"pdf_file_path": pdf_file_path,
"latex_code": latex_code
}

@app.post("/answer-application-questions")
Expand All @@ -85,3 +81,16 @@ def answer_application_questions(job: Job, question: Question, profile: Profile
"""

return openai_wrapper.generate_answer_questions(profile.resume.text, job.description, question.description, tailoring_options.ai_model)

@app.post("/save-latex-resume")
def save_latex_resume(latex: Latex, tailoring_options:TailoringOptions):
"""
Saves the edited LaTeX code as PDF
"""
current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
company_name = "edited"
compiler = Template_Details[tailoring_options.resume_template]['compiler']
latex_compiler_response = generate_pdf_from_latex(current_time, company_name, latex.latex_code, compiler)
pdf_path = f'./Resumes/edited/{current_time}'
pdf_file_path = save_pdf(pdf_path, latex_compiler_response.content)
return {"path":pdf_file_path}
5 changes: 5 additions & 0 deletions models/latex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from dataclasses import dataclass

@dataclass
class Latex:
latex_code: str
13 changes: 6 additions & 7 deletions openai_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
from models.ai_models import AIModel
from models.templates import ResumeTemplate, Template_Details
import os
from utils import generate_tex_and_tar
from utils import generate_tex_and_tar, generate_pdf_from_latex
from config import TEX_FILE_NAME, TAR_FOLDER_NAME
from datetime import datetime

client = OpenAI(api_key=OPEN_AI_KEY) # we recommend using python-dotenv to add OPENAI_API_KEY="My API Key" to your .env file so that your API Key is not stored in source control.

Expand Down Expand Up @@ -136,7 +137,7 @@ def create_tailored_plain_resume(resume: str, job_description: str, model=AIMode
logger.debug(f"The tailored resume plain text is: {tailored_resume}")
return tailored_resume

def covert_plain_resume_to_latex(current_time: str, company_name: str, plain_resume: str, model=AIModel.gpt_4o_mini, template=ResumeTemplate.Blue_Modern_Resume):
def covert_plain_resume_to_latex(time: str, company_name: str, plain_resume: str, model=AIModel.gpt_4o_mini, template=ResumeTemplate.Blue_Modern_Resume):

messages=[
{"role": "system", "content": "You are a helpful assistant."},
Expand All @@ -152,10 +153,8 @@ def covert_plain_resume_to_latex(current_time: str, company_name: str, plain_res
tailored_resume = json.loads(completion.choices[0].message.content)["tailored_resume"]
logger.debug(f"The tailored resume Latex code in iteration {i} is: {tailored_resume}")
trimed_tailored_resume = tailored_resume[tailored_resume.find(r"\documentclass"):tailored_resume.rfind(r"\end{document}")+len(r"\end{document}")] # removes possible extra things that AI adds
created_tar_file = generate_tex_and_tar(current_time, company_name, trimed_tailored_resume, TEX_FILE_NAME, TAR_FOLDER_NAME)
with open(created_tar_file, 'rb') as tar_file:
files = {'file':(os.path.basename(created_tar_file), tar_file, "application/x-tar")}
latex_compiler_response = requests.post(url=LaTeX_COMPILER_URL_DATA.format(tex_folder_path=f"{TAR_FOLDER_NAME}/{TEX_FILE_NAME}.tex", compiler=Template_Details[template]['compiler']), files= files)
compiler = Template_Details[template]['compiler']
latex_compiler_response = generate_pdf_from_latex(time, company_name, trimed_tailored_resume, compiler)
logger.debug(f"Request url to the LaTeX compiler is: {latex_compiler_response.url}")
if not b"error: " in latex_compiler_response.content: # there is no error in the compiled code
return latex_compiler_response, trimed_tailored_resume
Expand Down Expand Up @@ -192,4 +191,4 @@ def generate_answer_questions(resume: str, job_description: str, question: str,
],
response_format=TailoredAnswer
)
return json.loads(completion.choices[0].message.content)["tailored_answer"]
return json.loads(completion.choices[0].message.content)["tailored_answer"]
44 changes: 26 additions & 18 deletions utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import os
import requests
import tarfile
import openai_wrapper
from log import logger
from datetime import datetime
from models.tailoring_options import TailoringOptions
from config import APPLICANT_NAME
from config import TEX_FILE_NAME, TAR_FOLDER_NAME
from envs import LaTeX_COMPILER_URL_DATA

def generate_tex_and_tar(current_time: str, company_name: str, latex_content: str, file_name: str= "resume", folder_name: str="resume"):
def generate_tex_and_tar(time: str, company_name: str, latex_content: str, file_name: str= "resume", folder_name: str="resume"):
"""
Creates a folder, generates a .tex file inside it, and compresses the folder into a .tar file.
Expand All @@ -17,10 +20,10 @@ def generate_tex_and_tar(current_time: str, company_name: str, latex_content: st
"""
try:
# Path of a folder for saving .tex files
resume_folder_path = f'./Resumes/{current_time}_{company_name}'
resume_folder_path = f'./Resumes/{time}_{company_name}'

# Path of .tar file
tar_path = f'./CVs/{current_time}_{company_name}'
tar_path = f'./Resumes/{time}_{company_name}'

# Ensure the folder exists
os.makedirs(resume_folder_path, exist_ok=True)
Expand Down Expand Up @@ -48,19 +51,24 @@ def generate_tex_and_tar(current_time: str, company_name: str, latex_content: st
except Exception as e:
logger.debug(f"An error occurred: {e}")

def generate_pdf(company_name: str, tailored_plain_resume: str, tailoring_options: TailoringOptions):
# Get the current time formatted as YYYY-MM-DD_HH-MM-SS
current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")

# Make a folder for each job description to save PDF, .tex, and .tar files of tailored resume.
os.makedirs(f'./Resumes/{current_time}_{company_name}', exist_ok=True)
def generate_pdf_from_latex(time, company_name, latex_code, compiler):
"""
generate pdf file from latex code
"""
tar_file = generate_tex_and_tar(time, company_name, latex_code, TEX_FILE_NAME, TAR_FOLDER_NAME)
with open(tar_file, 'rb') as tar_file:
files = {'file':(os.path.basename(tar_file.name), tar_file, "application/x-tar")}
latex_compiler_response = requests.post(url=LaTeX_COMPILER_URL_DATA.format(tex_folder_path=f"{TAR_FOLDER_NAME}/{TEX_FILE_NAME}.tex", compiler=compiler), files= files)
return latex_compiler_response

latex_compiler_response, _ = openai_wrapper.covert_plain_resume_to_latex(
current_time, company_name, tailored_plain_resume, tailoring_options.ai_model, tailoring_options.resume_template
)
# Path to save pdf file of tailored resume
pdf_path = f'./Resumes/{current_time}_{company_name}/{APPLICANT_NAME}_resume.pdf'
with open(pdf_path, 'wb') as f:
f.write(latex_compiler_response.content)
logger.debug(f"Generated resume saved at here: {pdf_path}")
return pdf_path
def save_pdf(pdf_path, pdf_file):
"""
Save pdf file in pdf_path
"""
os.makedirs(pdf_path,exist_ok=True)
file_name = f"{APPLICANT_NAME}_cv.pdf"
pdf_file_path = os.path.join(pdf_path,file_name)
with open(pdf_file_path,'wb') as f:
f.write(pdf_file)
logger.debug(f"Generated pdf saved at here: {pdf_file_path}")
return pdf_file_path

0 comments on commit c3ddf83

Please sign in to comment.