JAA-ATS-Tool / main.py
saitejatirunagari's picture
Initial commit: JAA ATS Tool β€” PM job search + AI assessment + ATS resume gen
7ff6662
Raw
History Blame Contribute Delete
11.9 kB
#!/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()