Spaces:
Sleeping
Sleeping
File size: 8,738 Bytes
7ff6662 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | """
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
|