File size: 5,916 Bytes
56c7b6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import os
import docx
from docxtpl import DocxTemplate
from typing import TypedDict, List
from pydantic import BaseModel, Field
from langchain_google_genai import ChatGoogleGenerativeAI
from langgraph.graph import StateGraph, START, END
import logging

logger = logging.getLogger(__name__)

# --- Define State & Schemas ---
class ResumeState(TypedDict):
    original_resume: str
    job_description: str
    resume_skills: List[str]
    jd_skills: List[str]
    matched_skills: List[str]
    tailored_resume: dict

class SkillList(BaseModel):
    skills: List[str] = Field(description="A list of technical skills, languages, frameworks, and databases")

class ExperienceItem(BaseModel):
    job_title: str = Field(description="The job title")
    company: str = Field(description="The company name")
    duration: str = Field(description="The time period worked")
    bullets: List[str] = Field(description="3-5 bullet points emphasizing the matched skills")

class EducationItem(BaseModel):
    degree: str = Field(description="The name of the degree or certification")
    institution: str = Field(description="The name of the university or institution")
    graduation_date: str = Field(description="The graduation year or timeframe")

class ATSResumeOutput(BaseModel):
    name: str = Field(description="The candidate's full name")
    contact_info: str = Field(description="Email, phone, and links (LinkedIn, GitHub)")
    summary: str = Field(description="A professional summary emphasizing matched skills")
    skills: List[str] = Field(description="The optimized list of relevant skills")
    experience: List[ExperienceItem] = Field(description="The candidate's work history")
    education: List[EducationItem] = Field(description="The candidate's educational background")

class AILangGraphTailor:
    def __init__(self):
        # Initialize Gemini 2.5 Flash
        # Assumes GOOGLE_API_KEY is loaded in the environment 
        self.llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=0.2)
        self.app = self._build_graph()

    def extract_text_from_docx(self, file_path: str) -> str:
        """Reads a .docx file and returns all text as a single string."""
        try:
            doc = docx.Document(file_path)
            full_text = [para.text for para in doc.paragraphs if para.text.strip()]
            return "\n".join(full_text)
        except Exception as e:
            logger.error(f"Error reading document: {e}")
            raise

    def generate_final_docx(self, tailored_dict: dict, template_path: str, output_path: str):
        """Takes the JSON dictionary from Gemini and renders the final Word document using docxtpl."""
        try:
            doc = DocxTemplate(template_path)
            doc.render(tailored_dict)
            doc.save(output_path)
            logger.info(f"Success! Tailored document saved to {output_path}.")
        except Exception as e:
            logger.error(f"Error rendering docxtpl: {e}")
            raise

    # --- Node Functions ---
    def extract_resume_skills(self, state: ResumeState):
        structured_llm = self.llm.with_structured_output(SkillList)
        prompt = f"Extract all technical skills from this resume. Return only the skills.\n\nResume:\n{state['original_resume']}"
        return {"resume_skills": structured_llm.invoke(prompt).skills}

    def extract_jd_skills(self, state: ResumeState):
        structured_llm = self.llm.with_structured_output(SkillList)
        prompt = f"Extract all required technical skills from this job description. Return only the skills.\n\nJob Description:\n{state['job_description']}"
        return {"jd_skills": structured_llm.invoke(prompt).skills}

    def match_skills(self, state: ResumeState):
        resume_set = {s.strip().lower() for s in state['resume_skills']}
        jd_set = {s.strip().lower() for s in state['jd_skills']}
        matches = list(resume_set.intersection(jd_set))
        return {"matched_skills": matches}

    def tailor_resume(self, state: ResumeState):
        matched_skills_str = ", ".join(state['matched_skills'])
        structured_llm = self.llm.with_structured_output(ATSResumeOutput)
        prompt = f"""
        You are a professional resume writer. Your task is to rewrite the provided 'Original Resume' to strongly emphasize these 'Key Skills to Emphasize': {matched_skills_str}

        RULES:
        1. Rephrase the summary and experience bullet points to highlight the key skills, and do not use buzzwords while writing any Summary and experience. Keep everything simple.
        2. You MUST NOT add any new skills, projects, experiences, or education that are not in the Original Resume.
        3. Ensure the output is a professional, complete resume.

        Original Resume:
        ---
        {state['original_resume']}
        ---
        """
        tailored_data = structured_llm.invoke(prompt)
        return {"tailored_resume": tailored_data.model_dump()}

    def _build_graph(self):
        workflow = StateGraph(ResumeState)
        workflow.add_node("extract_resume", self.extract_resume_skills)
        workflow.add_node("extract_jd", self.extract_jd_skills)
        workflow.add_node("match_skills", self.match_skills)
        workflow.add_node("tailor_resume", self.tailor_resume)

        workflow.add_edge(START, "extract_resume")
        workflow.add_edge("extract_resume", "extract_jd")
        workflow.add_edge("extract_jd", "match_skills")
        workflow.add_edge("match_skills", "tailor_resume")
        workflow.add_edge("tailor_resume", END)
        return workflow.compile()

    def run_pipeline(self, original_resume_text: str, job_description: str) -> dict:
        """Executes the LangGraph pipeline."""
        initial_state = {
            "original_resume": original_resume_text,
            "job_description": job_description
        }
        return self.app.invoke(initial_state)