diff --git "a/Notebooks/CodeForge.ipynb" "b/Notebooks/CodeForge.ipynb" --- "a/Notebooks/CodeForge.ipynb" +++ "b/Notebooks/CodeForge.ipynb" @@ -9,7 +9,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 1, @@ -38,10 +38,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "c11377c5", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "❌ Upload failed: [Errno 2] No such file or directory: 'c:\\\\Users\\\\ATHARVA\\\\Downloads\\\\my codes\\\\python\\\\machine_learning\\\\Learning_Files\\\\ChirayuResume.pdf'\n" + ] + } + ], "source": [ "import os\n", "import cloudinary\n", @@ -85,10 +93,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "f986ff8f", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ PDF loaded into memory for processing!\n" + ] + } + ], "source": [ "import requests\n", "from io import BytesIO\n", @@ -113,7 +129,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "id": "938186bf", "metadata": {}, "outputs": [], @@ -145,7 +161,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 5, "id": "f4340cbb", "metadata": {}, "outputs": [ @@ -216,7 +232,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "b010e49b", "metadata": {}, "outputs": [], @@ -224,17 +240,6 @@ "from langchain_community.document_loaders import PyMuPDFLoader" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "4559e021", - "metadata": {}, - "outputs": [], - "source": [ - "loader = PyMuPDFLoader()\n", - "docs = loader.load()" - ] - }, { "cell_type": "code", "execution_count": null, @@ -315,7 +320,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "b75a238a", "metadata": {}, "outputs": [], @@ -372,7 +377,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 140, "id": "7da5b1c6", "metadata": {}, "outputs": [], @@ -398,10 +403,7 @@ " description=\"Type of experience: internship, full_time, contract, or freelance\"\n", " )\n", "\n", - " duration_months: Optional[int] = Field(\n", - " None,\n", - " description=\"Duration of this role in months. Null if not explicitly mentioned\"\n", - " )\n", + "\n", "\n", " technologies: Optional[List[str]] = Field(\n", " default_factory=list,\n", @@ -410,41 +412,32 @@ "\n", " responsibilities: Optional[List[str]] = Field(\n", " default_factory=list,\n", - " description=\"Key responsibilities, tasks, or learnings in concise bullet points\"\n", + " description=\"Key responsibilities, tasks, or learnings in concise bullet points keep it summarised detail *not* required\"\n", " )\n", "\n", "class ProjectItem(BaseModel):\n", " name: str = Field(..., description=\"Project name\")\n", " technologies: List[str] = Field(\n", " default_factory=list,\n", - " description=\"Technologies used in this project\"\n", - " )\n", - " what_was_built: Optional[str] = Field(\n", - " None,\n", - " description=\"One line — what problem it solved or what was built\"\n", + " description=\"Technologies used in this project hence learned during the project.\"\n", " )\n", + " \n", "\n", "\n", "class CertificationItem(BaseModel):\n", " name: str = Field(..., description=\"Certification name\")\n", - " issuer: Optional[str] = Field(None, description=\"Issuing organization\")\n", + " \n", " topics_covered: List[str] = Field(\n", " default_factory=list,\n", " description=\"Key topics or skills the certification covers\"\n", " )\n", "\n", "\n", - "class AchievementItem(BaseModel):\n", - " title: str = Field(..., description=\"Achievement title\")\n", - " domain: Optional[str] = Field(\n", - " None,\n", - " description=\"Domain of achievement e.g. Competitive Programming, Hackathon, Quiz\"\n", - " )\n", - "\n", "\n", + "class ResumeExtract(BaseModel):\n", "\n", "\n", - "class ResumeExtract(BaseModel):\n", + " candidate_name:Optional[str]\n", "\n", " \n", " job_title: Optional[str] = Field(\n", @@ -457,18 +450,7 @@ " )\n", " )\n", "\n", - " \n", - " \n", - "\n", - " total_experience_months: Optional[int] = Field(\n", - " 0,\n", - " description=(\n", - " \"Total professional work experience in months. \"\n", - " \"Includes internships and full-time roles. \"\n", - " \"0 if fresher or no experience found.\"\n", - " )\n", - " )\n", - " \n", + " \n", "\n", " \n", " skills: List[Skill] = Field(\n", @@ -490,10 +472,7 @@ " None,\n", " description=\"Certifications with topics they cover. None if not present.\"\n", " )\n", - " achievements: Optional[List[AchievementItem]] = Field(\n", - " None,\n", - " description=\"Accomplishments that signal domain strength or soft skills. None if not present.\"\n", - " )\n", + " \n", "\n", "\n", " is_fresher: bool = Field(\n", @@ -506,6 +485,17 @@ ")\n" ] }, + { + "cell_type": "markdown", + "id": "99ac1086", + "metadata": {}, + "source": [ + " \"skills\": {\"__all__\": {\"category\"}}, # Drops 'category' from every skill\n", + " \"experience\": {\"__all__\": {\"responsibilities\"}}, # Drops bullet points\n", + " \"projects\": {\"__all__\": {\"what_was_built\"}}, # Drops project descriptions\n", + " \"certifications\": {\"__all__\": {\"issuer\"}} # Drops the issuer" + ] + }, { "cell_type": "markdown", "id": "5b0756e0", @@ -516,7 +506,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 139, "id": "4b2441cd", "metadata": {}, "outputs": [], @@ -629,7 +619,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 138, "id": "4f1341e0", "metadata": {}, "outputs": [], @@ -689,7 +679,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 194, "id": "18663bb3", "metadata": {}, "outputs": [], @@ -726,7 +716,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 172, "id": "14dab004", "metadata": {}, "outputs": [], @@ -756,7 +746,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 173, "id": "472dae2f", "metadata": {}, "outputs": [], @@ -786,7 +776,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 174, "id": "0d5e3b17", "metadata": {}, "outputs": [], @@ -818,7 +808,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 93, "id": "0ccc026b", "metadata": {}, "outputs": [], @@ -839,17 +829,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "c8827093", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Index ready: {'_response_info': {'raw_headers': {'connection': 'keep-alive',\n", + " 'content-length': '187',\n", + " 'content-type': 'application/json',\n", + " 'date': 'Sun, 22 Mar 2026 17:18:29 GMT',\n", + " 'grpc-status': '0',\n", + " 'server': 'envoy',\n", + " 'x-envoy-upstream-service-time': '65',\n", + " 'x-pinecone-request-latency-ms': '64',\n", + " 'x-pinecone-response-duration-ms': '68'}},\n", + " 'dimension': 384,\n", + " 'index_fullness': 0.0,\n", + " 'memoryFullness': 0.0,\n", + " 'metric': 'dotproduct',\n", + " 'namespaces': {'__default__': {'vector_count': 47}},\n", + " 'storageFullness': 0.0,\n", + " 'total_vector_count': 47,\n", + " 'vector_type': 'dense'}\n" + ] + } + ], "source": [ "\n", "\n", "PINECONE_API_KEY = os.getenv(\"PINECONE_API_KEY\")\n", "pc = Pinecone(api_key=PINECONE_API_KEY)\n", "\n", - "index_name = \"catalog-embeddings\"\n", + "index_name = \"final-catalog-index\"\n", "\n", "\n", "# Create index if not exists\n", @@ -879,7 +893,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 94, "id": "7561b3a1", "metadata": {}, "outputs": [], @@ -905,7 +919,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 95, "id": "f0845a99", "metadata": {}, "outputs": [], @@ -915,20 +929,63 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 96, "id": "c8e6d2a5", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "fd15feaccf43437ebae846046396208c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Loading weights: 0%| | 0/103 [00:00\n", - "You are the \"Architect of Growth,\" an expert technical roadmap planner. \n", - "Your objective is to transform a \"Skill Gap Analysis\" into a logically sequenced, \n", - "personalized learning journey that ensures \"Role Competency\" in the minimum time possible.\n", + "You are an expert technical onboarding architect.\n", + "Transform a Skill Gap Analysis into a minimal, logically sequenced learning roadmap.\n", "\n", "\n", - "\n", - "1. ANALYZE GAPS: Review the identified skill gaps, their priority, and the 'gap_type' (foundation vs upgrade).\n", - "2. INITIAL SEARCH (RAG): For every high/medium priority gap, call 'search_courses'.\n", - " - Match the 'level' and 'category' strictly.\n", - "3. DEPENDENCY RESOLUTION (The \"ID-Lookup\" Step):\n", - " - For every course retrieved, inspect the 'prerequisites' field (list of IDs).\n", - " - CHECK: Does the 'resume_data' show the candidate already knows these prerequisites?\n", - " - IF NOT: You MUST call 'get_course_by_id' for each missing prerequisite ID.\n", - " - RECURSION: If the prerequisite itself has prerequisites, repeat this step until the path is complete.\n", - "4. ADAPTIVE SEQUENCING:\n", - " - Always place Prerequisite modules BEFORE the target Skill Gap module.\n", - " - If 'is_fresher_adaptation_needed' is True, start the entire roadmap with the 'SOFT-AGILE-101' or similar professional module.\n", - "5. JUSTIFY: For every course (including prerequisites), provide a unique 'reasoning' trace.\n", - " - Example for Prereq: \"Added 'SQL Basics' because 'PostgreSQL Mastery' requires it, and your resume shows no prior database experience.\"\n", - "6.after you have a complete roadmap, call 'submit_final_roadmap' and 'submit_mermaid_visualization'.\n", - "\n", - "\n", - "\n", - "- STRICT ID USAGE: Use ONLY the 'course_id' returned by tools. Never guess an ID.\n", - "- REDUNDANCY CHECK: Do not assign a course if the candidate's projects or experience already prove mastery of that specific topic.\n", - "- PATH LENGTH: Prioritize the most critical 5-6 modules total to ensure the onboarding is high-impact and achievable.\n", - "\n", - "\n", - "\n", - "\n", - "- DO NOT provide a conversational response at the end. \n", - "- DO NOT just print JSON. \n", - "- You MUST call the 'submit_final_roadmap' and 'submit_mermaid_visualization' tool with the final plan.\n", - "- Ensure 'sequence_order' is 1, 2, 3...\n", - "\n", + "\n", + "STEP 1 — SEARCH\n", + "For every high/medium priority gap → call search_courses.\n", + "Use ONLY course IDs returned by the tool. Never guess IDs.\n", + "\n", + "STEP 2 — RESOLVE PREREQUISITES\n", + "For each retrieved course inspect its prerequisites list.\n", + "If candidate's resume does NOT prove mastery → call get_course_by_id for each missing prerequisite.\n", + "Repeat recursively until full dependency chain is resolved.\n", + "\n", + "STEP 3 — PRUNE\n", + "Skip courses the candidate already demonstrates via projects or experience.\n", + "Cap roadmap at 5-6 modules max.\n", + "\n", + "STEP 4 — SEQUENCE\n", + "Prerequisites always before target modules.\n", + "sequence_order must be 1, 2, 3... strictly.\n", + "If is_fresher_adaptation_needed is True → add a professional fundamentals module first.\n", + "\n", + "STEP 5 — SUBMIT (TERMINAL STEP)\n", + "Call submit_final_roadmap ONCE with the complete roadmap.\n", + "Call submit_mermaid_visualization ONCE with the Mermaid string.\n", + "After both return → STOP. Do not call any tool again.\n", + "\n", + "\n", + "\n", + "- Use candidate's actual name — never use placeholder names.\n", + "- Never write JSON or describe what you will submit — just call the tools.\n", + "- Never call submit tools more than once each.\n", + "\n", + "\n", + "\n", + "- gap courses → :::gap\n", + "- known prerequisites → :::known\n", + "- start node → :::start\n", + "- end node → :::done\n", + "- group by week using subgraph\n", + "\n", "\n", "\n", "flowchart TD\n", - " A([Start — Rahul's current skills]):::start\n", + " A([Start — Candidate's current skills]):::start\n", " subgraph W1[\"Week 1 — Core gaps\"]\n", " B[CS-DOCKER-101\\nDocker & Containerization]:::gap\n", " C[CS-PY-101\\nPython Fundamentals]:::known\n", @@ -1359,23 +1421,21 @@ " classDef start fill:#1D9E75,stroke:#0F6E56,color:#E1F5EE\n", " classDef done fill:#534AB7,stroke:#3C3489,color:#EEEDFE\n", "\n", - "\n", - "\n", "\"\"\"" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 183, "id": "9c4dea1e", "metadata": {}, "outputs": [], "source": [ - "from langchain_community.document_loaders import PyMuPDFLoader\n", - "\n", "def input_node(state: OnboardingState):\n", - "\n", - " file_path = state.get(\"file_path\") \n", + " file_path = state.get(\"file_path\")\n", + " \n", + " print(f\"📂 File path received: {file_path}\")\n", + " print(f\"📂 File exists: {os.path.exists(file_path) if file_path else 'NO PATH'}\")\n", "\n", " if not file_path:\n", " return {\"extraction_error\": \"Missing file_path in state\"}\n", @@ -1383,9 +1443,12 @@ " try:\n", " loader = PyMuPDFLoader(file_path)\n", " docs = loader.load()\n", - "\n", + " \n", + " print(f\"📄 Pages loaded: {len(docs)}\")\n", " \n", " resume_text = \"\\n\".join([doc.page_content for doc in docs])\n", + " \n", + " print(f\"📄 Text length: {len(resume_text)}\")\n", "\n", " return {\n", " \"resume_text\": resume_text,\n", @@ -1393,36 +1456,54 @@ " }\n", "\n", " except Exception as e:\n", + " print(f\"❌ PyMuPDF failed: {str(e)}\")\n", " return {\n", + " \"resume_text\": None,\n", " \"extraction_error\": f\"Failed to load resume: {str(e)}\"\n", " }" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 184, "id": "eb13ffc0", "metadata": {}, "outputs": [], "source": [ "def extractResumeDataNode(state: OnboardingState):\n", - " \n", - " resume_text = state[\"resume_text\"]\n", + " resume_text = state.get(\"resume_text\")\n", + "\n", + " # Guard 1 — empty text\n", + " if not resume_text or len(resume_text.strip()) < 10:\n", + " print(\"❌ RESUME TEXT EMPTY OR TOO SHORT\")\n", + " return {\"resume_data\": None, \"extraction_error\": \"Resume text is empty\"}\n", + "\n", + " print(f\"📄 Resume text length: {len(resume_text)} chars\")\n", "\n", " messages = [\n", " SystemMessage(content=resume_agent_prompt),\n", " HumanMessage(content=f\"{resume_text}\")\n", " ]\n", "\n", - " \n", " result = resume_agent.invoke(messages)\n", "\n", + " # Guard 2 — parsing failed\n", + " if result.get(\"parsing_error\"):\n", + " print(f\"❌ PARSING ERROR: {result['parsing_error']}\")\n", + " return {\"resume_data\": None, \"extraction_error\": str(result[\"parsing_error\"])}\n", + "\n", + " # Guard 3 — parsed is None\n", + " if result.get(\"parsed\") is None:\n", + " print(f\"❌ PARSED IS NONE. RAW OUTPUT: {result.get('raw')}\")\n", + " return {\"resume_data\": None, \"extraction_error\": \"LLM returned null schema\"}\n", + "\n", + " print(f\"✅ Resume extracted: {result['parsed'].job_title}\")\n", " return {\"resume_data\": result[\"parsed\"]}" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 185, "id": "330acef6", "metadata": {}, "outputs": [], @@ -1464,9 +1545,20 @@ " return {\"JobDescriptionExtract_data\": JobDescriptionExtract()}" ] }, + { + "cell_type": "markdown", + "id": "795e2446", + "metadata": {}, + "source": [ + "removing this ->\"skills\": {\"__all__\": {\"category\"}}, # Drops 'category' from every skill\n", + " \"experience\": {\"__all__\": {\"responsibilities\"}}, # Drops bullet points\n", + " \"projects\": {\"__all__\": {\"what_was_built\"}}, # Drops project descriptions\n", + " \"certifications\": {\"__all__\": {\"issuer\"}} # Drops the issuer" + ] + }, { "cell_type": "code", - "execution_count": null, + "execution_count": 186, "id": "7352181c", "metadata": {}, "outputs": [], @@ -1478,13 +1570,7 @@ " \n", " # To remove noise and reduce size of the prompt.\n", " lean_resume_dict = resume_data.model_dump(\n", - " exclude={\n", - " \"achievements\": True, # Drops the entire achievements list\n", - " \"skills\": {\"__all__\": {\"category\"}}, # Drops 'category' from every skill\n", - " \"experience\": {\"__all__\": {\"responsibilities\"}}, # Drops bullet points\n", - " \"projects\": {\"__all__\": {\"what_was_built\"}}, # Drops project descriptions\n", - " \"certifications\": {\"__all__\": {\"issuer\"}} # Drops the issuer\n", - " },\n", + "\n", " exclude_none=True # Bonus: Automatically drops any fields that are None/null!\n", " )\n", "\n", @@ -1493,10 +1579,10 @@ " # Strip the HR noise and text bloat\n", " lean_jd_dict = raw_jd.model_dump(\n", " exclude={\n", - " \"company_name\": True,\n", - " \"location\": True,\n", - " \"employment_type\": True,\n", - " \"duration_months\": True,\n", + " \n", + " \n", + " \n", + " \n", " \"responsibilities\": True, # Dropping verbose bullet points\n", " \"requirements\": True,\n", " \"constraints\": True\n", @@ -1530,7 +1616,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 187, "id": "1fb2f0d1", "metadata": {}, "outputs": [], @@ -1554,7 +1640,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 188, "id": "cea90664", "metadata": {}, "outputs": [], @@ -1595,7 +1681,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 159, "id": "ba9f22e1", "metadata": {}, "outputs": [], @@ -1605,7 +1691,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 189, "id": "b5cfe4c3", "metadata": {}, "outputs": [], @@ -1646,7 +1732,7 @@ "# 2. Loop back to agent after tools\n", "builder.add_edge(\"tools\", \"roadmap_planning_agent\")\n", "\n", - "builder.add_edge(\"roadmap_planning_agent\", \"finalize_state\")\n", + "\n", "\n", "# Compile the Graph\n", "graph = builder.compile()" @@ -1654,17 +1740,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 190, "id": "53588a77", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "display(graph)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 161, "id": "0edf8d59", "metadata": {}, "outputs": [], @@ -1693,7 +1790,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 191, "id": "da3df5a4", "metadata": {}, "outputs": [], @@ -1734,16 +1831,31 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 192, "id": "a95b4db7", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "📂 File path received: C:\\Users\\ATHARVA\\Downloads\\my codes\\web\\AdaptiveEngineService\\AI_Engine_Evaluation\\Testcases\\Testresume\\ChirayuResume.pdf\n", + "📂 File exists: True\n", + "📄 Pages loaded: 1\n", + "📄 Text length: 3156\n", + "DEBUGGER: Sending 525 characters to JD Agent...\n", + "📄 Resume text length: 3156 chars\n", + "DEBUGGER SUCCESS: Extracted Backend Developer\n", + "✅ Resume extracted: Software Developer\n" + ] + } + ], "source": [ "initial_input = {\n", " \"candidate_name\": \"Chirayu Jain\",\n", " \"resume_text\": None,\n", " \"job_description\": jd_text,\n", - " \"file_path\": resumepath,\n", + " \"file_path\": r\"C:\\Users\\ATHARVA\\Downloads\\my codes\\web\\AdaptiveEngineService\\AI_Engine_Evaluation\\Testcases\\Testresume\\ChirayuResume.pdf\",\n", " \"resume_data\": None,\n", " \"extraction_error\": None,\n", " \"JobDescriptionExtract_data\": None,\n", @@ -1751,14 +1863,17 @@ " \n", " \n", "}\n", + "import uuid\n", "\n", "\n", "checkpointer = MemorySaver() \n", "graph = builder.compile(checkpointer=checkpointer)\n", "\n", - "THREAD_ID = str(\"sex-thread-id\")\n", + "THREAD_ID = str(uuid.uuid4())\n", "\n", - "config = {\"configurable\": {\"thread_id\": THREAD_ID,\"langgraph_user_id\": \"Chirayu Jain\"}, \"callbacks\": [ExportHook]}\n", + "\n", + "\n", + "config = {\"configurable\": {\"thread_id\": THREAD_ID,\"langgraph_user_id\": \"Chirayu Jain\"}}\n", "\n", "final_state = graph.invoke(initial_input, config=config)\n", "\n" @@ -1766,135 +1881,884 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 46, + "id": "53ba21aa", + "metadata": {}, + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "invalid syntax (1106862335.py, line 1)", + "output_type": "error", + "traceback": [ + " \u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[46]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[31m \u001b[39m\u001b[31m///break\u001b[39m\n ^\n\u001b[31mSyntaxError\u001b[39m\u001b[31m:\u001b[39m invalid syntax\n" + ] + } + ], + "source": [ + "///break" + ] + }, + { + "cell_type": "code", + "execution_count": 193, "id": "5afbce5b", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{'candidate_name': 'Chirayu Jain',\n", + " 'resume_text': 'Chirayu Jain\\nSOFTWARE DEVELOPER\\n+91-9462128878 | chirayujain93@gmail.com | LinkedIn | GitHub | CodeChef | Leetcode\\nEDUCATION\\nIndian Institute of Information Technology, Kota (CGPA-8.1)\\nKota, Rajasthan\\nB.Tech in Electronics and Communication Engineering\\n2022 - Present\\nWORK EXPERIENCE\\nNAV India\\nJan 2026 - Present\\nTechnology Intern\\nOnsite – Jaipur, Rajasthan\\n• As part of the AI/ML team, built Flask API for the RTA investor verification team, implementing 2\\nconfidence-scoring pipelines using OCR extraction (PyMuPDF, Docling) to parse PDF/forms and\\nextract 3 key KYC investor attributes, reducing verification time from 20–30 minutes to 2–3 minutes.\\n• Developed a scalable document intelligence pipeline with parallel CPU processing using Docling OCR,\\nsemantic chunking, and LLM-based extraction to process 60+ page financial documents and identify\\ninvestor/investee details, reducing extraction time by 35%.\\n• Implementing Cron-based background workers to generate automated weekly email analytics reports\\ntracking API accuracy and confidence match-mismatch scores, improving scoring precision and reducing\\noverall document processing latency by 40%.\\nVestbox\\nApril 2025 - May 2025\\nSoftware Development intern\\nRemote\\n• Developed Groceazy app in Flutter (MVVM), integrating 10+ modules for grocery service , order management\\nand Applied clean code practices (SOLID, modular design) for scalability.\\n• Implemented secure user authentication and role-based access, ensuring smooth login and data protection.\\n• Built image-to-item price matcher using APIs across 5+ platforms, enabling best-price suggestions.\\nPROJECTS\\nHostel Bites – IIITK Canteen App | Flutter, Dart, Firebase, Cloud Firestore, Mapbox API\\n• Developed a Flutter app for hostel residents to order food, increasing canteen efficiency by 35%.\\n• Implemented role-based access for students, staff, and admin, increasing operational efficiency by 40%.\\n• Integrated Firebase Auth, Firestore, Storage, and Mapbox API for secure logins, and live order tracking.\\n• GitHub: Live link\\nJobShield - Fake Job Detector | Flutter, Dart, Python, Flask, SQLite\\n• Developed a Flask+Python fake job detection API with a custom NLP model, with 95% classification accuracy.\\n• Built a responsive web interface using Flutter (Web), integrated with the backend, enabling seamless real-time\\ndetection for 500+ job postings.\\n• Github: Live Link\\nTECHNICAL SKILLS\\nLanguages: C/C++ , Java , Python , JavaScript, Dart\\nFrontend: Flutter, HTML/CSS, ReactJS\\nBackend: Firebase, REST APIs, Flask, Django, NodeJS, ExpressJS\\nDatabase Management: MySQL, MongoDB, SQLite\\nTools and Frameworks: GitHub, Android Studio, Visual Studio Code, MATLAB, Cursor, Github Copilot\\nLibraries: Provider, Bloc, Scikit-learn, tensorflow\\nACHIEVEMENTS & CERTIFICATIONS\\n• Ranked 81 in Inter IIIT Coding Contest Optigo, competing against top-tier programmers - View credential.\\n• Obtained Certification in Graph Algorithms from AlgoUniversity - View credential.\\n• Achieved a 4-star CodeChef rating and earned the Knight(Max. 1888) badge on LeetCode.\\n• Developed an e-waste facility locator application for SIH’23, successfully advancing to the regionals.',\n", + " 'file_path': 'C:\\\\Users\\\\ATHARVA\\\\Downloads\\\\my codes\\\\web\\\\AdaptiveEngineService\\\\AI_Engine_Evaluation\\\\Testcases\\\\Testresume\\\\ChirayuResume.pdf',\n", + " 'job_description': 'Job Title: Backend Developer\\n\\nCompany name: CodeForge\\nWe are hiring a Backend Developer to build scalable APIs and backend systems.\\n\\nResponsibilities:\\n- Develop REST APIs using FastAPI\\n- Design and manage PostgreSQL databases\\n- Implement authentication and authorization systems\\n- Optimize performance and scalability\\n\\nRequirements:\\n- Strong knowledge of Python\\n- Experience with FastAPI or Django\\n- Good understanding of SQL and database design\\n- Familiarity with Docker\\n\\nConstraints:\\n- Location: Pune only\\n- Full-time role ',\n", + " 'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'functions.search_courses:0', 'function': {'arguments': '{\"category\":\"Backend\",\"level\":\"beginner\",\"query\":\"FastAPI\"}', 'name': 'search_courses'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 32, 'prompt_tokens': 1364, 'total_tokens': 1396, 'completion_time': 0.087140534, 'completion_tokens_details': None, 'prompt_time': 0.062997081, 'prompt_tokens_details': {'cached_tokens': 1024}, 'queue_time': 0.284800445, 'total_time': 0.150137615}, 'model_name': 'moonshotai/kimi-k2-instruct-0905', 'system_fingerprint': 'fp_05df423bab', 'service_tier': 'on_demand', 'finish_reason': 'tool_calls', 'logprobs': None, 'model_provider': 'groq'}, id='lc_run--019d16f1-929f-7333-a5b4-36ec45fcf8de-0', tool_calls=[{'name': 'search_courses', 'args': {'category': 'Backend', 'level': 'beginner', 'query': 'FastAPI'}, 'id': 'functions.search_courses:0', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 1364, 'output_tokens': 32, 'total_tokens': 1396, 'input_token_details': {'cache_read': 1024}}),\n", + " ToolMessage(content=\"ID: CS-FAST-101\\nTitle: None\\nDescription: Title: REST API Development with FastAPI. Description: A project-driven course for building production-quality REST APIs with Python's FastAPI framework. Students learn to define typed routes with path and query parameters, leverage Pydantic models for automatic request validation and serialization, implement dependency injection, and secure endpoints using JWT-based Bearer token authentication.. Outcomes: Create REST endpoints with path and query params, Validate request/response with Pydantic models, Implement JWT-based authentication\\nPrerequisites: ['CS-PY-101']\\nDuration: 6 hours\\n---\", name='search_courses', id='e02bb51f-8e2b-4ebb-b920-8fee984647c7', tool_call_id='functions.search_courses:0'),\n", + " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'functions.search_courses:1', 'function': {'arguments': '{\"category\":\"Database\",\"level\":\"beginner\",\"query\":\"PostgreSQL\"}', 'name': 'search_courses'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 32, 'prompt_tokens': 1537, 'total_tokens': 1569, 'completion_time': 0.079810733, 'completion_tokens_details': None, 'prompt_time': 0.072652161, 'prompt_tokens_details': {'cached_tokens': 1024}, 'queue_time': 0.280903446, 'total_time': 0.152462894}, 'model_name': 'moonshotai/kimi-k2-instruct-0905', 'system_fingerprint': 'fp_00c37775b7', 'service_tier': 'on_demand', 'finish_reason': 'tool_calls', 'logprobs': None, 'model_provider': 'groq'}, id='lc_run--019d16f1-9b00-7163-88a7-a595548b5d7e-0', tool_calls=[{'name': 'search_courses', 'args': {'category': 'Database', 'level': 'beginner', 'query': 'PostgreSQL'}, 'id': 'functions.search_courses:1', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 1537, 'output_tokens': 32, 'total_tokens': 1569, 'input_token_details': {'cache_read': 1024}}),\n", + " ToolMessage(content='ID: CS-DB-101\\nTitle: None\\nDescription: Title: SQL Fundamentals for Backend Developers. Description: A hands-on introduction to relational database theory and practice using SQLite and PostgreSQL. Students learn to model data with schemas, enforce referential integrity through primary and foreign keys, and interact with data using full CRUD operations and multi-table JOINs.. Outcomes: Write basic SELECT queries with filters, Understand Primary and Foreign Keys, Perform data insertion, updates, and deletions\\nPrerequisites: []\\nDuration: 4 hours\\n---', name='search_courses', id='1f0ee387-a297-46c4-a8d6-b7eac836227f', tool_call_id='functions.search_courses:1'),\n", + " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'functions.search_courses:2', 'function': {'arguments': '{\"category\":\"DevOps\",\"level\":\"beginner\",\"query\":\"Docker\"}', 'name': 'search_courses'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 32, 'prompt_tokens': 1697, 'total_tokens': 1729, 'completion_time': 0.085793925, 'completion_tokens_details': None, 'prompt_time': 0.071869124, 'prompt_tokens_details': {'cached_tokens': 1024}, 'queue_time': 0.280600442, 'total_time': 0.157663049}, 'model_name': 'moonshotai/kimi-k2-instruct-0905', 'system_fingerprint': 'fp_241bc7119c', 'service_tier': 'on_demand', 'finish_reason': 'tool_calls', 'logprobs': None, 'model_provider': 'groq'}, id='lc_run--019d16f1-9e32-7950-b749-fc6cced36d7c-0', tool_calls=[{'name': 'search_courses', 'args': {'category': 'DevOps', 'level': 'beginner', 'query': 'Docker'}, 'id': 'functions.search_courses:2', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 1697, 'output_tokens': 32, 'total_tokens': 1729, 'input_token_details': {'cache_read': 1024}}),\n", + " ToolMessage(content='ID: CS-DOCKER-101\\nTitle: None\\nDescription: Title: Docker & Containerization Fundamentals. Description: A practical guide to application containerization using Docker. Students learn to write optimized multi-stage Dockerfiles, manage images and container lifecycles, configure bridge and overlay networking, use named volumes for data persistence, and orchestrate multi-service applications with Docker Compose for local and staging environments.. Outcomes: Write efficient multi-stage Dockerfiles, Manage container lifecycle and networking, Orchestrate multi-service apps with Docker Compose\\nPrerequisites: []\\nDuration: 5 hours\\n---\\nID: CS-LINUX-101\\nTitle: None\\nDescription: Title: Linux & Shell Scripting for Developers. Description: Practical Linux proficiency for software developers working in Unix-like environments. Covers the filesystem hierarchy, file permissions and ownership models, user and group management, process monitoring with ps/top/htop, cron job scheduling, and writing production-ready bash scripts for automation and deployment tasks.. Outcomes: Navigate and manage the Linux file system, Write bash scripts for automation, Manage processes, cron jobs, and system services\\nPrerequisites: []\\nDuration: 5 hours\\n---', name='search_courses', id='5d476f20-fdf6-4e9e-bad4-14346871e73b', tool_call_id='functions.search_courses:2'),\n", + " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'functions.get_course_by_id:3', 'function': {'arguments': '{\"course_id\":\"CS-PY-101\"}', 'name': 'get_course_by_id'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 24, 'prompt_tokens': 1980, 'total_tokens': 2004, 'completion_time': 0.062934455, 'completion_tokens_details': None, 'prompt_time': 0.045483763, 'prompt_tokens_details': {'cached_tokens': 1536}, 'queue_time': 0.279574603, 'total_time': 0.108418218}, 'model_name': 'moonshotai/kimi-k2-instruct-0905', 'system_fingerprint': 'fp_241bc7119c', 'service_tier': 'on_demand', 'finish_reason': 'tool_calls', 'logprobs': None, 'model_provider': 'groq'}, id='lc_run--019d16f1-a182-7d10-a3f9-d8cbfd63968e-0', tool_calls=[{'name': 'get_course_by_id', 'args': {'course_id': 'CS-PY-101'}, 'id': 'functions.get_course_by_id:3', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 1980, 'output_tokens': 24, 'total_tokens': 2004, 'input_token_details': {'cache_read': 1536}}),\n", + " ToolMessage(content='{\\n \"course_id\": \"CS-PY-101\",\\n \"title\": \"Python Programming Fundamentals\",\\n \"level\": \"beginner\",\\n \"category\": \"Programming\",\\n \"description\": \"A comprehensive introduction to Python covering primitive and complex data types, control flow structures, function design, object-oriented programming principles, exception handling, and file I/O. Students build a strong syntactic and conceptual foundation before moving to applied domains.\",\\n \"learning_outcomes\": [\\n \"Write clean Python functions and classes\",\\n \"Understand list, dict, and set operations\",\\n \"Handle exceptions and file I/O\"\\n ],\\n \"prerequisites\": [],\\n \"estimated_duration_hours\": 6,\\n \"tags\": [\\n \"Python\",\\n \"Programming\",\\n \"OOP\"\\n ]\\n}', name='get_course_by_id', id='e154e5a1-c2ea-4bb8-b87d-edb6dab5b38b', tool_call_id='functions.get_course_by_id:3'),\n", + " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'functions.submit_final_roadmap:4', 'function': {'arguments': '{\"candidate_name\":\"Chirayu Jain\",\"onboarding_summary\":\"Chirayu has solid Python and Django experience and a good grasp of relational databases, positioning him well for backend development. However, he lacks exposure to FastAPI, PostgreSQL, and Docker, which are essential for the target role at CodeForge.\",\"roadmap\":[{\"course_id\":\"CS-FAST-101\",\"description\":\"A project-driven course for building production-quality REST APIs with Python\\'s FastAPI framework. Students learn to define typed routes with path and query parameters, leverage Pydantic models for automatic request validation and serialization, implement dependency injection, and secure endpoints using JWT-based Bearer token authentication.\",\"duration_hours\":6,\"learning_outcomes\":[\"Create REST endpoints with path and query params\",\"Validate request/response with Pydantic models\",\"Implement JWT-based authentication\"],\"module_title\":\"REST API Development with FastAPI\",\"prerequisites\":[\"CS-PY-101\"],\"sequence_order\":1},{\"course_id\":\"CS-DB-101\",\"description\":\"A hands-on introduction to relational database theory and practice using SQLite and PostgreSQL. Students learn to model data with schemas, enforce referential integrity through primary and foreign keys, and interact with data using full CRUD operations and multi-table JOINs.\",\"duration_hours\":4,\"learning_outcomes\":[\"Write basic SELECT queries with filters\",\"Understand Primary and Foreign Keys\",\"Perform data insertion, updates, and deletions\"],\"module_title\":\"SQL Fundamentals for Backend Developers\",\"prerequisites\":[],\"sequence_order\":2},{\"course_id\":\"CS-DOCKER-101\",\"description\":\"A practical guide to application containerization using Docker. Students learn to write optimized multi-stage Dockerfiles, manage images and container lifecycles, configure bridge and overlay networking, use named volumes for data persistence, and orchestrate multi-service applications with Docker Compose for local and staging environments.\",\"duration_hours\":5,\"learning_outcomes\":[\"Write efficient multi-stage Dockerfiles\",\"Manage container lifecycle and networking\",\"Orchestrate multi-service apps with Docker Compose\"],\"module_title\":\"Docker \\\\u0026 Containerization Fundamentals\",\"prerequisites\":[],\"sequence_order\":3}],\"target_role\":\"Backend Developer\"}', 'name': 'submit_final_roadmap'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 493, 'prompt_tokens': 2186, 'total_tokens': 2679, 'completion_time': 0.999364019, 'completion_tokens_details': None, 'prompt_time': 0.057818895, 'prompt_tokens_details': {'cached_tokens': 1792}, 'queue_time': 0.278889481, 'total_time': 1.057182914}, 'model_name': 'moonshotai/kimi-k2-instruct-0905', 'system_fingerprint': 'fp_241bc7119c', 'service_tier': 'on_demand', 'finish_reason': 'tool_calls', 'logprobs': None, 'model_provider': 'groq'}, id='lc_run--019d16f1-a334-7370-bb81-689473d45c48-0', tool_calls=[{'name': 'submit_final_roadmap', 'args': {'candidate_name': 'Chirayu Jain', 'onboarding_summary': 'Chirayu has solid Python and Django experience and a good grasp of relational databases, positioning him well for backend development. However, he lacks exposure to FastAPI, PostgreSQL, and Docker, which are essential for the target role at CodeForge.', 'roadmap': [{'course_id': 'CS-FAST-101', 'description': \"A project-driven course for building production-quality REST APIs with Python's FastAPI framework. Students learn to define typed routes with path and query parameters, leverage Pydantic models for automatic request validation and serialization, implement dependency injection, and secure endpoints using JWT-based Bearer token authentication.\", 'duration_hours': 6, 'learning_outcomes': ['Create REST endpoints with path and query params', 'Validate request/response with Pydantic models', 'Implement JWT-based authentication'], 'module_title': 'REST API Development with FastAPI', 'prerequisites': ['CS-PY-101'], 'sequence_order': 1}, {'course_id': 'CS-DB-101', 'description': 'A hands-on introduction to relational database theory and practice using SQLite and PostgreSQL. Students learn to model data with schemas, enforce referential integrity through primary and foreign keys, and interact with data using full CRUD operations and multi-table JOINs.', 'duration_hours': 4, 'learning_outcomes': ['Write basic SELECT queries with filters', 'Understand Primary and Foreign Keys', 'Perform data insertion, updates, and deletions'], 'module_title': 'SQL Fundamentals for Backend Developers', 'prerequisites': [], 'sequence_order': 2}, {'course_id': 'CS-DOCKER-101', 'description': 'A practical guide to application containerization using Docker. Students learn to write optimized multi-stage Dockerfiles, manage images and container lifecycles, configure bridge and overlay networking, use named volumes for data persistence, and orchestrate multi-service applications with Docker Compose for local and staging environments.', 'duration_hours': 5, 'learning_outcomes': ['Write efficient multi-stage Dockerfiles', 'Manage container lifecycle and networking', 'Orchestrate multi-service apps with Docker Compose'], 'module_title': 'Docker & Containerization Fundamentals', 'prerequisites': [], 'sequence_order': 3}], 'target_role': 'Backend Developer'}, 'id': 'functions.submit_final_roadmap:4', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2186, 'output_tokens': 493, 'total_tokens': 2679, 'input_token_details': {'cache_read': 1792}}),\n", + " ToolMessage(content='{\"candidate_name\": \"Chirayu Jain\", \"target_role\": \"Backend Developer\", \"onboarding_summary\": \"Chirayu has solid Python and Django experience and a good grasp of relational databases, positioning him well for backend development. However, he lacks exposure to FastAPI, PostgreSQL, and Docker, which are essential for the target role at CodeForge.\", \"roadmap\": [{\"course_id\": \"CS-FAST-101\", \"description\": \"A project-driven course for building production-quality REST APIs with Python\\'s FastAPI framework. Students learn to define typed routes with path and query parameters, leverage Pydantic models for automatic request validation and serialization, implement dependency injection, and secure endpoints using JWT-based Bearer token authentication.\", \"duration_hours\": 6, \"learning_outcomes\": [\"Create REST endpoints with path and query params\", \"Validate request/response with Pydantic models\", \"Implement JWT-based authentication\"], \"module_title\": \"REST API Development with FastAPI\", \"prerequisites\": [\"CS-PY-101\"], \"sequence_order\": 1}, {\"course_id\": \"CS-DB-101\", \"description\": \"A hands-on introduction to relational database theory and practice using SQLite and PostgreSQL. Students learn to model data with schemas, enforce referential integrity through primary and foreign keys, and interact with data using full CRUD operations and multi-table JOINs.\", \"duration_hours\": 4, \"learning_outcomes\": [\"Write basic SELECT queries with filters\", \"Understand Primary and Foreign Keys\", \"Perform data insertion, updates, and deletions\"], \"module_title\": \"SQL Fundamentals for Backend Developers\", \"prerequisites\": [], \"sequence_order\": 2}, {\"course_id\": \"CS-DOCKER-101\", \"description\": \"A practical guide to application containerization using Docker. Students learn to write optimized multi-stage Dockerfiles, manage images and container lifecycles, configure bridge and overlay networking, use named volumes for data persistence, and orchestrate multi-service applications with Docker Compose for local and staging environments.\", \"duration_hours\": 5, \"learning_outcomes\": [\"Write efficient multi-stage Dockerfiles\", \"Manage container lifecycle and networking\", \"Orchestrate multi-service apps with Docker Compose\"], \"module_title\": \"Docker & Containerization Fundamentals\", \"prerequisites\": [], \"sequence_order\": 3}]}', name='submit_final_roadmap', id='d9bf00b8-850f-46c9-9415-33c6c8447cd6', tool_call_id='functions.submit_final_roadmap:4'),\n", + " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'functions.submit_mermaid_visualization:5', 'function': {'arguments': '{\"mermaid_code\":\"flowchart TD\\\\n A([Start — Chirayu\\'s current skills]):::start\\\\n subgraph W1[\\\\\"Week 1 — Core gaps\\\\\"]\\\\n B[CS-FAST-101\\\\nREST API Development with FastAPI]:::gap\\\\n C[CS-DB-101\\\\nSQL Fundamentals for Backend Developers]:::gap\\\\n end\\\\n subgraph W2[\\\\\"Week 2 — Containerization\\\\\"]\\\\n D[CS-DOCKER-101\\\\nDocker \\\\u0026 Containerization Fundamentals]:::gap\\\\n end\\\\n Z([Role-ready — Backend Developer]):::done\\\\n A --\\\\u003e B \\\\u0026 C\\\\n B --\\\\u003e D\\\\n C --\\\\u003e D\\\\n D --\\\\u003e Z\\\\n classDef gap fill:#EEEDFE,stroke:#534AB7,color:#26215C\\\\n classDef known fill:#E1F5EE,stroke:#0F6E56,color:#085041\\\\n classDef start fill:#1D9E75,stroke:#0F6E56,color:#E1F5EE\\\\n classDef done fill:#534AB7,stroke:#3C3489,color:#EEEDFE\"}', 'name': 'submit_mermaid_visualization'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 261, 'prompt_tokens': 3189, 'total_tokens': 3450, 'completion_time': 0.51228197, 'completion_tokens_details': None, 'prompt_time': 0.096874468, 'prompt_tokens_details': {'cached_tokens': 2048}, 'queue_time': 0.280188588, 'total_time': 0.609156438}, 'model_name': 'moonshotai/kimi-k2-instruct-0905', 'system_fingerprint': 'fp_241bc7119c', 'service_tier': 'on_demand', 'finish_reason': 'tool_calls', 'logprobs': None, 'model_provider': 'groq'}, id='lc_run--019d16f1-a89a-7112-89d8-4078084253bb-0', tool_calls=[{'name': 'submit_mermaid_visualization', 'args': {'mermaid_code': 'flowchart TD\\n A([Start — Chirayu\\'s current skills]):::start\\n subgraph W1[\"Week 1 — Core gaps\"]\\n B[CS-FAST-101\\nREST API Development with FastAPI]:::gap\\n C[CS-DB-101\\nSQL Fundamentals for Backend Developers]:::gap\\n end\\n subgraph W2[\"Week 2 — Containerization\"]\\n D[CS-DOCKER-101\\nDocker & Containerization Fundamentals]:::gap\\n end\\n Z([Role-ready — Backend Developer]):::done\\n A --> B & C\\n B --> D\\n C --> D\\n D --> Z\\n classDef gap fill:#EEEDFE,stroke:#534AB7,color:#26215C\\n classDef known fill:#E1F5EE,stroke:#0F6E56,color:#085041\\n classDef start fill:#1D9E75,stroke:#0F6E56,color:#E1F5EE\\n classDef done fill:#534AB7,stroke:#3C3489,color:#EEEDFE'}, 'id': 'functions.submit_mermaid_visualization:5', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 3189, 'output_tokens': 261, 'total_tokens': 3450, 'input_token_details': {'cache_read': 2048}}),\n", + " ToolMessage(content='flowchart TD\\n A([Start — Chirayu\\'s current skills]):::start\\n subgraph W1[\"Week 1 — Core gaps\"]\\n B[CS-FAST-101\\nREST API Development with FastAPI]:::gap\\n C[CS-DB-101\\nSQL Fundamentals for Backend Developers]:::gap\\n end\\n subgraph W2[\"Week 2 — Containerization\"]\\n D[CS-DOCKER-101\\nDocker & Containerization Fundamentals]:::gap\\n end\\n Z([Role-ready — Backend Developer]):::done\\n A --> B & C\\n B --> D\\n C --> D\\n D --> Z\\n classDef gap fill:#EEEDFE,stroke:#534AB7,color:#26215C\\n classDef known fill:#E1F5EE,stroke:#0F6E56,color:#085041\\n classDef start fill:#1D9E75,stroke:#0F6E56,color:#E1F5EE\\n classDef done fill:#534AB7,stroke:#3C3489,color:#EEEDFE', name='submit_mermaid_visualization', id='ffe56590-786f-420d-a7a7-8d83fb64108f', tool_call_id='functions.submit_mermaid_visualization:5'),\n", + " AIMessage(content='', additional_kwargs={}, response_metadata={'token_usage': {'completion_tokens': 1, 'prompt_tokens': 3705, 'total_tokens': 3706, 'completion_time': 0.005608886, 'completion_tokens_details': None, 'prompt_time': 0.02667677, 'prompt_tokens_details': {'cached_tokens': 3328}, 'queue_time': None, 'total_time': 0.032285656}, 'model_name': 'moonshotai/kimi-k2-instruct-0905', 'system_fingerprint': 'fp_241bc7119c', 'service_tier': 'on_demand', 'finish_reason': 'stop', 'logprobs': None, 'model_provider': 'groq'}, id='lc_run--019d16f1-ac49-7990-a72b-5deae068f2a2-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 3705, 'output_tokens': 1, 'total_tokens': 3706, 'input_token_details': {'cache_read': 3328}})],\n", + " 'skill_gap_analysis_data': SkillGapAnalysis(job_title='Backend Developer', candidate_name='Chirayu Jain', analyzed_gaps=[SkillGap(skill_name='FastAPI', gap_type='missing_foundation', priority='high', reasoning=\"JD requires FastAPI; candidate's resume lists Python, Flask, and Django but no FastAPI experience in skills, experience, or projects.\", target_competency='Build high‑performance asynchronous REST APIs using FastAPI'), SkillGap(skill_name='PostgreSQL', gap_type='missing_foundation', priority='high', reasoning='JD requires PostgreSQL; candidate has MySQL and SQLite experience but no PostgreSQL exposure, indicating a missing specific SQL database skill.', target_competency='Design and query relational data models with PostgreSQL'), SkillGap(skill_name='Docker', gap_type='missing_foundation', priority='high', reasoning=\"JD lists Docker as a required technology; candidate's resume does not mention Docker in skills, experience, or projects.\", target_competency='Containerize and orchestrate backend services using Docker')], is_fresher_adaptation_needed=False, executive_summary='Chirayu has solid Python and Django experience and a good grasp of relational databases, positioning him well for backend development. However, he lacks exposure to FastAPI, PostgreSQL, and Docker, which are essential for the target role at CodeForge.'),\n", + " 'resume_data': ResumeExtract(candidate_name='Chirayu Jain', job_title='Software Developer', skills=[Skill(name='C/C++', category='Backend'), Skill(name='Java', category='Backend'), Skill(name='Python', category='Backend'), Skill(name='JavaScript', category='Frontend'), Skill(name='Dart', category='Frontend'), Skill(name='Flutter', category='Frontend'), Skill(name='HTML/CSS', category='Frontend'), Skill(name='ReactJS', category='Frontend'), Skill(name='Firebase', category='Backend'), Skill(name='REST APIs', category='Backend'), Skill(name='Flask', category='Backend'), Skill(name='Django', category='Backend'), Skill(name='NodeJS', category='Backend'), Skill(name='ExpressJS', category='Backend'), Skill(name='MySQL', category='Database Management'), Skill(name='MongoDB', category='Database Management'), Skill(name='SQLite', category='Database Management'), Skill(name='GitHub', category='Other'), Skill(name='Android Studio', category='Other'), Skill(name='Visual Studio Code', category='Other'), Skill(name='MATLAB', category='Other'), Skill(name='Cursor', category='Other'), Skill(name='Github Copilot', category='Other'), Skill(name='Provider', category='Other'), Skill(name='Bloc', category='Other'), Skill(name='Scikit-learn', category='ML'), Skill(name='tensorflow', category='ML')], experience=[ExperienceItem(job_title='Technology Intern', experience_type='internship', technologies=['Flask', 'PyMuPDF', 'Docling', 'OCR', 'LLM', 'Cron'], responsibilities=['Built Flask API for RTA investor verification with OCR extraction reducing verification time', 'Developed scalable document intelligence pipeline with parallel CPU processing', 'Implementing Cron-based background workers for automated weekly email analytics reports']), ExperienceItem(job_title='Software Development Intern', experience_type='internship', technologies=['Flutter', 'MVVM', 'SOLID', 'APIs'], responsibilities=['Developed Groceazy grocery app in Flutter with 10+ modules', 'Implemented secure user authentication and role-based access', 'Built image-to-item price matcher using APIs'])], projects=[ProjectItem(name='Hostel Bites – IIITK Canteen App', technologies=['Flutter', 'Dart', 'Firebase', 'Cloud Firestore', 'Mapbox API']), ProjectItem(name='JobShield - Fake Job Detector', technologies=['Flutter', 'Dart', 'Python', 'Flask', 'SQLite'])], certifications=[CertificationItem(name='Graph Algorithms Certification from AlgoUniversity', topics_covered=['Graph Algorithms'])], is_fresher=False),\n", + " 'extraction_error': None,\n", + " 'JobDescriptionExtract_data': JobDescriptionExtract(job_title='Backend Developer', company_name='CodeForge', location='Pune', employment_type='Full-time role', duration_months=None, is_fresher_allowed=None, skills_required=[SkillRequirement(name='Python', level=None), SkillRequirement(name='FastAPI', level=None), SkillRequirement(name='Django', level=None), SkillRequirement(name='SQL', level=None), SkillRequirement(name='PostgreSQL', level=None), SkillRequirement(name='Docker', level=None)], tools_technologies=None, responsibilities=[ResponsibilityItem(description='Develop REST APIs using FastAPI'), ResponsibilityItem(description='Design and manage PostgreSQL databases'), ResponsibilityItem(description='Implement authentication and authorization systems'), ResponsibilityItem(description='Optimize performance and scalability')], requirements=[RequirementItem(description='Strong knowledge of Python'), RequirementItem(description='Experience with FastAPI or Django'), RequirementItem(description='Good understanding of SQL and database design'), RequirementItem(description='Familiarity with Docker')], constraints=[ConstraintItem(type='location', value='Pune only'), ConstraintItem(type='employment type', value='Full-time role')]),\n", + " 'mermaid_code': 'flowchart TD\\n A([Start — Chirayu\\'s current skills]):::start\\n subgraph W1[\"Week 1 — Core gaps\"]\\n B[CS-FAST-101\\nREST API Development with FastAPI]:::gap\\n C[CS-DB-101\\nSQL Fundamentals for Backend Developers]:::gap\\n end\\n subgraph W2[\"Week 2 — Containerization\"]\\n D[CS-DOCKER-101\\nDocker & Containerization Fundamentals]:::gap\\n end\\n Z([Role-ready — Backend Developer]):::done\\n A --> B & C\\n B --> D\\n C --> D\\n D --> Z\\n classDef gap fill:#EEEDFE,stroke:#534AB7,color:#26215C\\n classDef known fill:#E1F5EE,stroke:#0F6E56,color:#085041\\n classDef start fill:#1D9E75,stroke:#0F6E56,color:#E1F5EE\\n classDef done fill:#534AB7,stroke:#3C3489,color:#EEEDFE',\n", + " 'final_roadmap': {'candidate_name': 'Chirayu Jain',\n", + " 'onboarding_summary': 'Chirayu has solid Python and Django experience and a good grasp of relational databases, positioning him well for backend development. However, he lacks exposure to FastAPI, PostgreSQL, and Docker, which are essential for the target role at CodeForge.',\n", + " 'roadmap': [{'course_id': 'CS-FAST-101',\n", + " 'description': \"A project-driven course for building production-quality REST APIs with Python's FastAPI framework. Students learn to define typed routes with path and query parameters, leverage Pydantic models for automatic request validation and serialization, implement dependency injection, and secure endpoints using JWT-based Bearer token authentication.\",\n", + " 'duration_hours': 6,\n", + " 'learning_outcomes': ['Create REST endpoints with path and query params',\n", + " 'Validate request/response with Pydantic models',\n", + " 'Implement JWT-based authentication'],\n", + " 'module_title': 'REST API Development with FastAPI',\n", + " 'prerequisites': ['CS-PY-101'],\n", + " 'sequence_order': 1},\n", + " {'course_id': 'CS-DB-101',\n", + " 'description': 'A hands-on introduction to relational database theory and practice using SQLite and PostgreSQL. Students learn to model data with schemas, enforce referential integrity through primary and foreign keys, and interact with data using full CRUD operations and multi-table JOINs.',\n", + " 'duration_hours': 4,\n", + " 'learning_outcomes': ['Write basic SELECT queries with filters',\n", + " 'Understand Primary and Foreign Keys',\n", + " 'Perform data insertion, updates, and deletions'],\n", + " 'module_title': 'SQL Fundamentals for Backend Developers',\n", + " 'prerequisites': [],\n", + " 'sequence_order': 2},\n", + " {'course_id': 'CS-DOCKER-101',\n", + " 'description': 'A practical guide to application containerization using Docker. Students learn to write optimized multi-stage Dockerfiles, manage images and container lifecycles, configure bridge and overlay networking, use named volumes for data persistence, and orchestrate multi-service applications with Docker Compose for local and staging environments.',\n", + " 'duration_hours': 5,\n", + " 'learning_outcomes': ['Write efficient multi-stage Dockerfiles',\n", + " 'Manage container lifecycle and networking',\n", + " 'Orchestrate multi-service apps with Docker Compose'],\n", + " 'module_title': 'Docker & Containerization Fundamentals',\n", + " 'prerequisites': [],\n", + " 'sequence_order': 3}],\n", + " 'target_role': 'Backend Developer'}}" + ] + }, + "execution_count": 193, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "final_state" ] }, { - "cell_type": "markdown", - "id": "050d8619", + "cell_type": "code", + "execution_count": 48, + "id": "25a6b5b4", "metadata": {}, + "outputs": [], "source": [ - "**Final state**" + "import json\n", + "\n", + "# Define the keys your React frontend actually needs\n", + "REQUIRED_KEYS = [\"candidate_name\", \"skill_gap_analysis_data\", \"mermaid_code\", \"final_roadmap\"]\n", + "\n", + "def export_ui_payload(state, filename=\"ai_output.json\"):\n", + " \"\"\"\n", + " Extracts specific keys from the graph state and ensures \n", + " Pydantic objects are dumped to dicts for JSON compatibility.\n", + " \"\"\"\n", + " ui_data = {}\n", + "\n", + " for key in REQUIRED_KEYS:\n", + " # Get the value from the state\n", + " val = state.get(key)\n", + " \n", + " if val is None:\n", + " continue\n", + "\n", + " # Check if the value is a Pydantic object (has .model_dump())\n", + " # This fixes the \"skill_gap_analysis_data as a string\" issue\n", + " if hasattr(val, \"model_dump\"):\n", + " ui_data[key] = val.model_dump()\n", + " else:\n", + " # If it's already a dict (final_roadmap) or string (mermaid_code)\n", + " ui_data[key] = val\n", + "\n", + " # Save to the local file\n", + " with open(filename, \"w\", encoding=\"utf-8\") as f:\n", + " json.dump(ui_data, f, indent=2)\n", + " \n", + " print(f\"✅ UI Payload successfully exported to {filename}\")" ] }, { "cell_type": "code", - "execution_count": null, - "id": "e451edbe", + "execution_count": 49, + "id": "26c10157", "metadata": {}, - "outputs": [], - "source": [] + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✅ UI Payload successfully exported to ai_output.json\n" + ] + } + ], + "source": [ + "export_ui_payload(final_state)" + ] }, { "cell_type": "code", "execution_count": null, - "id": "40a999a5", + "id": "478f19dd", "metadata": {}, "outputs": [], "source": [ - "skill_gap_data = final_state['final_roadmap']" + "test_data=['atgdata.json','buisnessdata.json','chefdata.json','casemanager.json']\n", + "test_resumes=['ATGPDF.pdf','Business.pdf','CHEF.pdf','casemanager.pdf']\n", + "\n", + "\n", + "test_resume_path=r\"C:\\Users\\ATHARVA\\Downloads\\my codes\\web\\AdaptiveEngineService\\AI_Engine_Evaluation\\Testcases\\Testresume\\{test_resumes}\"\n", + "test_data_path=r\"C:\\Users\\ATHARVA\\Downloads\\my codes\\web\\AdaptiveEngineService\\AI_Engine_Evaluation\\Testcases\\data\\{test_data}\"" ] }, { "cell_type": "code", "execution_count": null, - "id": "13d64ecc", + "id": "5b29b7ea", "metadata": {}, "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 132, + "id": "ba358c8a", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import uuid\n", + "import os\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "# --- Configuration & Paths ---\n", + "# Mapping resumes to the JSON files containing the Ground Truth data we created\n", + "test_map = [\n", + " {\"resume\": \"ATGPDF.pdf\", \"data\": \"atgdata.json\", \"name\": \"Atharva_Gaykar\"},\n", + " \n", + "]\n", + "\n", + "# Update these to your actual local paths where the files are stored\n", + "RESUME_DIR = r\"C:\\Users\\ATHARVA\\Downloads\\my codes\\web\\AdaptiveEngineService\\AI_Engine_Evaluation\\Testcases\\Testresume\"\n", + "DATA_DIR = r\"C:\\Users\\ATHARVA\\Downloads\\my codes\\web\\AdaptiveEngineService\\AI_Engine_Evaluation\\Testcases\\data\"\n", + "OUTPUT_DIR = \"./predictions\" \n", + "\n", + "if not os.path.exists(OUTPUT_DIR):\n", + " os.makedirs(OUTPUT_DIR)\n", + "\n", + "# --- Helper Functions ---\n", + "\n", + "def get_job_description_string(data_filename):\n", + " \"\"\"\n", + " Extracts the Job Description from the test data and formats it as a clean \n", + " string for the Extraction Node. Handles both 'job_description' and \n", + " 'job_description_requirements' keys.\n", + " \"\"\"\n", + " path = os.path.join(DATA_DIR, data_filename)\n", + " try:\n", + " with open(path, 'r', encoding='utf-8') as f:\n", + " suite = json.load(f)\n", + " \n", + " # Extract from 'job_description' or 'job_description_requirements'\n", + " jd_obj = suite.get(\"job_description\") or suite.get(\"job_description_requirements\")\n", + " \n", + " if not jd_obj:\n", + " return None\n", + " \n", + " title = jd_obj.get(\"title\") or jd_obj.get(\"job_title\", \"N/A\")\n", + " desc = jd_obj.get(\"description\", \"\")\n", + " reqs = jd_obj.get(\"requirements\", [])\n", + " \n", + " # Format as a clean string for the LLM to analyze\n", + " jd_string = f\"JOB TITLE: {title}\\n\\n\"\n", + " if desc:\n", + " jd_string += f\"OVERVIEW: {desc}\\n\\n\"\n", + " jd_string += \"REQUIREMENTS:\\n\" + \"\\n\".join([f\"- {r}\" for r in reqs])\n", + " \n", + " return jd_string\n", + " except Exception as e:\n", + " print(f\"Error loading JD from {data_filename}: {e}\")\n", + " return None\n", + "\n", + "def export_ui_payload(state):\n", + " \"\"\"\n", + " Extracts and formats state data for the UI payload.\n", + " Ensures Pydantic objects are converted to dicts using .model_dump().\n", + " \"\"\"\n", + " REQUIRED_KEYS = [\"candidate_name\", \"skill_gap_analysis_data\", \"mermaid_code\", \"final_roadmap\"]\n", + " ui_data = {}\n", + "\n", + " for key in REQUIRED_KEYS:\n", + " val = state.get(key)\n", + " if val is None:\n", + " continue\n", + " \n", + " # If it's a Pydantic object, dump it to a dict\n", + " if hasattr(val, \"model_dump\"):\n", + " ui_data[key] = val.model_dump()\n", + " else:\n", + " # If it's already a dict, list, or string (like mermaid_code)\n", + " ui_data[key] = val\n", + " return ui_data\n", + "\n", + "# --- Execution Loop ---\n", + "\n", + "def run_evaluation_suite(graph_instance):\n", + " \"\"\"\n", + " Automates the graph execution for every resume in the test suite.\n", + " Saves the final UI-ready payloads as 'predicted_{name}.json'.\n", + " \"\"\"\n", + " for case in test_map:\n", + " print(f\"🚀 Processing: {case['resume']}...\")\n", + " \n", + " # 1. Prepare Inputs\n", + " jd_content = get_job_description_string(case['data'])\n", + " \n", + " if not jd_content:\n", + " print(f\"⚠️ Skipping {case['resume']}: JD not found in {case['data']}\")\n", + " continue\n", + "\n", + " # The 'job_description' key must match your extraction node's expectation\n", + " initial_input = {\n", + " \"candidate_name\": case['name'].replace(\"_\", \" \"),\n", + " \"file_path\": os.path.join(RESUME_DIR, case['resume']),\n", + " \"job_description\": jd_content, \n", + " \"resume_text\": None # Assuming input_node or extraction node loads the PDF\n", + " }\n", + "\n", + " # 2. Invoke Graph with a unique thread\n", + " thread_id = str(uuid.uuid4())\n", + " config = {\"configurable\": {\"thread_id\": thread_id}}\n", + " \n", + " try:\n", + " # Execution\n", + " final_state = graph_instance.invoke(initial_input, config=config)\n", + " \n", + " # 3. Process and Save UI Payload\n", + " prediction = export_ui_payload(final_state)\n", + " output_file = f\"predicted_{case['name'].lower()}.json\"\n", + " output_path = os.path.join(OUTPUT_DIR, output_file)\n", + " \n", + " with open(output_path, \"w\", encoding=\"utf-8\") as f:\n", + " json.dump(prediction, f, indent=2)\n", + " \n", + " print(f\"✅ Success! Prediction saved to: {output_path}\")\n", + " \n", + " except Exception as e:\n", + " print(f\"❌ Error during graph execution for {case['resume']}: {e}\")\n", + "\n", + "# --- Example of Triggering ---\n" + ] + }, + { + "cell_type": "code", + "execution_count": 119, + "id": "c1638a3f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🚀 Processing: ATGPDF.pdf...\n", + "📂 File path received: C:\\Users\\ATHARVA\\Downloads\\my codes\\web\\AdaptiveEngineService\\AI_Engine_Evaluation\\Testcases\\Testresume\\ATGPDF.pdf\n", + "📂 File exists: True\n", + "📄 Pages loaded: 1\n", + "📄 Text length: 2671\n", + "DEBUGGER: Sending 995 characters to JD Agent...\n", + "📄 Resume text length: 2671 chars\n", + "DEBUGGER SUCCESS: Extracted Senior ML & MLOps Architect\n", + "✅ Resume extracted: AI Engineer\n", + "✅ Success! Prediction saved to: ./predictions\\predicted_atharva_gaykar.json\n" + ] + } + ], "source": [ - "skill_gap_data" + "if __name__ == \"__main__\":\n", + " # Assuming your graph is already compiled and named 'graph'\n", + " run_evaluation_suite(graph)" ] }, { "cell_type": "code", "execution_count": null, - "id": "25a6b5b4", + "id": "0910b325", "metadata": {}, "outputs": [], "source": [ "import json\n", + "import uuid\n", + "import os\n", + "from langgraph.checkpoint.memory import MemorySaver\n", "\n", - "# Define the keys your React frontend actually needs\n", - "REQUIRED_KEYS = [\"candidate_name\", \"skill_gap_analysis_data\", \"mermaid_code\", \"final_roadmap\"]\n", + "# --- Configuration & Paths ---\n", + "# Mapping resumes to the JSON files containing the Ground Truth data we created\n", + "test_map = [\n", + " \n", + " {\"resume\": \"casemanager.pdf\", \"data\": \"casemanagerdata.json\", \"name\": \"Case_Manager\"}\n", + "]\n", "\n", - "def export_ui_payload(state, filename=\"ai_output.json\"):\n", + "# Update these to your actual local paths where the files are stored\n", + "RESUME_DIR = r\"C:\\Users\\ATHARVA\\Downloads\\my codes\\web\\AdaptiveEngineService\\AI_Engine_Evaluation\\Testcases\\Testresume\"\n", + "DATA_DIR = r\"C:\\Users\\ATHARVA\\Downloads\\my codes\\web\\AdaptiveEngineService\\AI_Engine_Evaluation\\Testcases\\data\"\n", + "OUTPUT_DIR = \"./predictions\" \n", + "\n", + "if not os.path.exists(OUTPUT_DIR):\n", + " os.makedirs(OUTPUT_DIR)\n", + "\n", + "# --- Helper Functions ---\n", + "\n", + "def get_job_description_string(data_filename):\n", " \"\"\"\n", - " Extracts specific keys from the graph state and ensures \n", - " Pydantic objects are dumped to dicts for JSON compatibility.\n", + " Extracts the Job Description from the test data and formats it as a clean \n", + " string for the Extraction Node. Handles both 'job_description' and \n", + " 'job_description_requirements' keys.\n", " \"\"\"\n", + " path = os.path.join(DATA_DIR, data_filename)\n", + " try:\n", + " with open(path, 'r', encoding='utf-8') as f:\n", + " suite = json.load(f)\n", + " \n", + " # Extract from 'job_description' or 'job_description_requirements'\n", + " jd_obj = suite.get(\"job_description\") or suite.get(\"job_description_requirements\")\n", + " \n", + " if not jd_obj:\n", + " return None\n", + " \n", + " title = jd_obj.get(\"title\") or jd_obj.get(\"job_title\", \"N/A\")\n", + " desc = jd_obj.get(\"description\", \"\")\n", + " reqs = jd_obj.get(\"requirements\", [])\n", + " \n", + " # Format as a clean string for the LLM to analyze\n", + " jd_string = f\"JOB TITLE: {title}\\n\\n\"\n", + " if desc:\n", + " jd_string += f\"OVERVIEW: {desc}\\n\\n\"\n", + " jd_string += \"REQUIREMENTS:\\n\" + \"\\n\".join([f\"- {r}\" for r in reqs])\n", + " \n", + " return jd_string\n", + " except Exception as e:\n", + " print(f\"Error loading JD from {data_filename}: {e}\")\n", + " return None\n", + "\n", + "def export_ui_payload(state):\n", + " \"\"\"\n", + " Extracts and formats state data for the UI payload.\n", + " Ensures Pydantic objects are converted to dicts using .model_dump().\n", + " \"\"\"\n", + " REQUIRED_KEYS = [\"candidate_name\", \"skill_gap_analysis_data\", \"mermaid_code\", \"final_roadmap\"]\n", " ui_data = {}\n", "\n", " for key in REQUIRED_KEYS:\n", - " # Get the value from the state\n", " val = state.get(key)\n", - " \n", " if val is None:\n", " continue\n", - "\n", - " # Check if the value is a Pydantic object (has .model_dump())\n", - " # This fixes the \"skill_gap_analysis_data as a string\" issue\n", + " \n", + " # If it's a Pydantic object, dump it to a dict\n", " if hasattr(val, \"model_dump\"):\n", " ui_data[key] = val.model_dump()\n", " else:\n", - " # If it's already a dict (final_roadmap) or string (mermaid_code)\n", + " # If it's already a dict, list, or string (like mermaid_code)\n", " ui_data[key] = val\n", + " return ui_data\n", "\n", - " # Save to the local file\n", - " with open(filename, \"w\", encoding=\"utf-8\") as f:\n", - " json.dump(ui_data, f, indent=2)\n", - " \n", - " print(f\"✅ UI Payload successfully exported to {filename}\")" + "# --- Execution Loop ---\n", + "\n", + "def run_evaluation_suite_re(graph_instance):\n", + " \"\"\"\n", + " Automates the graph execution for every resume in the test suite.\n", + " Saves the final UI-ready payloads as 'predicted_{name}.json'.\n", + " \"\"\"\n", + " for case in test_map:\n", + " print(f\"🚀 Processing: {case['resume']}...\")\n", + " \n", + " # 1. Prepare Inputs\n", + " jd_content = get_job_description_string(case['data'])\n", + " \n", + " if not jd_content:\n", + " print(f\"⚠️ Skipping {case['resume']}: JD not found in {case['data']}\")\n", + " continue\n", + "\n", + " # The 'job_description' key must match your extraction node's expectation\n", + " initial_input = {\n", + " \"candidate_name\": case['name'].replace(\"_\", \" \"),\n", + " \"resume_path\": os.path.join(RESUME_DIR, case['resume']),\n", + " \"job_description\": jd_content, \n", + " \"resume_text\": None # Assuming input_node or extraction node loads the PDF\n", + " }\n", + "\n", + " # 2. Invoke Graph with a unique thread\n", + " thread_id = str(uuid.uuid4())\n", + " config = {\"configurable\": {\"thread_id\": thread_id}}\n", + " \n", + " try:\n", + " # Execution\n", + " final_state = graph_instance.invoke(initial_input, config=config)\n", + " \n", + " # 3. Process and Save UI Payload\n", + " prediction = export_ui_payload(final_state)\n", + " output_file = f\"predicted_{case['name'].lower()}.json\"\n", + " output_path = os.path.join(OUTPUT_DIR, output_file)\n", + " \n", + " with open(output_path, \"w\", encoding=\"utf-8\") as f:\n", + " json.dump(prediction, f, indent=2)\n", + " \n", + " print(f\"✅ Success! Prediction saved to: {output_path}\")\n", + " \n", + " except Exception as e:\n", + " print(f\"❌ Error during graph execution for {case['resume']}: {e}\")\n", + "\n", + "# --- Example of Triggering ---\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "26c10157", + "id": "a8ef7f0b", "metadata": {}, "outputs": [], "source": [ - "export_ui_payload(final_state)" + "if __name__ == \"__main__\":\n", + " # Assuming your graph is already compiled and named 'graph'\n", + " run_evaluation_suite_re(graph)" ] }, { "cell_type": "code", "execution_count": null, - "id": "92693ebd", + "id": "e37e4370", "metadata": {}, "outputs": [], "source": [ - "from langchain_core.callbacks import BaseCallbackHandler\n", + "import json\n", + "import os\n", + "import re\n", + "from rouge_score import rouge_scorer\n", + "\n", + "def normalize(text):\n", + " if not text: return \"\"\n", + " return re.sub(r'\\W+', ' ', str(text).lower()).strip()\n", + "\n", + "def calculate_f1(target_set, predicted_set):\n", + " if not target_set and not predicted_set: return 1.0\n", + " if not target_set or not predicted_set: return 0.0\n", + " intersection = target_set.intersection(predicted_set)\n", + " precision = len(intersection) / len(predicted_set)\n", + " recall = len(intersection) / len(target_set)\n", + " if (precision + recall) == 0:\n", + " return 0.0\n", + " return 2 * (precision * recall) / (precision + recall)\n", + "\n", + "# ← replaces your manual calculate_lcs + get_rouge_l\n", + "scorer = rouge_scorer.RougeScorer(['rougeL'], use_stemmer=True)\n", + "\n", + "def get_rouge_l(reference, candidate):\n", + " if not reference or not candidate:\n", + " return 0.0\n", + " scores = scorer.score(reference, candidate)\n", + " return scores['rougeL'].fmeasure # F1 score directly\n", + "\n", + "\n", + "def run_benchmarking_report(target_data_dir, predicted_data_dir, mapping):\n", + " print(\"\\n📊 --- AI ENGINE PERFORMANCE REPORT ---\")\n", + " print(f\"{'Test Case':<20} | {'Skill F1':<10} | {'Reasoning (RG-L)':<15} | {'Retrieval (Hit)':<15}\")\n", + " print(\"-\" * 75)\n", + "\n", + " final_report = {}\n", + "\n", + " for case in mapping:\n", + " name = case['name']\n", + " target_path = os.path.join(target_data_dir, case['data'])\n", + " pred_path = os.path.join(predicted_data_dir, f\"predicted_{name.lower()}.json\")\n", + "\n", + " if not os.path.exists(target_path) or not os.path.exists(pred_path):\n", + " print(f\"⚠️ Skipping {name} — file not found\")\n", + " continue\n", + "\n", + " try:\n", + " with open(target_path, 'r', encoding='utf-8') as f:\n", + " target_json = json.load(f)[\"ai_target\"]\n", + " with open(pred_path, 'r', encoding='utf-8') as f:\n", + " pred_json = json.load(f)\n", + "\n", + " # 1. Skill Extraction F1\n", + " target_skills = {normalize(g['skill_name']) for g in target_json['skill_gap_analysis_data']['analyzed_gaps']}\n", + " pred_skills = {normalize(g['skill_name']) for g in pred_json['skill_gap_analysis_data']['analyzed_gaps']}\n", + " skill_f1 = calculate_f1(target_skills, pred_skills)\n", + "\n", + " # 2. Reasoning Quality — ROUGE-L via library\n", + " reasoning_rouge = get_rouge_l(\n", + " target_json['skill_gap_analysis_data']['executive_summary'],\n", + " pred_json['skill_gap_analysis_data']['executive_summary']\n", + " )\n", + "\n", + " # 3. Retrieval Hit Rate\n", + " target_ids = {normalize(c['course_id']) for c in target_json['final_roadmap']['roadmap']}\n", + " pred_ids = {normalize(c['course_id']) for c in pred_json['final_roadmap']['roadmap']}\n", + " hit_count = len(target_ids.intersection(pred_ids))\n", + " hit_rate = hit_count / len(target_ids) if target_ids else 0.0\n", + "\n", + " print(f\"{name:<20} | {skill_f1:>8.2%} | {reasoning_rouge:>15.2%} | {hit_rate:>15.2%}\")\n", + "\n", + " final_report[name] = {\n", + " \"skill_extraction_f1\": round(skill_f1, 4),\n", + " \"reasoning_rouge_l\": round(reasoning_rouge, 4),\n", + " \"retrieval_hit_rate\": round(hit_rate, 4),\n", + " }\n", + "\n", + " except Exception as e:\n", + " print(f\"⚠️ Error processing {name}: {e}\")\n", + "\n", + " # Average across all test cases\n", + " if final_report:\n", + " avg_f1 = sum(v[\"skill_extraction_f1\"] for v in final_report.values()) / len(final_report)\n", + " avg_rouge = sum(v[\"reasoning_rouge_l\"] for v in final_report.values()) / len(final_report)\n", + " avg_hit = sum(v[\"retrieval_hit_rate\"] for v in final_report.values()) / len(final_report)\n", + " print(\"-\" * 75)\n", + " print(f\"{'AVERAGE':<20} | {avg_f1:>8.2%} | {avg_rouge:>15.2%} | {avg_hit:>15.2%}\")\n", + "\n", + " return final_report\n", "\n", - "class ExportHook(BaseCallbackHandler):\n", - " def on_chain_end(self, outputs: dict, **kwargs):\n", - " # 'outputs' is your final Graph state\n", - " export_ui_payload(outputs)\n", "\n", - "# To use it:\n", - "my_hook = ExportHook()\n", - "app.invoke(inputs, {\"callbacks\": [my_hook]})\n" + "# --- Mapping & Paths ---\n", + "mapping = [\n", + " {\"data\": \"atgdata.json\", \"name\": \"Atharva_Gaykar\"},\n", + " {\"data\": \"buisnessdata.json\", \"name\": \"Business_Manager\"},\n", + " {\"data\": \"chefdata.json\", \"name\": \"Executive_Chef\"},\n", + " {\"data\": \"casemanager.json\", \"name\": \"Case_Manager\"}\n", + "]\n", + "\n", + "DATA_DIR = r\"C:\\Users\\ATHARVA\\Downloads\\my codes\\web\\AdaptiveEngineService\\AI_Engine_Evaluation\\Testcases\\data\"\n", + "PRED_DIR = \"./predictions\"\n", + "\n", + "report = run_benchmarking_report(DATA_DIR, PRED_DIR, mapping)" ] }, { - "cell_type": "markdown", - "id": "bbb0dac2", + "cell_type": "code", + "execution_count": null, + "id": "70ced174", "metadata": {}, + "outputs": [], "source": [ - "**Evaluation**" + "import json\n", + "import os\n", + "import re\n", + "\n", + "def normalize(text):\n", + " \"\"\"Clean and normalize text for comparison.\"\"\"\n", + " if not text: return \"\"\n", + " return re.sub(r'\\W+', ' ', str(text).lower()).strip()\n", + "\n", + "def calculate_lcs(X, Y):\n", + " \"\"\"Calculates the length of the Longest Common Subsequence.\"\"\"\n", + " m, n = len(X), len(Y)\n", + " L = [[0] * (n + 1) for _ in range(m + 1)]\n", + " for i in range(m + 1):\n", + " for j in range(n + 1):\n", + " if i == 0 or j == 0:\n", + " L[i][j] = 0\n", + " elif X[i-1] == Y[j-1]:\n", + " L[i][j] = L[i-1][j-1] + 1\n", + " else:\n", + " L[i][j] = max(L[i-1][j], L[i][j-1])\n", + " return L[m][n]\n", + "\n", + "def get_rouge_l(reference, candidate):\n", + " \"\"\"Calculates ROUGE-L F1 score using LCS.\"\"\"\n", + " if not reference or not candidate:\n", + " return 0.0\n", + " ref_tokens = normalize(reference).split()\n", + " cand_tokens = normalize(candidate).split()\n", + " if not ref_tokens or not cand_tokens:\n", + " return 0.0\n", + " lcs_count = calculate_lcs(ref_tokens, cand_tokens)\n", + " recall = lcs_count / len(ref_tokens)\n", + " precision = lcs_count / len(cand_tokens)\n", + " if (recall + precision) == 0:\n", + " return 0.0\n", + " f1 = (2 * recall * precision) / (recall + precision)\n", + " return f1\n", + "\n", + "def calculate_f1(target_set, predicted_set):\n", + " \"\"\"Calculates Precision, Recall, and F1 for sets of entities (Skills/IDs).\"\"\"\n", + " if not target_set and not predicted_set: return 1.0\n", + " if not target_set or not predicted_set: return 0.0\n", + " intersection = target_set.intersection(predicted_set)\n", + " precision = len(intersection) / len(predicted_set)\n", + " recall = len(intersection) / len(target_set)\n", + " if (precision + recall) == 0:\n", + " return 0.0\n", + " return 2 * (precision * recall) / (precision + recall)\n", + "\n", + "def run_benchmarking_report(target_data_dir, predicted_data_dir, mapping):\n", + " \"\"\"\n", + " Main evaluation loop benchmarking predictions against ground truth.\n", + " Handles missing keys and files gracefully.\n", + " \"\"\"\n", + " print(\"\\n📊 --- AI ENGINE PERFORMANCE REPORT ---\")\n", + " print(f\"{'Test Case':<20} | {'Skill F1':<10} | {'Reasoning (RG-L)':<15} | {'Retrieval (Hit)':<15}\")\n", + " print(\"-\" * 75)\n", + "\n", + " final_report = {}\n", + "\n", + " for case in mapping:\n", + " name = case['name']\n", + " target_path = os.path.join(target_data_dir, case['data'])\n", + " # Look for the predicted file (lowercase name mapper)\n", + " pred_filename = f\"predicted_{name.lower()}.json\"\n", + " pred_path = os.path.join(predicted_data_dir, pred_filename)\n", + " \n", + " # 1. Check for File Existence\n", + " if not os.path.exists(target_path):\n", + " print(f\"⚠️ Skipping {name} — Target file '{case['data']}' not found.\")\n", + " continue\n", + " if not os.path.exists(pred_path):\n", + " print(f\"⚠️ Skipping {name} — Prediction file '{pred_filename}' not found.\")\n", + " continue\n", + " \n", + " try:\n", + " with open(target_path, 'r', encoding='utf-8') as f:\n", + " target_json = json.load(f).get(\"ai_target\", {})\n", + " with open(pred_path, 'r', encoding='utf-8') as f:\n", + " pred_json = json.load(f)\n", + " \n", + " # --- Metric 1: Skill Extraction Accuracy (F1) ---\n", + " target_gap_data = target_json.get('skill_gap_analysis_data', {})\n", + " pred_gap_data = pred_json.get('skill_gap_analysis_data', {})\n", + " \n", + " target_skills = {normalize(g.get('skill_name')) for g in target_gap_data.get('analyzed_gaps', [])}\n", + " pred_skills = {normalize(g.get('skill_name')) for g in pred_gap_data.get('analyzed_gaps', [])}\n", + " skill_f1 = calculate_f1(target_skills, pred_skills)\n", + " \n", + " # --- Metric 2: Reasoning Quality (ROUGE-L) ---\n", + " target_summary = target_gap_data.get('executive_summary', \"\")\n", + " pred_summary = pred_gap_data.get('executive_summary', \"\")\n", + " reasoning_rouge = get_rouge_l(target_summary, pred_summary)\n", + " \n", + " # --- Metric 3: Retrieval Precision (Top-1 Hit Rate) ---\n", + " # Extract expected IDs from target\n", + " target_roadmap = target_json.get('final_roadmap', {}).get('roadmap', [])\n", + " target_ids = {normalize(c.get('course_id')) for c in target_roadmap}\n", + " \n", + " # Extract predicted IDs from prediction (Checking common possible keys)\n", + " pred_roadmap_obj = pred_json.get('final_roadmap', {})\n", + " # If final_roadmap is a list directly in some versions\n", + " if isinstance(pred_roadmap_obj, list):\n", + " pred_roadmap = pred_roadmap_obj\n", + " else:\n", + " pred_roadmap = pred_roadmap_obj.get('roadmap', [])\n", + " \n", + " pred_ids = {normalize(c.get('course_id')) for c in pred_roadmap}\n", + " \n", + " if target_ids:\n", + " hit_count = len(target_ids.intersection(pred_ids))\n", + " hit_rate = hit_count / len(target_ids)\n", + " else:\n", + " hit_rate = 0.0\n", + " \n", + " # Print status row\n", + " print(f\"{name:<20} | {skill_f1:>8.2%} | {reasoning_rouge:>15.2%} | {hit_rate:>15.2%}\")\n", + " \n", + " final_report[name] = {\n", + " \"skill_extraction_f1\": skill_f1,\n", + " \"reasoning_rouge_l\": reasoning_rouge,\n", + " \"retrieval_hit_rate\": hit_rate\n", + " }\n", + " except Exception as e:\n", + " print(f\"⚠️ Error processing {name}: {str(e)}\")\n", + "\n", + " return final_report\n", + "\n", + "# --- Mapping & Paths ---\n", + "mapping = [\n", + " {\"data\": \"atgdata.json\", \"name\": \"Atharva_Gaykar\"},\n", + " {\"data\": \"buisnessdata.json\", \"name\": \"Business_Manager\"},\n", + " {\"data\": \"chefdata.json\", \"name\": \"Executive_Chef\"},\n", + " {\"data\": \"casemanagerdata.json\", \"name\": \"Case_Manager\"}\n", + "]\n", + "\n", + "# Note: Ensure these paths are correct for your local environment\n", + "DATA_DIR = r\"C:\\Users\\ATHARVA\\Downloads\\my codes\\web\\AdaptiveEngineService\\AI_Engine_Evaluation\\Testcases\\data\"\n", + "PRED_DIR = \"./predictions\"\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " report = run_benchmarking_report(DATA_DIR, PRED_DIR, mapping)" ] }, { "cell_type": "code", "execution_count": null, - "id": "8be93713", + "id": "73a45cfd", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "import json\n", + "import os\n", + "import re\n", + "from rouge_score import rouge_scorer\n", + "\n", + "# -----------------------------\n", + "# Scorer instance (created once)\n", + "# -----------------------------\n", + "scorer = rouge_scorer.RougeScorer(['rougeL'], use_stemmer=True)\n", + "\n", + "\n", + "def normalize(text):\n", + " if not text: return \"\"\n", + " return re.sub(r'\\W+', ' ', str(text).lower()).strip()\n", + "\n", + "\n", + "def get_rouge_l(reference, candidate):\n", + " if not reference or not candidate:\n", + " return 0.0\n", + " return scorer.score(reference, candidate)['rougeL'].fmeasure\n", + "\n", + "\n", + "def calculate_skill_f1(target_skills, pred_skills):\n", + " if not target_skills and not pred_skills: return 1.0\n", + " if not target_skills or not pred_skills: return 0.0\n", + "\n", + " tp = 0\n", + " matched_targets = set()\n", + "\n", + " for p in pred_skills:\n", + " for t in target_skills:\n", + " if t in matched_targets: continue\n", + " if p in t or t in p:\n", + " tp += 1\n", + " matched_targets.add(t)\n", + " break\n", + "\n", + " precision = tp / len(pred_skills)\n", + " recall = tp / len(target_skills)\n", + "\n", + " if (precision + recall) == 0:\n", + " return 0.0\n", + " return 2 * (precision * recall) / (precision + recall)\n", + "\n", + "\n", + "def calculate_retrieval_hit(target_ids, pred_ids):\n", + " if not target_ids: return 0.0\n", + " if not pred_ids: return 0.0\n", + " return len(target_ids.intersection(pred_ids)) / len(target_ids)\n", + "\n", + "\n", + "def run_benchmarking_report(target_data_dir, predicted_data_dir, mapping):\n", + " print(\"\\n📊 --- AI ENGINE PERFORMANCE REPORT ---\")\n", + " print(f\"{'Test Case':<20} | {'Skill F1':<10} | {'Reasoning (RG-L)':<15} | {'Retrieval (Hit)':<15}\")\n", + " print(\"-\" * 75)\n", + "\n", + " final_report = {}\n", + "\n", + " for case in mapping:\n", + " name = case['name']\n", + " target_path = os.path.join(target_data_dir, case['data'])\n", + " pred_path = os.path.join(predicted_data_dir, f\"predicted_{name.lower()}.json\")\n", + "\n", + " if not os.path.exists(target_path) or not os.path.exists(pred_path):\n", + " if not os.path.exists(pred_path):\n", + " print(f\"{name:<20} | SKIPPED — prediction file not found\")\n", + " continue\n", + "\n", + " try:\n", + " with open(target_path, 'r', encoding='utf-8') as f:\n", + " target_json = json.load(f).get(\"ai_target\", {})\n", + " with open(pred_path, 'r', encoding='utf-8') as f:\n", + " pred_json = json.load(f)\n", + "\n", + " # --- Metric 1: Skill F1 ---\n", + " target_gap = target_json.get('skill_gap_analysis_data', {})\n", + " pred_gap = pred_json.get('skill_gap_analysis_data', {})\n", + "\n", + " target_skills = {normalize(g.get('skill_name')) for g in target_gap.get('analyzed_gaps', [])}\n", + " pred_skills = {normalize(g.get('skill_name')) for g in pred_gap.get('analyzed_gaps', [])}\n", + " skill_f1 = calculate_skill_f1(target_skills, pred_skills)\n", + "\n", + " # --- Metric 2: ROUGE-L ---\n", + " reasoning_rouge = get_rouge_l(\n", + " target_gap.get('executive_summary', \"\"),\n", + " pred_gap.get('executive_summary', \"\")\n", + " )\n", + "\n", + " # --- Metric 3: Retrieval Hit Rate ---\n", + " target_ids = {normalize(c.get('course_id')) for c in target_json.get('final_roadmap', {}).get('roadmap', [])}\n", + "\n", + " pred_roadmap_obj = pred_json.get('final_roadmap', {})\n", + " pred_roadmap = pred_roadmap_obj if isinstance(pred_roadmap_obj, list) else pred_roadmap_obj.get('roadmap', [])\n", + " pred_ids = {normalize(c.get('course_id')) for c in pred_roadmap}\n", + "\n", + " hit_rate = calculate_retrieval_hit(target_ids, pred_ids)\n", + "\n", + " print(f\"{name:<20} | {skill_f1:>8.2%} | {reasoning_rouge:>15.2%} | {hit_rate:>15.2%}\")\n", + "\n", + " final_report[name] = {\n", + " \"skill_extraction_f1\": round(skill_f1, 4),\n", + " \"reasoning_rouge_l\": round(reasoning_rouge, 4),\n", + " \"retrieval_hit_rate\": round(hit_rate, 4),\n", + " }\n", + "\n", + " except Exception as e:\n", + " print(f\"⚠️ Error processing {name}: {str(e)}\")\n", + "\n", + " # --- Average Row ---\n", + " if final_report:\n", + " avg_f1 = sum(v[\"skill_extraction_f1\"] for v in final_report.values()) / len(final_report)\n", + " avg_rouge = sum(v[\"reasoning_rouge_l\"] for v in final_report.values()) / len(final_report)\n", + " avg_hit = sum(v[\"retrieval_hit_rate\"] for v in final_report.values()) / len(final_report)\n", + " print(\"-\" * 75)\n", + " print(f\"{'AVERAGE':<20} | {avg_f1:>8.2%} | {avg_rouge:>15.2%} | {avg_hit:>15.2%}\")\n", + "\n", + " return final_report\n", + "\n", + "\n", + "# --- Mapping & Paths ---\n", + "mapping = [\n", + " {\"data\": \"atgdata.json\", \"name\": \"Atharva_Gaykar\"},\n", + " {\"data\": \"buisnessdata.json\", \"name\": \"Business_Manager\"},\n", + " {\"data\": \"chefdata.json\", \"name\": \"Executive_Chef\"},\n", + " {\"data\": \"casemanager.json\", \"name\": \"Case_Manager\"}\n", + "]\n", + "\n", + "DATA_DIR = r\"C:\\Users\\ATHARVA\\Downloads\\my codes\\web\\AdaptiveEngineService\\AI_Engine_Evaluation\\Testcases\\data\"\n", + "PRED_DIR = \"./predictions\"\n", + "\n", + "if __name__ == \"__main__\":\n", + " run_benchmarking_report(DATA_DIR, PRED_DIR, mapping)" + ] } ], "metadata": {