Spaces:
Sleeping
Sleeping
changes
Browse files- app/graph.py +7 -1
- app/main.py +3 -5
- app/nodes/graphnodes.py +62 -61
- app/schemas/resume_extract_schema.py +1 -35
- app/schemas/skill_gap_analysis_schema.py +1 -2
- app/{graph_trial.py → test.py} +16 -4
app/graph.py
CHANGED
|
@@ -2,15 +2,21 @@ from app.state.state import OnboardingState
|
|
| 2 |
from app.nodes.graphnodes import *
|
| 3 |
from langgraph.prebuilt import ToolNode ,tools_condition
|
| 4 |
from langgraph.graph import StateGraph,END,START
|
|
|
|
| 5 |
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
builder = StateGraph(OnboardingState)
|
| 9 |
|
| 10 |
# Define Nodes
|
| 11 |
builder.add_node("input_node", input_node)
|
| 12 |
builder.add_node("resume_data_extraction", extractResumeDataNode)
|
| 13 |
-
builder.add_node("jd_data_extraction", extractJDDataNode)
|
| 14 |
builder.add_node("skill_gap_analysis", skill_gap_node)
|
| 15 |
|
| 16 |
# The ReAct Agent Node
|
|
|
|
| 2 |
from app.nodes.graphnodes import *
|
| 3 |
from langgraph.prebuilt import ToolNode ,tools_condition
|
| 4 |
from langgraph.graph import StateGraph,END,START
|
| 5 |
+
from langgraph.types import RetryPolicy
|
| 6 |
|
| 7 |
|
| 8 |
+
node_retry = RetryPolicy(
|
| 9 |
+
max_attempts=3,
|
| 10 |
+
initial_interval=1.5,
|
| 11 |
+
retry_on=[ConnectionError]
|
| 12 |
+
)
|
| 13 |
|
| 14 |
builder = StateGraph(OnboardingState)
|
| 15 |
|
| 16 |
# Define Nodes
|
| 17 |
builder.add_node("input_node", input_node)
|
| 18 |
builder.add_node("resume_data_extraction", extractResumeDataNode)
|
| 19 |
+
builder.add_node("jd_data_extraction", extractJDDataNode ,retry_policy=node_retry)
|
| 20 |
builder.add_node("skill_gap_analysis", skill_gap_node)
|
| 21 |
|
| 22 |
# The ReAct Agent Node
|
app/main.py
CHANGED
|
@@ -5,7 +5,6 @@ from app.utils.ui_payload_constructor import UIPayload
|
|
| 5 |
from app.graph import graph
|
| 6 |
from app.utils.cloudinary_utils import get_resume_url
|
| 7 |
|
| 8 |
-
|
| 9 |
app = FastAPI(title="Adaptive Onboarding Engine")
|
| 10 |
|
| 11 |
app.add_middleware(
|
|
@@ -15,7 +14,6 @@ app.add_middleware(
|
|
| 15 |
allow_headers=["*"],
|
| 16 |
)
|
| 17 |
|
| 18 |
-
|
| 19 |
@app.post("/analyze")
|
| 20 |
async def analyze(
|
| 21 |
user_id: str = Form(..., description="User ID — used to fetch resume from Cloudinary"),
|
|
@@ -68,6 +66,6 @@ def health():
|
|
| 68 |
return {"status": "ok", "service": "Adaptive Onboarding Engine"}
|
| 69 |
|
| 70 |
|
| 71 |
-
if __name__ == "__main__":
|
| 72 |
-
|
| 73 |
-
|
|
|
|
| 5 |
from app.graph import graph
|
| 6 |
from app.utils.cloudinary_utils import get_resume_url
|
| 7 |
|
|
|
|
| 8 |
app = FastAPI(title="Adaptive Onboarding Engine")
|
| 9 |
|
| 10 |
app.add_middleware(
|
|
|
|
| 14 |
allow_headers=["*"],
|
| 15 |
)
|
| 16 |
|
|
|
|
| 17 |
@app.post("/analyze")
|
| 18 |
async def analyze(
|
| 19 |
user_id: str = Form(..., description="User ID — used to fetch resume from Cloudinary"),
|
|
|
|
| 66 |
return {"status": "ok", "service": "Adaptive Onboarding Engine"}
|
| 67 |
|
| 68 |
|
| 69 |
+
# if __name__ == "__main__":
|
| 70 |
+
# import uvicorn
|
| 71 |
+
# uvicorn.run(app, host="127.0.0.1", port=8000)
|
app/nodes/graphnodes.py
CHANGED
|
@@ -1,22 +1,25 @@
|
|
| 1 |
from app.state.state import OnboardingState
|
| 2 |
-
from langchain_core.messages import SystemMessage, HumanMessage,ToolMessage,AIMessage
|
| 3 |
from app.prompts.resume_agent_prompt import resume_agent_prompt
|
| 4 |
from app.prompts.jd_agent_prompt import jd_agent_prompt
|
| 5 |
from app.prompts.roadmap_planner_agent_prompt import roadmap_planner_agent_prompt
|
| 6 |
-
from app.agents.agents import resume_agent,jd_agent,roadmap_planner_agent,gap_analysis_agent
|
| 7 |
from app.prompts.gap_analysis_agent_prompt import gap_analysis_agent_prompt
|
| 8 |
import json
|
|
|
|
| 9 |
from app.tools.tools import *
|
| 10 |
from langchain_community.document_loaders import PyMuPDFLoader
|
| 11 |
-
from langgraph.prebuilt import ToolNode
|
| 12 |
from app.schemas.jd_extract_schema import JobDescriptionExtract
|
| 13 |
from app.schemas.resume_extract_schema import ResumeExtract
|
| 14 |
from app.schemas.skill_gap_analysis_schema import SkillGapAnalysis
|
| 15 |
|
|
|
|
| 16 |
|
| 17 |
-
def input_node(state: OnboardingState):
|
| 18 |
|
| 19 |
-
|
|
|
|
|
|
|
| 20 |
|
| 21 |
if not file_path:
|
| 22 |
return {"extraction_error": "Missing file_path in state"}
|
|
@@ -24,8 +27,6 @@ def input_node(state: OnboardingState):
|
|
| 24 |
try:
|
| 25 |
loader = PyMuPDFLoader(file_path)
|
| 26 |
docs = loader.load()
|
| 27 |
-
|
| 28 |
-
|
| 29 |
resume_text = "\n".join([doc.page_content for doc in docs])
|
| 30 |
|
| 31 |
return {
|
|
@@ -34,13 +35,14 @@ def input_node(state: OnboardingState):
|
|
| 34 |
}
|
| 35 |
|
| 36 |
except Exception as e:
|
|
|
|
| 37 |
return {
|
| 38 |
"extraction_error": f"Failed to load resume: {str(e)}"
|
| 39 |
}
|
| 40 |
-
|
| 41 |
|
| 42 |
def extractResumeDataNode(state: OnboardingState):
|
| 43 |
-
|
| 44 |
resume_text = state["resume_text"]
|
| 45 |
|
| 46 |
messages = [
|
|
@@ -48,105 +50,104 @@ def extractResumeDataNode(state: OnboardingState):
|
|
| 48 |
HumanMessage(content=f"<resume_text>{resume_text}</resume_text>")
|
| 49 |
]
|
| 50 |
|
| 51 |
-
|
| 52 |
result = resume_agent.invoke(messages)
|
| 53 |
|
| 54 |
return {"resume_data": result["parsed"]}
|
| 55 |
|
| 56 |
|
| 57 |
def extractJDDataNode(state: OnboardingState):
|
| 58 |
-
|
| 59 |
jd_text = state.get("job_description", "")
|
| 60 |
-
|
| 61 |
if not jd_text or len(jd_text.strip()) < 5:
|
| 62 |
-
|
| 63 |
return {"JobDescriptionExtract_data": JobDescriptionExtract()}
|
| 64 |
|
| 65 |
-
|
| 66 |
|
| 67 |
messages = [
|
| 68 |
SystemMessage(content=jd_agent_prompt),
|
| 69 |
-
HumanMessage(content=f"
|
| 70 |
]
|
| 71 |
|
| 72 |
try:
|
| 73 |
-
# 2. Invoke the agent
|
| 74 |
result = jd_agent.invoke(messages)
|
| 75 |
-
|
| 76 |
-
# 3. Handle the 'parsed' key (ensure your chain is configured correctly)
|
| 77 |
-
# If result is already the Pydantic object, use it directly.
|
| 78 |
-
# If result is a dict with 'parsed', use result['parsed'].
|
| 79 |
parsed_data = result.get("parsed") if isinstance(result, dict) else result
|
| 80 |
|
| 81 |
-
# 4. Critical Check: Did it actually find anything?
|
| 82 |
if parsed_data.job_title is None and parsed_data.tools_technologies is None:
|
| 83 |
-
|
| 84 |
else:
|
| 85 |
-
|
| 86 |
|
| 87 |
return {"JobDescriptionExtract_data": parsed_data}
|
| 88 |
-
|
| 89 |
except Exception as e:
|
| 90 |
-
|
| 91 |
return {"JobDescriptionExtract_data": JobDescriptionExtract()}
|
| 92 |
-
|
| 93 |
-
|
| 94 |
|
| 95 |
|
| 96 |
def skill_gap_node(state: OnboardingState):
|
| 97 |
-
|
| 98 |
-
resume_data = state["resume_data"]
|
| 99 |
-
candidate_name = state
|
| 100 |
-
|
| 101 |
-
# To remove noise and reduce size of the prompt.
|
| 102 |
-
lean_resume_dict = resume_data.model_dump(
|
| 103 |
-
exclude_none=True # Bonus: Automatically drops any fields that are None/null!
|
| 104 |
-
)
|
| 105 |
-
|
| 106 |
-
raw_jd = state["JobDescriptionExtract_data"]
|
| 107 |
-
|
| 108 |
-
# Strip the HR noise and text bloat
|
| 109 |
-
lean_jd_dict = raw_jd.model_dump(
|
| 110 |
-
exclude_none=True # Drops any null fields
|
| 111 |
-
)
|
| 112 |
-
|
| 113 |
-
lean_resume_json = json.dumps(lean_resume_dict, indent=2)
|
| 114 |
|
|
|
|
|
|
|
|
|
|
| 115 |
|
|
|
|
|
|
|
| 116 |
lean_jd_json = json.dumps(lean_jd_dict, indent=2)
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
messages = [
|
| 119 |
SystemMessage(content=gap_analysis_agent_prompt),
|
| 120 |
-
HumanMessage(content=
|
| 121 |
-
|
| 122 |
]
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
return {"skill_gap_analysis_data": result["parsed"]}
|
| 128 |
-
|
| 129 |
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
|
| 132 |
def roadmap_planning_node(state: OnboardingState):
|
| 133 |
"""
|
| 134 |
-
|
| 135 |
-
decides which
|
| 136 |
"""
|
| 137 |
skill_gap_data = state["skill_gap_analysis_data"]
|
| 138 |
|
| 139 |
-
|
|
|
|
|
|
|
| 140 |
|
| 141 |
system_prompt = SystemMessage(content=roadmap_planner_agent_prompt)
|
| 142 |
-
input_msg = HumanMessage(content=f"<
|
| 143 |
-
|
| 144 |
-
response = roadmap_planner_agent.invoke([system_prompt, input_msg] + state["messages"])
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
|
| 152 |
-
|
|
|
|
|
|
| 1 |
from app.state.state import OnboardingState
|
| 2 |
+
from langchain_core.messages import SystemMessage, HumanMessage, ToolMessage, AIMessage
|
| 3 |
from app.prompts.resume_agent_prompt import resume_agent_prompt
|
| 4 |
from app.prompts.jd_agent_prompt import jd_agent_prompt
|
| 5 |
from app.prompts.roadmap_planner_agent_prompt import roadmap_planner_agent_prompt
|
| 6 |
+
from app.agents.agents import resume_agent, jd_agent, roadmap_planner_agent, gap_analysis_agent, roadmap_planner_agent_tools
|
| 7 |
from app.prompts.gap_analysis_agent_prompt import gap_analysis_agent_prompt
|
| 8 |
import json
|
| 9 |
+
import logging
|
| 10 |
from app.tools.tools import *
|
| 11 |
from langchain_community.document_loaders import PyMuPDFLoader
|
| 12 |
+
from langgraph.prebuilt import ToolNode, tools_condition
|
| 13 |
from app.schemas.jd_extract_schema import JobDescriptionExtract
|
| 14 |
from app.schemas.resume_extract_schema import ResumeExtract
|
| 15 |
from app.schemas.skill_gap_analysis_schema import SkillGapAnalysis
|
| 16 |
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
|
|
|
|
| 19 |
|
| 20 |
+
def input_node(state: OnboardingState):
|
| 21 |
+
"""Load and extract text from resume PDF."""
|
| 22 |
+
file_path = state.get("file_path")
|
| 23 |
|
| 24 |
if not file_path:
|
| 25 |
return {"extraction_error": "Missing file_path in state"}
|
|
|
|
| 27 |
try:
|
| 28 |
loader = PyMuPDFLoader(file_path)
|
| 29 |
docs = loader.load()
|
|
|
|
|
|
|
| 30 |
resume_text = "\n".join([doc.page_content for doc in docs])
|
| 31 |
|
| 32 |
return {
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
except Exception as e:
|
| 38 |
+
logger.error(f"Failed to load resume: {str(e)}")
|
| 39 |
return {
|
| 40 |
"extraction_error": f"Failed to load resume: {str(e)}"
|
| 41 |
}
|
| 42 |
+
|
| 43 |
|
| 44 |
def extractResumeDataNode(state: OnboardingState):
|
| 45 |
+
"""Extract structured resume data using resume agent."""
|
| 46 |
resume_text = state["resume_text"]
|
| 47 |
|
| 48 |
messages = [
|
|
|
|
| 50 |
HumanMessage(content=f"<resume_text>{resume_text}</resume_text>")
|
| 51 |
]
|
| 52 |
|
|
|
|
| 53 |
result = resume_agent.invoke(messages)
|
| 54 |
|
| 55 |
return {"resume_data": result["parsed"]}
|
| 56 |
|
| 57 |
|
| 58 |
def extractJDDataNode(state: OnboardingState):
|
| 59 |
+
"""Extract structured job description data using JD agent."""
|
| 60 |
jd_text = state.get("job_description", "")
|
| 61 |
+
|
| 62 |
if not jd_text or len(jd_text.strip()) < 5:
|
| 63 |
+
logger.warning("job_description text is missing from state")
|
| 64 |
return {"JobDescriptionExtract_data": JobDescriptionExtract()}
|
| 65 |
|
| 66 |
+
logger.info(f"Extracting JD from {len(jd_text)} characters")
|
| 67 |
|
| 68 |
messages = [
|
| 69 |
SystemMessage(content=jd_agent_prompt),
|
| 70 |
+
HumanMessage(content=f"<job_description>{jd_text}</job_description>")
|
| 71 |
]
|
| 72 |
|
| 73 |
try:
|
|
|
|
| 74 |
result = jd_agent.invoke(messages)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
parsed_data = result.get("parsed") if isinstance(result, dict) else result
|
| 76 |
|
|
|
|
| 77 |
if parsed_data.job_title is None and parsed_data.tools_technologies is None:
|
| 78 |
+
logger.warning("JD extraction returned empty schema")
|
| 79 |
else:
|
| 80 |
+
logger.info(f"Successfully extracted job title: {parsed_data.job_title}")
|
| 81 |
|
| 82 |
return {"JobDescriptionExtract_data": parsed_data}
|
| 83 |
+
|
| 84 |
except Exception as e:
|
| 85 |
+
logger.error(f"JD extraction failed: {str(e)}")
|
| 86 |
return {"JobDescriptionExtract_data": JobDescriptionExtract()}
|
|
|
|
|
|
|
| 87 |
|
| 88 |
|
| 89 |
def skill_gap_node(state: OnboardingState):
|
| 90 |
+
"""Analyze skill gaps between resume and job description."""
|
| 91 |
+
resume_data = state["resume_data"]
|
| 92 |
+
candidate_name = state.get("candidate_name", "Candidate")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
+
# Convert Pydantic models to lean dicts (exclude None values)
|
| 95 |
+
lean_resume_dict = resume_data.model_dump(exclude_none=True)
|
| 96 |
+
lean_jd_dict = state["JobDescriptionExtract_data"].model_dump(exclude_none=True)
|
| 97 |
|
| 98 |
+
# Serialize to JSON
|
| 99 |
+
lean_resume_json = json.dumps(lean_resume_dict, indent=2)
|
| 100 |
lean_jd_json = json.dumps(lean_jd_dict, indent=2)
|
| 101 |
|
| 102 |
+
# Clean prompt with proper formatting
|
| 103 |
+
prompt_text = f"""Analyze the skill gaps for the following candidate:
|
| 104 |
+
|
| 105 |
+
Candidate Name: {candidate_name}
|
| 106 |
+
|
| 107 |
+
Resume:
|
| 108 |
+
{lean_resume_json}
|
| 109 |
+
|
| 110 |
+
Job Description:
|
| 111 |
+
{lean_jd_json}
|
| 112 |
+
|
| 113 |
+
Please provide a detailed skill gap analysis."""
|
| 114 |
+
|
| 115 |
messages = [
|
| 116 |
SystemMessage(content=gap_analysis_agent_prompt),
|
| 117 |
+
HumanMessage(content=prompt_text)
|
|
|
|
| 118 |
]
|
| 119 |
|
| 120 |
+
try:
|
| 121 |
+
result = gap_analysis_agent.invoke(messages)
|
| 122 |
+
return {"skill_gap_analysis_data": result["parsed"]}
|
|
|
|
|
|
|
| 123 |
|
| 124 |
+
except Exception as e:
|
| 125 |
+
logger.error(f"Skill gap analysis failed: {str(e)}")
|
| 126 |
+
return {"skill_gap_analysis_data": SkillGapAnalysis()}
|
| 127 |
|
| 128 |
|
| 129 |
def roadmap_planning_node(state: OnboardingState):
|
| 130 |
"""
|
| 131 |
+
Plan learning roadmap based on skill gaps.
|
| 132 |
+
This node decides which tools to call next based on the analysis.
|
| 133 |
"""
|
| 134 |
skill_gap_data = state["skill_gap_analysis_data"]
|
| 135 |
|
| 136 |
+
# Convert Pydantic model to dict, then to JSON
|
| 137 |
+
skill_gap_dict = skill_gap_data.model_dump()
|
| 138 |
+
skill_gap_json = json.dumps(skill_gap_dict, indent=2)
|
| 139 |
|
| 140 |
system_prompt = SystemMessage(content=roadmap_planner_agent_prompt)
|
| 141 |
+
input_msg = HumanMessage(content=f"<skill_gap_analysis>\n{skill_gap_json}\n</skill_gap_analysis>")
|
|
|
|
|
|
|
| 142 |
|
| 143 |
+
try:
|
| 144 |
+
response = roadmap_planner_agent.invoke([system_prompt, input_msg] + state.get("messages", []))
|
| 145 |
+
return {"messages": [response]}
|
| 146 |
|
| 147 |
+
except Exception as e:
|
| 148 |
+
logger.error(f"Roadmap planning failed: {str(e)}")
|
| 149 |
+
return {"messages": [AIMessage(content=f"Error in roadmap planning: {str(e)}")]}
|
| 150 |
|
| 151 |
|
| 152 |
+
# Initialize tool node for roadmap planner
|
| 153 |
+
tool_node = ToolNode(roadmap_planner_agent_tools)
|
app/schemas/resume_extract_schema.py
CHANGED
|
@@ -8,22 +8,6 @@ class Skill(BaseModel):
|
|
| 8 |
)
|
| 9 |
|
| 10 |
|
| 11 |
-
class ExperienceItem(BaseModel):
|
| 12 |
-
job_title: str = Field(
|
| 13 |
-
...,
|
| 14 |
-
description="Role title of the candidate. Example: 'Backend Intern', 'Software Engineer'"
|
| 15 |
-
)
|
| 16 |
-
|
| 17 |
-
technologies: Optional[List[str]] = Field(
|
| 18 |
-
default_factory=list,
|
| 19 |
-
description="Technologies, tools, or frameworks used in this role"
|
| 20 |
-
)
|
| 21 |
-
|
| 22 |
-
responsibilities: Optional[List[str]] = Field(
|
| 23 |
-
default_factory=list,
|
| 24 |
-
description="Key responsibilities, tasks, or learnings in concise bullet points keep it summarised detail *not* required"
|
| 25 |
-
)
|
| 26 |
-
|
| 27 |
class ProjectItem(BaseModel):
|
| 28 |
name: str = Field(..., description="Project name")
|
| 29 |
technologies: List[str] = Field(
|
|
@@ -52,32 +36,14 @@ class ResumeExtract(BaseModel):
|
|
| 52 |
)
|
| 53 |
)
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
skills: List[Skill] = Field(
|
| 59 |
default_factory=list,
|
| 60 |
description="Skills explicitly listed by the candidate"
|
| 61 |
)
|
| 62 |
-
|
| 63 |
-
default_factory=list,
|
| 64 |
-
description=(
|
| 65 |
-
"Each role as a separate entry. "
|
| 66 |
-
"No company name needed — focus on what was done and learned."
|
| 67 |
-
)
|
| 68 |
-
)
|
| 69 |
projects: List[ProjectItem] = Field(
|
| 70 |
default_factory=list,
|
| 71 |
description="Projects with technologies used and what was built"
|
| 72 |
)
|
| 73 |
|
| 74 |
|
| 75 |
-
|
| 76 |
-
is_fresher: bool = Field(
|
| 77 |
-
...,
|
| 78 |
-
description=(
|
| 79 |
-
"Set to True if the candidate lacks full-time professional employment. "
|
| 80 |
-
"Academic projects, certifications, and internships are considered "
|
| 81 |
-
"part of the learning phase and do not qualify a candidate as 'non-fresher' hence is_."
|
| 82 |
-
)
|
| 83 |
-
)
|
|
|
|
| 8 |
)
|
| 9 |
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
class ProjectItem(BaseModel):
|
| 12 |
name: str = Field(..., description="Project name")
|
| 13 |
technologies: List[str] = Field(
|
|
|
|
| 36 |
)
|
| 37 |
)
|
| 38 |
|
|
|
|
|
|
|
|
|
|
| 39 |
skills: List[Skill] = Field(
|
| 40 |
default_factory=list,
|
| 41 |
description="Skills explicitly listed by the candidate"
|
| 42 |
)
|
| 43 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
projects: List[ProjectItem] = Field(
|
| 45 |
default_factory=list,
|
| 46 |
description="Projects with technologies used and what was built"
|
| 47 |
)
|
| 48 |
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/schemas/skill_gap_analysis_schema.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import List, Optional, Literal
|
|
| 5 |
class SkillGap(BaseModel):
|
| 6 |
skill_name: str = Field(
|
| 7 |
...,
|
| 8 |
-
description="The specific
|
| 9 |
)
|
| 10 |
|
| 11 |
gap_type: Literal["missing_foundation", "needs_advanced_upgrade"] = Field(
|
|
@@ -24,7 +24,6 @@ class SkillGap(BaseModel):
|
|
| 24 |
reasoning: str = Field(
|
| 25 |
...,
|
| 26 |
description=(
|
| 27 |
-
"The 'Reasoning Trace'. This MUST be provided for every skill gap identified. "
|
| 28 |
"Explain exactly WHY this gap was flagged based on the resume vs JD comparison. "
|
| 29 |
"Example: 'JD requires FastAPI; candidate has Python experience but no record of using FastAPI framework.'"
|
| 30 |
)
|
|
|
|
| 5 |
class SkillGap(BaseModel):
|
| 6 |
skill_name: str = Field(
|
| 7 |
...,
|
| 8 |
+
description="The specific tool missing or requiring an upgrade (e.g., 'Docker')"
|
| 9 |
)
|
| 10 |
|
| 11 |
gap_type: Literal["missing_foundation", "needs_advanced_upgrade"] = Field(
|
|
|
|
| 24 |
reasoning: str = Field(
|
| 25 |
...,
|
| 26 |
description=(
|
|
|
|
| 27 |
"Explain exactly WHY this gap was flagged based on the resume vs JD comparison. "
|
| 28 |
"Example: 'JD requires FastAPI; candidate has Python experience but no record of using FastAPI framework.'"
|
| 29 |
)
|
app/{graph_trial.py → test.py}
RENAMED
|
@@ -33,9 +33,7 @@ initial_input = {
|
|
| 33 |
"resume_data": None,
|
| 34 |
"extraction_error": None,
|
| 35 |
"JobDescriptionExtract_data": None,
|
| 36 |
-
"skill_gap_analysis_data": None
|
| 37 |
-
|
| 38 |
-
|
| 39 |
}
|
| 40 |
|
| 41 |
|
|
@@ -47,7 +45,21 @@ config = {"configurable": {"thread_id": THREAD_ID}}
|
|
| 47 |
# final_result = graph.invoke(initial_input, config=config)
|
| 48 |
|
| 49 |
|
| 50 |
-
# print(final_result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
|
| 53 |
|
|
|
|
| 33 |
"resume_data": None,
|
| 34 |
"extraction_error": None,
|
| 35 |
"JobDescriptionExtract_data": None,
|
| 36 |
+
"skill_gap_analysis_data": None
|
|
|
|
|
|
|
| 37 |
}
|
| 38 |
|
| 39 |
|
|
|
|
| 45 |
# final_result = graph.invoke(initial_input, config=config)
|
| 46 |
|
| 47 |
|
| 48 |
+
# print(final_result['resume_data'])
|
| 49 |
+
|
| 50 |
+
# print()
|
| 51 |
+
|
| 52 |
+
# print(final_result['JobDescriptionExtract_data'])
|
| 53 |
+
|
| 54 |
+
# print()
|
| 55 |
+
|
| 56 |
+
# print(final_result['skill_gap_analysis_data'])
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# print()
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# print(final_result['final_roadmap'])
|
| 63 |
|
| 64 |
|
| 65 |
|