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()