Spaces:
Sleeping
Sleeping
| """ | |
| Job Assessor β 2-phase pipeline. | |
| Phase 1 (INSTANT): Keyword-based scoring for all jobs. | |
| - No LLM calls β pure keyword/signal matching against Saiteja's profile | |
| - Scores 1-10 based on title, skills, and description signals | |
| - Takes <1 second for any number of jobs | |
| Phase 2 (LLM, top jobs only): Detailed assessment for top PHASE2_MAX jobs. | |
| - Sends to multi-model pool (GLM + Qwen + GPT-OSS + others) | |
| - Each model picks up batches from shared queue as it finishes | |
| - Fallback: if all models fail, keeps keyword score from Phase 1 | |
| """ | |
| import re | |
| from colorama import Fore, Style, init | |
| from .model_pool import ModelPool, PHASE2_THRESHOLD, PHASE2_MAX | |
| from .scrapers.base import Job | |
| init(autoreset=True) | |
| # PM keywords for pre-filtering | |
| PM_KEYWORDS = { | |
| "product manager", "product owner", "product lead", "product director", | |
| "vp product", "head of product", "chief product", "product strategist", | |
| "technical product", "growth product", "ai product", "data product", | |
| "platform product", "saas product", "product marketing", "program manager", | |
| "product operations", "product analyst", "associate product", "senior product", | |
| "lead product", "product development", "product management", | |
| } | |
| BLOCK_KEYWORDS = { | |
| "teacher", "faculty", "lecturer", "professor", "tutor", | |
| "civil engineer", "mechanical engineer", "electrical engineer", | |
| "accountant", "chartered accountant", " ca ", "nurse", "doctor", | |
| "chef", "cook", "driver", "security guard", | |
| "hardware engineer", "network engineer", "rf engineer", | |
| "sap consultant", "sap basis", "sap abap", "erp consultant", | |
| "custody product", "care advisor", "specialty care", "speciality care", | |
| } | |
| # High-match signals for Saiteja's profile | |
| HIGH_MATCH_SIGNALS = { | |
| # Role signals (title) | |
| "title": [ | |
| "ai product", "ai pm", "conversational", "edtech", "growth product", | |
| "growth pm", "technical product", "technical pm", "platform product", | |
| "data product", "saas product", "senior product manager", | |
| "lead product", "head of product", "product director", | |
| ], | |
| # Skill signals (description) | |
| "skills": [ | |
| "funnel optimization", "funnel", "conversion rate", "a/b test", | |
| "conversational ai", "llm", "chatbot", "ocr", "automation pipeline", | |
| "crm", "webengage", "whatsapp", "growth hacking", "user acquisition", | |
| "retention", "cohort", "edtech", "b2c", "0 to 1", "0β1", | |
| "product-led", "viral", "referral", "engagement", | |
| ], | |
| # Company type signals | |
| "company": [ | |
| "swiggy", "zomato", "meesho", "razorpay", "cred", "juspay", | |
| "groww", "zepto", "blinkit", "nykaa", "mamaearth", "unacademy", | |
| "byju", "classplus", "vedantu", "toppr", "upgrad", "scaler", | |
| "google", "microsoft", "amazon", "meta", "flipkart", "myntra", | |
| "ola", "uber", "paytm", "phonepe", "gpay", "freshworks", "zoho", | |
| ], | |
| } | |
| MEDIUM_MATCH_SIGNALS = { | |
| "title": [ | |
| "product owner", "associate product", "product manager", | |
| "product specialist", "product analyst", | |
| ], | |
| "skills": [ | |
| "product strategy", "roadmap", "agile", "scrum", "user research", | |
| "ux research", "data-driven", "kpi", "analytics", "feature prioritization", | |
| "prd", "go-to-market", "gtm", "cross-functional", "stakeholder", | |
| ], | |
| } | |
| def _is_relevant(title: str) -> bool: | |
| """Use the same PM filter as the scraper for consistency.""" | |
| from .scrapers.base import BaseScraper | |
| return BaseScraper.is_pm_role(title) | |
| def keyword_score(job: Job) -> int: | |
| """ | |
| Instant 1-10 score based on title, description, and company signals. | |
| No LLM required β deterministic, sub-millisecond. | |
| """ | |
| title = (job.title or "").lower() | |
| desc = (job.description or "").lower() | |
| comp = (job.company or "").lower() | |
| text = title + " " + desc + " " + comp | |
| score = 4 # baseline for PM-relevant job | |
| # High match β title signals (+3) | |
| for sig in HIGH_MATCH_SIGNALS["title"]: | |
| if sig in title: | |
| score += 3 | |
| break | |
| # High match β skill signals (+2) | |
| skill_hits = sum(1 for sig in HIGH_MATCH_SIGNALS["skills"] if sig in text) | |
| score += min(skill_hits * 1, 3) | |
| # Company signals (+1) | |
| for sig in HIGH_MATCH_SIGNALS["company"]: | |
| if sig in comp: | |
| score += 1 | |
| break | |
| # Medium match β title signals (+1) | |
| for sig in MEDIUM_MATCH_SIGNALS["title"]: | |
| if sig in title: | |
| score += 1 | |
| break | |
| # Penalise if description is empty (less info) | |
| if not job.description: | |
| score -= 1 | |
| # Clamp to 1-10 | |
| return max(1, min(10, score)) | |
| class JobAssessor: | |
| BATCH_SIZE = 8 # Jobs per LLM call β all fast models handle 8 at once | |
| def __init__(self, model_pool: ModelPool, compact_profile: str): | |
| self.pool = model_pool | |
| self.compact_profile = compact_profile | |
| def assess_all(self, jobs: list[Job]) -> list[dict]: | |
| # ββ Step 1: Pre-filter by title ββββββββββββββββββββββββββββββββββ | |
| relevant = [j for j in jobs if _is_relevant(j.title)] | |
| irrelevant = [j for j in jobs if not _is_relevant(j.title)] | |
| print(f"\n{Fore.CYAN}Pre-filter: {len(relevant)} PM-relevant | {len(irrelevant)} irrelevant (skipped){Style.RESET_ALL}") | |
| skipped_results = [ | |
| { | |
| **j.to_dict(), | |
| "relevance_score": 1, "skills_match_percentage": 0, | |
| "experience_match": "Not Relevant", "matching_skills": "", | |
| "missing_skills": "", "key_strengths": "", | |
| "recommendation": "Not a PM role β filtered.", | |
| "ats_keywords": "", "application_priority": "Low", | |
| "_raw_assessment": {}, "_assessed_by": "pre-filter", | |
| } | |
| for j in irrelevant | |
| ] | |
| if not relevant: | |
| return skipped_results | |
| # ββ Step 2: Phase 1 β instant keyword scoring βββββββββββββββββββββ | |
| print(f"{Fore.CYAN}Phase 1: Keyword scoring {len(relevant)} jobs (instant)...{Style.RESET_ALL}") | |
| for job in relevant: | |
| job._keyword_score = keyword_score(job) | |
| kw_high = sum(1 for j in relevant if j._keyword_score >= 7) | |
| kw_med = sum(1 for j in relevant if 5 <= j._keyword_score < 7) | |
| print(f"{Fore.GREEN}β Keyword scoring done β High(7+): {kw_high} Medium(5-6): {kw_med}{Style.RESET_ALL}") | |
| # ββ Step 3: Phase 2 β LLM detailed assessment of ALL relevant jobs ββ | |
| # Process ALL PM jobs (no cap) β fast models handle batches quickly | |
| top_jobs = sorted(relevant, key=lambda x: x._keyword_score, reverse=True) | |
| print(f"\n{Fore.YELLOW}Phase 2: LLM detailed assessment of ALL {len(top_jobs)} PM jobs...{Style.RESET_ALL}") | |
| llm_results: dict[str, dict] = {} # url β assessment dict | |
| if top_jobs: | |
| assessed = self.pool.process_all( | |
| jobs=top_jobs, | |
| compact_profile=self.compact_profile, | |
| batch_size=self.BATCH_SIZE, | |
| ) | |
| for r in assessed: | |
| url = r.get("url", "") | |
| if url: | |
| llm_results[url] = r | |
| # ββ Step 4: Merge β LLM result if available, else keyword score ββ | |
| final = [] | |
| for job in relevant: | |
| url = job.url or "" | |
| if url in llm_results: | |
| # Use LLM result (already a full dict) | |
| final.append(llm_results[url]) | |
| else: | |
| # Use keyword score | |
| score = job._keyword_score | |
| final.append({ | |
| **job.to_dict(), | |
| "relevance_score": score, | |
| "skills_match_percentage": min(score * 10, 100), | |
| "experience_match": "Good fit" if score >= 7 else "Unknown", | |
| "matching_skills": "", | |
| "missing_skills": "", | |
| "key_strengths": "", | |
| "recommendation": f"Keyword score: {score}/10. LLM assessment not run.", | |
| "ats_keywords": "", | |
| "application_priority": "High" if score >= 8 else ("Medium" if score >= 6 else "Low"), | |
| "_raw_assessment": {"score": score}, | |
| "_assessed_by": "keyword", | |
| }) | |
| all_results = final + skipped_results | |
| all_results.sort(key=lambda x: x.get("relevance_score", 0), reverse=True) | |
| return all_results | |