Team Ai
Apppublic

imran-decoder/pathway

sourceHugging Faceupdated 10mo agoView on Hugging Face
0likes
main.py288 linesDownload Raw Back to root
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)