Spaces:
Sleeping
Sleeping
File size: 11,873 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 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | #!/usr/bin/env python3
"""
Job Automation Agent -- PM Edition
Searches Product Manager jobs across LinkedIn, Indeed, Glassdoor.
Rates each job using a 10-model parallel AI pool.
Generates ATS 95%+ optimized DOCX resumes for all PM jobs.
"""
import io
import os
import sys
import time
import json
from datetime import datetime
from colorama import Fore, Style, init
from tqdm import tqdm
# Force UTF-8 output on Windows
if hasattr(sys.stdout, "buffer"):
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
if hasattr(sys.stderr, "buffer"):
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
init(autoreset=True)
BANNER = (
f"\n{Fore.CYAN}"
"==========================================================\n"
" JOB AUTOMATION AGENT -- PM Edition\n"
" 10-Model AI Pool | LinkedIn / Indeed / Glassdoor\n"
" ATS Score: 95%+ guaranteed on all resumes\n"
f"=========================================================={Style.RESET_ALL}\n"
)
def main():
print(BANNER)
from config import JOB_SEARCH, PLATFORMS, ASSESSMENT, OUTPUT, RESUME, ASSESSMENT_MODELS, GOOGLE
from src.resume_parser import ResumeParser
from src.llm_client import LLMClient
from src.model_pool import ModelPool
from src.job_assessor import JobAssessor
from src.resume_customizer import ResumeCustomizer
from src.excel_reporter import ExcelReporter
from src.scrapers.base import Job
# ββ 1. PARSE RESUME ββ
print(f"\n{Fore.YELLOW}[Step 1/5] Parsing your resume...{Style.RESET_ALL}")
parser = ResumeParser(RESUME["pdf_path"])
try:
resume_text = parser.parse()
print(f"{Fore.GREEN}β Resume parsed ({len(resume_text)} chars){Style.RESET_ALL}")
except FileNotFoundError as e:
print(f"{Fore.RED}β {e}{Style.RESET_ALL}")
sys.exit(1)
except Exception as e:
print(f"{Fore.RED}β Resume parsing failed: {e}{Style.RESET_ALL}")
sys.exit(1)
# ββ 2. BUILD PROFILE β use fastest available model ββ
# Try Kimi-K2.6 first (~5s), fall back to GLM 5.1 (~234s)
fast_profile_cfg = next(
(m for m in ASSESSMENT_MODELS
if m.get("phase2") and m.get("api_key")
and m["name"] in ("Kimi-K2.6", "Step-3.7-Flash", "Qwen3.5-397b")),
None
)
model_name = fast_profile_cfg["name"] if fast_profile_cfg else "GLM 5.1"
print(f"\n{Fore.YELLOW}[Step 2/5] Building profile with {model_name}...{Style.RESET_ALL}")
llm = LLMClient()
try:
if fast_profile_cfg:
user_profile_json = llm.extract_profile_summary_fast(fast_profile_cfg, resume_text)
else:
user_profile_json = llm.extract_profile_summary(resume_text)
compact_profile = llm.build_compact_profile(user_profile_json)
print(f"{Fore.GREEN}β Profile extracted{Style.RESET_ALL}")
print(f"\n{Fore.CYAN}--- Your Profile Summary ---{Style.RESET_ALL}")
try:
pd = json.loads(user_profile_json)
print(f" Name: {pd.get('name', 'N/A')}")
print(f" Role: {pd.get('current_role', 'N/A')}")
print(f" Experience: {pd.get('total_experience_years', 'N/A')} years")
print(f" Skills: {', '.join(pd.get('core_skills', [])[:8])}")
except Exception:
print(compact_profile)
except Exception as e:
print(f"{Fore.YELLOW}β LLM profile extraction failed, using raw resume.{Style.RESET_ALL}")
user_profile_json = resume_text[:1500]
compact_profile = resume_text[:300]
# ββ 3. SCRAPE JOBS ββ
print(f"\n{Fore.YELLOW}[Step 3/5] Searching jobs across platforms...{Style.RESET_ALL}")
# Load dedup store
from src.job_history import is_duplicate, bulk_mark_seen, get_stats as dedup_stats, clear_old_entries
dedup_days = ASSESSMENT.get("dedup_days", 30)
clear_old_entries(days=90) # housekeep old entries
dstats = dedup_stats()
print(f" Dedup store: {dstats['total_seen']} jobs seen historically "
f"({dstats['seen_last_7_days']} in last 7 days) "
f"β skipping duplicates from last {dedup_days} days")
all_jobs: list[Job] = []
seen_urls: set = set()
skipped_dup = 0
scraper_map = {}
if PLATFORMS.get("linkedin"):
from src.scrapers.linkedin import LinkedInScraper
scraper_map["LinkedIn"] = LinkedInScraper()
if PLATFORMS.get("naukri"):
from src.scrapers.naukri import NaukriScraper
scraper_map["Naukri"] = NaukriScraper()
if PLATFORMS.get("indeed"):
from src.scrapers.indeed import IndeedScraper
scraper_map["Indeed"] = IndeedScraper()
if PLATFORMS.get("glassdoor"):
from src.scrapers.glassdoor import GlassdoorScraper
scraper_map["Glassdoor"] = GlassdoorScraper()
roles_to_search = JOB_SEARCH["roles"][:3]
locations_to_search = [JOB_SEARCH["locations"][0], "Bangalore", "Remote"]
for platform_name, scraper in scraper_map.items():
print(f"\n {Fore.CYAN}Scraping {platform_name}...{Style.RESET_ALL}")
platform_jobs = []
for role in roles_to_search:
for location in locations_to_search:
try:
jobs = scraper.search(role, location, max_results=10)
for job in jobs:
if not job.url or job.url in seen_urls:
continue
# PM-only filter at scrape time
if not scraper.is_pm_role(job.title):
continue
# Dedup check
if dedup_days > 0 and is_duplicate(job.url, days=dedup_days):
skipped_dup += 1
continue
seen_urls.add(job.url)
platform_jobs.append(job)
except Exception as e:
print(f" {Fore.RED}Error ({platform_name}, {role}, {location}): {e}{Style.RESET_ALL}")
time.sleep(1)
# Fetch descriptions for jobs that don't have them
needs_desc = [j for j in platform_jobs if not j.description][:15]
if needs_desc:
with tqdm(total=len(needs_desc), desc=f" Fetching {platform_name} descriptions", leave=False) as pbar:
for job in needs_desc:
try:
scraper.get_job_details(job)
except Exception:
pass
pbar.update(1)
print(f" {Fore.GREEN}β {platform_name}: {len(platform_jobs)} new jobs{Style.RESET_ALL}")
all_jobs.extend(platform_jobs)
if skipped_dup > 0:
print(f" {Fore.YELLOW}Skipped {skipped_dup} duplicate jobs (seen in last {dedup_days} days){Style.RESET_ALL}")
# Test mode β cap at N jobs for quick validation
if ASSESSMENT.get("test_mode") and all_jobs:
limit = ASSESSMENT.get("test_jobs_limit", 5)
all_jobs = all_jobs[:limit]
print(f"\n{Fore.YELLOW}TEST MODE: capped to {len(all_jobs)} jobs{Style.RESET_ALL}")
print(f"\n{Fore.GREEN}Total unique jobs collected: {len(all_jobs)}{Style.RESET_ALL}")
if not all_jobs:
print(f"{Fore.RED}No jobs found. Check internet connection.{Style.RESET_ALL}")
sys.exit(1)
# ββ 4. ASSESS JOBS β 4-model parallel pool ββ
print(f"\n{Fore.YELLOW}[Step 4/5] Assessing jobs with 4-model AI pool...{Style.RESET_ALL}")
print(f"{Fore.CYAN}Models: {', '.join(m['name'] for m in ASSESSMENT_MODELS if m.get('api_key'))}{Style.RESET_ALL}")
t_assess = time.time()
model_pool = ModelPool(ASSESSMENT_MODELS)
assessor = JobAssessor(model_pool, compact_profile)
assessed_jobs = assessor.assess_all(all_jobs)
t_elapsed = time.time() - t_assess
scores = [j.get("relevance_score", 0) for j in assessed_jobs]
print(f"\n{Fore.CYAN}Assessment done in {t_elapsed:.0f}s{Style.RESET_ALL}")
print(f" High Priority (8-10): {sum(1 for s in scores if s >= 8)}")
print(f" Good Match (6-7): {sum(1 for s in scores if 6 <= s <= 7)}")
print(f" Lower Match (<6): {sum(1 for s in scores if s < 6)}")
if assessed_jobs:
top = assessed_jobs[0]
print(f" Top Job: {top.get('title')} at {top.get('company')} β Score {top.get('relevance_score')}/10")
# ββ 5. GENERATE RESUMES (all jobs) + ATS SCORES ββ
# ββ Mark all new jobs as seen (dedup for next run) ββ
bulk_mark_seen(all_jobs)
print(f" {Fore.CYAN}Marked {len(all_jobs)} jobs in dedup store (won't repeat next run){Style.RESET_ALL}")
print(f"\n{Fore.YELLOW}[Step 5/6] Generating resumes + ATS scoring...{Style.RESET_ALL}")
# Pick fastest reliable model for LLM keyword extraction
# (Kimi ~5s or Step-3.7-Flash ~8s β both much faster than GLM's 234s)
fast_cfg = next(
(m for m in ASSESSMENT_MODELS
if m.get("phase2") and m.get("api_key")
and m["name"] in ("Kimi-K2.6", "Step-3.7-Flash", "Qwen3.5-397b")),
None
)
if fast_cfg:
print(f" {Fore.CYAN}LLM keyword extraction: {fast_cfg['name']}{Style.RESET_ALL}")
customizer = ResumeCustomizer(llm, resume_text, OUTPUT["resumes_dir"],
fast_model_cfg=fast_cfg)
assessed_jobs = customizer.customize_for_jobs(
assessed_jobs,
min_score_for_llm=ASSESSMENT["min_score_for_llm_resume"],
max_llm_resumes=ASSESSMENT["max_llm_resumes"],
generate_all=ASSESSMENT["generate_all_resumes"],
)
llm_resumes = sum(1 for j in assessed_jobs if j.get("resume_generated") == "LLM Tailored")
tmpl_resumes = sum(1 for j in assessed_jobs if j.get("resume_generated") == "Template")
print(f" LLM-tailored: {llm_resumes} | Template: {tmpl_resumes}")
# ββ 6. WRITE GOOGLE SHEET + EXCEL ββ
# Resumes are stored locally (date-based folder) β no Drive upload needed.
print(f"\n{Fore.YELLOW}[Step 6/6] Writing to Google Sheet + Excel...{Style.RESET_ALL}")
batch_label = datetime.now().strftime("%Y-%m-%d %H:%M")
sheet_ok = False
try:
from src.gsheets import write_jobs_to_sheet, get_sheet_url
sheet_ok = write_jobs_to_sheet(
assessed_jobs,
sheet_id=GOOGLE["sheet_id"],
tab_name=GOOGLE["sheet_tab"],
batch_label=batch_label,
)
if sheet_ok:
sheet_url = get_sheet_url(GOOGLE["sheet_id"])
print(f" {Fore.GREEN}β Google Sheet updated: {sheet_url}{Style.RESET_ALL}")
else:
print(f" {Fore.YELLOW}β Google Sheet not updated (run connect_google.py to connect){Style.RESET_ALL}")
except Exception as e:
print(f" {Fore.YELLOW}β Google Sheet skipped: {e}{Style.RESET_ALL}")
# Always generate local Excel
reporter = ExcelReporter(OUTPUT["excel_path"])
excel_path = reporter.generate(assessed_jobs)
# ββ DONE ββ
print(f"\n{Fore.GREEN}{'='*58}")
print(f" JOB SEARCH COMPLETE! {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print(f"{'='*58}{Style.RESET_ALL}")
print(f" Total Jobs: {len(assessed_jobs)}")
print(f" Assessed in: {t_elapsed:.0f}s")
print(f" LLM Resumes: {llm_resumes}")
print(f" Template Resumes: {tmpl_resumes}")
print(f" Excel Report: {excel_path}")
print(f" Resumes Folder: {OUTPUT['resumes_dir']}{batch_label[:10]}/")
if sheet_ok:
print(f" Google Sheet: {get_sheet_url(GOOGLE['sheet_id'])}")
print(f"\n{Fore.CYAN}All jobs written with direct apply links!{Style.RESET_ALL}\n")
try:
os.startfile(os.path.abspath(excel_path))
except Exception:
pass
if __name__ == "__main__":
main()
|