imran-decoder/pathway
0
1import os2import re3import json4import uvicorn5from typing import List, Optional, Dict, Any6from fastapi import FastAPI, HTTPException7from pydantic import BaseModel8from dotenv import load_dotenv9from pymongo import MongoClient10from bson import ObjectId, errors as bson_errors11from langchain_community.graphs import Neo4jGraph12from huggingface_hub import InferenceClient13 14# --- 1. Configuration & Setup ---15load_dotenv()16 17app = FastAPI(title="NSQF Career Pathway API")18 19# Verify Environment Variables20required_vars = ["NEO4J_URI", "NEO4J_USERNAME", "NEO4J_PASSWORD", "MONGO_URI", "HF_TOKEN"]21missing_vars = [var for var in required_vars if not os.getenv(var)]22if missing_vars:23 print(f"⚠️ CRITICAL WARNING: Missing environment variables: {missing_vars}")24 print("⚠️ Ensure .env file is present. Exiting...")25 exit(1)26 27# Initialize Clients28try:29 print("⏳ Connecting to Neo4j...")30 graph = Neo4jGraph(31 url=os.getenv("NEO4J_URI"),32 username=os.getenv("NEO4J_USERNAME"),33 password=os.getenv("NEO4J_PASSWORD")34 )35 36 print("⏳ Connecting to MongoDB...")37 client = MongoClient(os.getenv("MONGO_URI"))38 db = client["career-advisor"]39 users_collection = db["userdetails"]40 41 print("⏳ Connecting to Hugging Face...")42 hf_client = InferenceClient(43 model="meta-llama/Llama-3.1-8B-Instruct",44 token=os.getenv("HF_TOKEN")45 )46 print("✅ All systems initialized.")47 48except Exception as e:49 print(f"❌ Global Init Error: {e}")50 exit(1)51 52 53# --- 2. Data Models ---54class UserRequest(BaseModel):55 user_id: str56 57class PathwayStep(BaseModel):58 step_order: int59 nsqf_level: str60 role_title: str61 description: str62 skills_required: List[str]63 user_alignment: str64 65class CareerPathwayPlan(BaseModel):66 target_sector: str67 user_fit_summary: str68 steps: List[PathwayStep]69 70class APIResponse(BaseModel):71 status: str72 data: CareerPathwayPlan73 74 75# --- 3. Helper Functions ---76def extract_keywords(text: str) -> List[str]:77 """Extracts significant keywords from a string."""78 if not text: return []79 STOPWORDS = {80 "and", "or", "the", "in", "of", "for", "with", "a", "an", "to", "at",81 "by", "on", "is", "are", "was", "were", "be", "been", "being",82 "sector", "industry", "field", "area", "services", "management", "development"83 }84 clean_text = re.sub(r'[^a-zA-Z0-9\s]', '', text.lower())85 words = clean_text.split()86 keywords = [w for w in words if w not in STOPWORDS and len(w) > 2]87 return list(set(keywords))88 89NSQF_MAPPING = {90 "skills": {91 "Python": ["IT-ITeS", "Data Science"],92 "Java": ["IT-ITeS"],93 "Teaching": ["Education"],94 "Nursing": ["Healthcare"],95 "Welding": ["Manufacturing"]96 },97 "interestSectors": {98 "Technology": ["IT-ITeS"],99 "Health": ["Healthcare"],100 "Construction": ["Infrastructure"]101 },102 "qualifications": {103 "B.Tech": ["IT-ITeS", "Engineering"],104 "MBBS": ["Healthcare"],105 "B.Ed": ["Education"]106 }107}108 109def resolve_user_nsqf_sectors(user_doc: Dict) -> List[str]:110 """Maps user attributes to official NSQF Sectors."""111 matched_sectors = set()112 for skill in user_doc.get("skills", []):113 matched_sectors.update(NSQF_MAPPING["skills"].get(skill, []))114 for sec in user_doc.get("interestSectors", []):115 matched_sectors.update(NSQF_MAPPING["interestSectors"].get(sec, []))116 117 highest_q = user_doc.get("education", {}).get("highestQualification", "")118 if highest_q:119 matched_sectors.update(NSQF_MAPPING["qualifications"].get(highest_q, []))120 121 return list(matched_sectors)122 123 124# --- 4. API Endpoint ---125@app.post("/generate-pathway", response_model=APIResponse)126async def generate_pathway(payload: UserRequest):127 target_user_id = payload.user_id128 129 # --- STEP A: Fetch User from MongoDB ---130 if not ObjectId.is_valid(target_user_id):131 raise HTTPException(status_code=400, detail="Invalid MongoDB ObjectId format")132 133 try:134 user_doc = users_collection.find_one({"_id": ObjectId(target_user_id)})135 except bson_errors.InvalidId:136 raise HTTPException(status_code=400, detail="Invalid User ID")137 138 # Fallback for testing if ID doesn't exist139 if not user_doc:140 print(f"⚠️ User {target_user_id} not found. Using Mock Data.")141 user_doc = {142 "_id": target_user_id,143 "skills": ["Python", "Data Analysis", "SQL"],144 "interestSectors": ["Technology"],145 "education": {"highestQualification": "B.Tech"}146 }147 148 # --- STEP B: Determine Sector Context ---149 user_nsqf_sectors = resolve_user_nsqf_sectors(user_doc)150 151 # --- STEP C: Neo4j Query (Find Top Sector) ---152 query_sector = f"""153 MATCH (u:User {{mongoId: "{target_user_id}"}})-[r:RECOMMENDED_FOR]->(q:Qualification)-[b:BELONGS_TO_SECTOR]->(s:Sector)154 WITH s, q, r155 ORDER BY r.rank DESC156 RETURN s.name AS sector, head(collect(q.title)) AS top_title, max(r.rank) AS rank157 ORDER BY rank DESC158 LIMIT 1159 """160 161 try:162 result_sector = graph.query(query_sector)163 except Exception as e:164 print(f"Neo4j Error: {e}")165 result_sector = []166 167 # Fallback if Neo4j returns nothing168 if result_sector:169 top_sector = result_sector[0]['sector']170 top_title = result_sector[0]['top_title']171 else:172 top_sector = "IT-ITeS" # Default173 top_title = "Software Developer"174 175 # --- STEP D: Neo4j Query (Get Pathway Data) ---176 sector_keywords = extract_keywords(top_sector)177 title_keywords = extract_keywords(top_title)178 all_keywords = list(set(sector_keywords + title_keywords))179 180 sanitized_keywords = [kw.replace("'", "") for kw in all_keywords]181 182 # Build dynamic WHERE clause183 if sanitized_keywords:184 where_clause = " OR ".join([f"toLower(q.title) CONTAINS '{kw}'" for kw in sanitized_keywords])185 else:186 where_clause = "1=1"187 188 query_pathway = f"""189 MATCH (q:Qualification)-[:BELONGS_TO_SECTOR]->(s:Sector)190 WHERE s.name = "{top_sector}" AND ({where_clause})191 RETURN q.title AS title, q.nsqf_level AS nsqf_level, q.progression_pathway AS progression_pathway192 ORDER BY q.nsqf_level ASC193 LIMIT 10194 """195 196 try:197 pathway_data = graph.query(query_pathway)198 except Exception:199 pathway_data = []200 201 # --- STEP E: LLM Generation (Strict JSON) ---202 203 # 1. Define the Schema strictly204 json_schema = """205 {206 "career_pathway_plan": {207 "target_sector": "String",208 "user_fit_summary": "String",209 "steps": [210 {211 "step_order": 1,212 "nsqf_level": "String",213 "role_title": "String",214 "description": "String (Short description of role)",215 "skills_required": ["String", "String"],216 "user_alignment": "String (How well user fits)"217 }218 ]219 }220 }221 """222 223 # 2. Construct Prompt224 system_prompt = f"""225 You are a Career Counselor API Backend. 226 You DO NOT speak to the user directly. 227 You ONLY output valid JSON matching this schema exactly:228 {json_schema}229 230 Do not add Markdown formatting (like ```json). Just the raw JSON string.231 """232 233 user_prompt = f"""234 CONTEXT:235 User Skills: {user_doc.get('skills')}236 User Education: {user_doc.get('education', {}).get('highestQualification')}237 Target Sector: {top_sector}238 239 AVAILABLE QUALIFICATIONS (From Database):240 {pathway_data}241 242 TASK:243 Construct a logical career progression (Low NSQF -> High NSQF) using the qualifications above.244 Map the user's current skills to the appropriate starting point.245 """246 247 try:248 response = hf_client.chat.completions.create(249 model="meta-llama/Llama-3.1-8B-Instruct",250 messages=[251 {"role": "system", "content": system_prompt},252 {"role": "user", "content": user_prompt}253 ],254 max_tokens=1000,255 temperature=0.3, # Low temp for structure stability256 top_p=0.9257 )258 259 raw_content = response.choices[0].message.content.strip()260 261 # --- STEP F: JSON Cleanup & Validation ---262 263 # Remove Markdown wrappers if Llama adds them264 if "```json" in raw_content:265 raw_content = raw_content.split("```json")[1].split("```")[0].strip()266 elif "```" in raw_content:267 raw_content = raw_content.split("```")[1].split("```")[0].strip()268 269 # Parse270 parsed_json = json.loads(raw_content)271 272 # Validate structure wraps correctly273 if "career_pathway_plan" not in parsed_json:274 # Attempt to fix if LLM returned just the inner object275 parsed_json = {"career_pathway_plan": parsed_json}276 277 return {"status": "success", "data": parsed_json["career_pathway_plan"]}278 279 except json.JSONDecodeError:280 print(f"❌ JSON Parse Error. Raw Output:\n{raw_content}")281 raise HTTPException(status_code=500, detail="AI generated invalid JSON structure.")282 except Exception as e:283 print(f"❌ Processing Error: {str(e)}")284 raise HTTPException(status_code=500, detail=str(e))285 286 287if __name__ == "__main__":288 uvicorn.run(app, host="0.0.0.0", port=7860)