from app.state.state import OnboardingState from langchain_core.messages import SystemMessage, HumanMessage,ToolMessage,AIMessage from app.prompts.resume_agent_prompt import resume_agent_prompt from app.prompts.jd_agent_prompt import jd_agent_prompt from app.prompts.roadmap_planner_agent_prompt import roadmap_planner_agent_prompt from app.agents.agents import resume_agent,jd_agent,roadmap_planner_agent,gap_analysis_agent from app.prompts.gap_analysis_agent_prompt import gap_analysis_agent_prompt from app.schemas.pydanticschema import ResumeExtract,JobDescriptionExtract,SkillGapAnalysis import json from app.tools.tools import * from langchain_community.document_loaders import PyMuPDFLoader from langgraph.prebuilt import ToolNode ,tools_condition def input_node(state: OnboardingState): file_path = state.get("file_path") if not file_path: return {"extraction_error": "Missing file_path in state"} try: loader = PyMuPDFLoader(file_path) docs = loader.load() resume_text = "\n".join([doc.page_content for doc in docs]) return { "resume_text": resume_text, "extraction_error": None } except Exception as e: return { "extraction_error": f"Failed to load resume: {str(e)}" } def extractResumeDataNode(state: OnboardingState): resume_text = state["resume_text"] messages = [ SystemMessage(content=resume_agent_prompt), HumanMessage(content=f"{resume_text}") ] result = resume_agent.invoke(messages) return {"resume_data": result["parsed"]} def extractJDDataNode(state: OnboardingState): # 1. Safety Check: Is the text even in the state? jd_text = state.get("job_description", "") if not jd_text or len(jd_text.strip()) < 5: print("DEBUGGER ERROR: job_description text is MISSING from state!") return {"JobDescriptionExtract_data": JobDescriptionExtract()} print(f"DEBUGGER: Sending {len(jd_text)} characters to JD Agent...") messages = [ SystemMessage(content=jd_agent_prompt), HumanMessage(content=f"EXTRACT FROM THIS TEXT:\n\n{jd_text}") ] try: # 2. Invoke the agent result = jd_agent.invoke(messages) # 3. Handle the 'parsed' key (ensure your chain is configured correctly) # If result is already the Pydantic object, use it directly. # If result is a dict with 'parsed', use result['parsed']. parsed_data = result.get("parsed") if isinstance(result, dict) else result # 4. Critical Check: Did it actually find anything? if parsed_data.job_title is None and parsed_data.tools_technologies is None: print("DEBUGGER WARNING: LLM returned empty schema! Checking prompt...") else: print(f"DEBUGGER SUCCESS: Extracted {parsed_data.job_title}") return {"JobDescriptionExtract_data": parsed_data} except Exception as e: print(f"DEBUGGER CRITICAL: Invoke failed: {str(e)}") return {"JobDescriptionExtract_data": JobDescriptionExtract()} def skill_gap_node(state: OnboardingState): resume_data = state["resume_data"] candidate_name = state["candidate_name"] # To remove noise and reduce size of the prompt. lean_resume_dict = resume_data.model_dump( exclude_none=True # Bonus: Automatically drops any fields that are None/null! ) raw_jd = state["JobDescriptionExtract_data"] # Strip the HR noise and text bloat lean_jd_dict = raw_jd.model_dump( exclude_none=True # Drops any null fields ) lean_resume_json = json.dumps(lean_resume_dict, indent=2) lean_jd_json = json.dumps(lean_jd_dict, indent=2) messages = [ SystemMessage(content=gap_analysis_agent_prompt), HumanMessage(content=f"Users Resume:UserName:{candidate_name} Resume:{lean_resume_json} Job Description:{lean_jd_json}"), ] result = gap_analysis_agent.invoke(messages) return {"skill_gap_analysis_data": result["parsed"]} def finalize_state_node(state: OnboardingState): """ Final node that extracts structured data from the message scratchpad and populates the main state keys. No global variables needed! """ final_roadmap = None mermaid_code = None # We search the messages in reverse to find the LATEST tool calls for msg in reversed(state["messages"]): # Check if the message has tool calls (this will be an AIMessage) if hasattr(msg, "tool_calls") and msg.tool_calls: for tool_call in msg.tool_calls: # 1. Extract the Roadmap JSON if tool_call["name"] == "submit_final_roadmap": final_roadmap = tool_call["args"] # 2. Extract the Mermaid String elif tool_call["name"] == "submit_mermaid_visualization": mermaid_code = tool_call["args"].get("mermaid_code") # Once we have both, we can stop searching if final_roadmap and mermaid_code: break return { "final_roadmap": final_roadmap, "mermaid_code": mermaid_code } tool_node = ToolNode(roadmap_planner_agent_tools)