File size: 14,159 Bytes
7ff6662
 
 
 
 
945ae45
 
7ff6662
 
 
 
 
 
 
 
 
 
 
 
 
 
b15fd58
df898a6
 
b15fd58
df898a6
 
b15fd58
 
df898a6
7ff6662
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b617fcc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53c490d
b617fcc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7ff6662
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b617fcc
 
 
 
 
 
 
7ff6662
 
 
 
 
b617fcc
 
7ff6662
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f479a80
 
 
47b43eb
 
 
 
 
 
 
 
 
 
 
 
 
2a811e0
 
 
 
 
 
 
 
 
 
f479a80
 
 
 
 
 
 
 
 
7ff6662
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4eafa75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
---
title: JAA ATS Tool
emoji: πŸ€–
colorFrom: blue
colorTo: indigo
sdk: docker
app_port: 7860
pinned: false
license: mit
---

# Job Automation Agent β€” PM Edition

Automated Product Manager job search, AI-powered assessment, ATS-optimized resume generation, and Google Sheets reporting β€” all in one pipeline.

---

## What It Does

| Step | What Happens |
|------|--------------|
| 1 | Parses your PDF resume (Kimi-K2.6, ~5s) |
| 2 | Scrapes PM-only jobs from **LinkedIn, Indeed, Glassdoor, Remotive, WeWorkRemotely** β€” ALL selected roles Γ— locations, last N days |
| 3 | Filters non-PM roles at scrape time; dedup by URL + (title, company) + 30-day SQLite history; platform-level total cap |
| 4 | Fetches FULL job descriptions in bulk (parallel HTTP / single browser session) |
| 5 | Assesses ALL jobs using 7 parallel AI models (10-model pool via NVIDIA API) |
| 6 | Generates ATS-optimized resumes for ALL jobs β€” DOCX **and PDF**, After β‰₯ Before guaranteed, target 95% |
| 7 | Writes everything to your Google Sheet with direct job links |
| 8 | Saves local Excel report + resumes in `data/output/resumes/YYYY-MM-DD/` |
| 9 | Saves run to history β€” reload past runs in one click from the History panel |

---

## Quick Start

```powershell
# 1. Install dependencies
pip install -r requirements.txt
playwright install chromium

# 2. Copy and fill in your API keys
copy .env.example .env
# Edit .env with your NVIDIA_API_KEY, GOOGLE_SHEET_ID, etc.

# 3. Place your resume PDF
# Copy your resume to: data/resume/resume.pdf

# 4. Connect Google (one-time browser login)
python connect_google.py

# 5. Run a test (5 jobs)
# In config.py: ASSESSMENT["test_mode"] = True
python main.py

# 6. Run full production mode (all 100+ PM jobs)
# In config.py: ASSESSMENT["test_mode"] = False
python main.py

# 7. Or use the Streamlit UI
streamlit run ui.py
```

---

## ever-jobs Integration (160+ Platforms)

This project integrates the [ever-jobs](https://github.com/ever-jobs/ever-jobs) NestJS service, which provides REST API access to 160+ job board scrapers.

### Prerequisites

- **Docker Desktop** (preferred): [Install Docker Desktop](https://docs.docker.com/desktop/install/windows-install/)
- OR **Node.js 24.x** for npm subprocess fallback

### Setup (one-time)

```bash
# Clone ever-jobs to vendor/ directory (done automatically during setup)
git clone https://github.com/ever-jobs/ever-jobs.git vendor/ever-jobs --depth=1

# Start via Docker (preferred)
cd vendor/ever-jobs && docker compose up -d

# Verify API is running
curl http://localhost:3001/health
# OR from Python:
python -c "from src.ever_jobs_bridge.server import is_running; print(is_running())"
```

### Automatic Startup

The pipeline calls `ensure_running()` automatically before any ever-jobs platforms are scraped. It tries Docker first, falls back to `npm run start` if Docker is unavailable.

### Platform Selection

The UI has a single **"🌐 Job Platforms"** section with three groups. Selecting LinkedIn, Indeed, Glassdoor, Remotive, WeWorkRemotely, or Naukri uses their dedicated high-quality scrapers; all other platforms go through the ever-jobs REST API.

| Group | Count | Description |
|-------|-------|-------------|
| Search Boards | 94 | General job boards. India-relevant defaults pre-selected. |
| ATS Platforms | 37 | Greenhouse, Lever, Workday etc. β€” companies post jobs here. NOT resume scoring. |
| Company Pages | 39 | Direct career pages (Flipkart, Swiggy, Amazon, Google, etc.) |

**India default platforms:** linkedin, naukri, indeed, glassdoor, google, bdjobs, internshala, bayt, iimjobs, foundit

### ATS Clarification

> **Important:** "ATS" in ever-jobs means Applicant Tracking System **PLATFORMS**
> (tools companies use to post jobs, like Greenhouse or Lever).
> It does **NOT** mean ATS resume scoring.
>
> Our ATS resume scoring (`src/ats_scorer.py`) uses a 70% JD keyword match +
> 30% resume quality hybrid and is **NOT changed** by this integration.

### Performance Notes

| Selection | Expected Time |
|-----------|---------------|
| Default 10 platforms | ~1–3 minutes |
| 30 platforms | ~3–5 minutes |
| 100+ platforms | 5–10 minutes (warning shown in UI) |

Playwright-based scrapers (some company pages) are inherently slower than API-based boards.

### Cross-Platform Deduplication

In addition to URL-based dedup, a **content fingerprint** (SHA-256 of normalized title+company) catches cross-platform duplicates. For example, the same "Product Manager at Google" posting on LinkedIn AND Greenhouse (different URLs) is detected and deduplicated.

---

## Project Structure

```
Job Automation Agent/
β”œβ”€β”€ main.py                    # Main pipeline (6-step orchestrator)
β”œβ”€β”€ config.py                  # All configuration β€” models, platforms, ATS settings
β”œβ”€β”€ ui.py                      # Streamlit UI (4 tabs: Search, Results, Job Details, Research)
β”œβ”€β”€ connect_google.py          # One-time Google OAuth setup
β”œβ”€β”€ setup_google.py            # Service account alternative
β”œβ”€β”€ requirements.txt
β”œβ”€β”€ .env                       # API keys (never commit)
β”œβ”€β”€ .env.example               # Template for .env
β”‚
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ resume_parser.py       # PDF β†’ plain text (pdfplumber)
β”‚   β”œβ”€β”€ llm_client.py          # GLM 5.1 wrapper (profile extract, resume customize)
β”‚   β”œβ”€β”€ model_pool.py          # 10-model parallel AI pool (NVIDIA API)
β”‚   β”œβ”€β”€ job_assessor.py        # Phase 1 keyword scoring + Phase 2 LLM assessment
β”‚   β”œβ”€β”€ job_history.py         # SQLite dedup store (data/job_history.db)
β”‚   β”œβ”€β”€ ats_scorer.py          # Hybrid ATS scoring (70% JD match + 30% quality)
β”‚   β”œβ”€β”€ resume_customizer.py   # LLM-tailored DOCX resume generator
β”‚   β”œβ”€β”€ gsheets.py             # Google Sheets + Drive upload
β”‚   β”œβ”€β”€ excel_reporter.py      # Local Excel report
β”‚   β”œβ”€β”€ odysseus_llm_core.py   # Deep research LLM core
β”‚   β”‚
β”‚   β”œβ”€β”€ ever_jobs_bridge/       # ever-jobs NestJS REST API adapter
β”‚   β”‚   β”œβ”€β”€ __init__.py        # Package init
β”‚   β”‚   β”œβ”€β”€ server.py          # Docker/npm server lifecycle (start/stop/health)
β”‚   β”‚   β”œβ”€β”€ client.py          # HTTP client for POST /api/jobs/search
β”‚   β”‚   β”œβ”€β”€ mapper.py          # IJob JSON β†’ Job dataclass mapper
β”‚   β”‚   └── platforms.py       # 170 platform catalog with group metadata
β”‚   β”‚
β”‚   β”œβ”€β”€ scrapers/
β”‚   β”‚   β”œβ”€β”€ base.py            # Job dataclass + BaseScraper + is_pm_role() filter
β”‚   β”‚   β”œβ”€β”€ linkedin.py        # LinkedIn scraper (requests + BeautifulSoup)
β”‚   β”‚   β”œβ”€β”€ indeed.py          # Indeed scraper (Playwright for JS rendering)
β”‚   β”‚   β”œβ”€β”€ glassdoor.py       # Glassdoor scraper (Playwright)
β”‚   β”‚   β”œβ”€β”€ naukri.py          # Naukri (disabled β€” blocked by Akamai)
β”‚   β”‚   └── ever_jobs.py       # EverJobsScraper (REST adapter for 160+ platforms)
β”‚   β”‚
β”‚   └── research/
β”‚       β”œβ”€β”€ deep_researcher.py # Odysseus IterResearch engine (Thinkβ†’Searchβ†’Extractβ†’Synthesize)
β”‚       └── search.py          # DuckDuckGo + Bing fallback, 12h cache
β”‚
└── data/
    β”œβ”€β”€ resume/resume.pdf      # Your resume (add this)
    β”œβ”€β”€ job_history.db         # Dedup SQLite DB (auto-created)
    β”œβ”€β”€ research_cache/        # 12h DuckDuckGo result cache
    └── output/
        β”œβ”€β”€ resumes/           # Generated DOCX resumes (Company_JobTitle.docx)
        └── reports/           # Excel reports
```

---

## Configuration (`config.py`)

### AI Models (10-model pool via NVIDIA API)

| Model | Speed | Phase 2 | Notes |
|-------|-------|---------|-------|
| Kimi-K2.6 | ~5s/batch | βœ… | Fastest, handles most work |
| Step-3.7-Flash | ~8-35s | βœ… | |
| Qwen3.5-397b | ~9s | βœ… | |
| Qwen3.5-122b-v2 | ~12s | βœ… | |
| GPT-OSS-120b | ~11s | βœ… | |
| Qwen3.5-122b | ~40s | βœ… | |
| DeepSeek-v4-Pro | ~42s | βœ… | |
| DeepSeek-v4-Flash | ~229s | ❌ | Too slow for phase 2 |
| GLM-5.1 | ~234s | ❌ | Used for resume parsing only |
| MiniMax-M2.7 | ~908s | ❌ | Blocked/rate-limited |

### Key Settings

```python
ASSESSMENT = {
    "min_score_for_llm_resume": 6,   # LLM-tailored resume for score >= this
    "generate_all_resumes":     True, # Template resume for ALL PM jobs
    "max_llm_resumes":          30,   # Max LLM resumes per run
    "dedup_days":               30,   # Skip jobs seen in last 30 days
    "test_mode":                True, # ← Set False for full production run
    "test_jobs_limit":          5,    # Max jobs in test mode
}
```

---

## ATS Scoring Method

Hybrid scoring: **70% JD Match + 30% Resume Quality**

- **JD Match (70%)**: Extract keywords FROM the specific job description β†’ match against resume using word-boundary regex (`(?<!\w)kw(?!\w)`) β€” same approach as Resume-Matcher
- **Resume Quality (30%)**: 6-factor structural analysis (measurable achievements, contact info, education, etc.)
- **Iterative optimization**: Up to 3 LLM attempts to push score up, gap report fed back each iteration
- **Fallback**: `_inject_missing_keywords()` weaves still-missing terms naturally

### Keyword extraction is JD-driven, skills-only, and honest (industry standard)

- Keywords are extracted **from each JD itself** (not a fixed stored list), but
  **skills-only**: a term is kept only if it's a recognised skill/tool/method/
  domain/soft-skill in our gazetteer. Prose nouns (Goals, Authority, Enterprise,
  Productivity…) can never become "keywords" β€” exactly how real checkers like
  Jobalytics work (they match against a curated skills list, not every word).
- **Dedicated, categorized SKILLS section** (Tools & Analytics / Methodologies /
  Domains / Core Competencies) β€” the #1 ATS keyword vehicle, placed after the
  summary, parsed and counted by Jobalytics/Resume Worded.
- **Quality over quantity**: cover the JD's real skills (~20–26), each 1–3Γ—,
  across the skills section + summary + woven bullets. Best practice is 15–25
  keywords at 60–80% coverage β€” NOT 100 stuffed terms.
- **No cap, distributed smartly.** Every meaningful missing keyword is kept β€”
  woven into relevant bullets first (the ideal, never-penalised place), then any
  remainder spread across several short sentences (each its own paragraph, ≀10
  items). A single long comma-dump is *deliberately not* produced:
  `_strip_keyword_spam` removes any 15+-separator line before scoring, exactly as
  real ATS checkers and recruiters discount stuffing β€” so distributing keeps them
  all counted without looking like spam.
- **Buzzwords removed.** Vague abstractions (innovation, solutions, tools,
  leadership, leverage, scalable…) are never injected β€” real checkers like Resume
  Worded penalise them.
- **Honest expectation:** 90%+ is reached on JDs that genuinely fit the
  candidate's background. Out-of-domain JDs score honestly lower β€” that reflects
  reality (and matches third-party checkers), rather than a faked number.
- The displayed score is a **conservative estimate** (leans low vs our raw
  internal coverage); always verify the final resume on Jobalytics/Simplify.

**Benchmark results** (correctly differentiates β€” full resume + capable LLM):
- In-domain PM JDs (Airtel, generic PM) β†’ ~83–87
- Out-of-domain JDs (SecOps, lending) β†’ ~60–70, honestly

---

## Google Sheet Columns

| Column | Description |
|--------|-------------|
| Batch Date | When the run happened |
| Rank | Score rank within this batch |
| Job Title / Company / Location | Job details |
| Platform | LinkedIn / Indeed / Glassdoor |
| Relevance Score | AI assessment (1–10) |
| ATS Before (%) | ATS score on original resume |
| ATS After (%) | ATS score on tailored resume |
| ATS Improvement | After βˆ’ Before |
| Resume Quality | Structural quality score |
| Priority | High / Medium / Low |
| Matching / Missing Skills | Gap analysis |
| AI Recommendation | LLM reasoning |
| Apply Link | Direct job URL (clickable) |
| Resume Link | Google Drive link to tailored resume |
| Application Status | Dropdown: Not Applied β†’ Offer |

---

## Environment Variables (`.env`)

```
NVIDIA_API_KEY=nvapi-...          # GLM 5.1 + primary key
NVIDIA_API_KEY_2=nvapi-...        # DeepSeek-v4-Pro, MiniMax
NVIDIA_API_KEY_3=nvapi-...        # Kimi-K2.6
NVIDIA_API_KEY_4=nvapi-...        # Qwen3.5-122b
NVIDIA_API_KEY_5=nvapi-...        # GPT-OSS-120b
NVIDIA_API_KEY_6=nvapi-...        # DeepSeek-v4-Flash
NVIDIA_API_KEY_7=nvapi-...        # Qwen3.5-397b, Qwen3.5-122b-v2
NVIDIA_API_KEY_8=nvapi-...        # Step-3.7-Flash
GOOGLE_SHEET_ID=1Ehxt3eo...       # Your Google Sheet ID
RESUME_PATH=data/resume/resume.pdf
```

---

## Logging & Debugging

Every pipeline run writes a timestamped log to `data/logs/run_YYYY-MM-DD_HH-MM-SS.log`.

To diagnose failures:
1. Run a search from the UI
2. Switch to the **πŸ“‹ Logs** tab
3. Errors show in red, warnings in yellow
4. Use **Download Full Log File** to share or inspect offline
5. Previous runs are also listed in the selector

The log captures:
- Every scrape attempt (role, location, raw result count)
- Full Python tracebacks on any exception
- All `print()` output from scrapers and LLM clients
- Playwright browser output

---

## Known Issues / Pending

| Issue | Status | Notes |
|-------|--------|-------|
| Google Drive upload `'Client' object has no attribute 'auth'` | Pending fix | gspread auth method mismatch |
| LLM resumes = 0 (GLM timeout during customization) | Pending fix | Switch to Kimi/Step for resume generation |
| Naukri blocked by Akamai | Permanent skip | Returns 406 / "Access Denied" with Playwright |
| Google OAuth "Access blocked" | Fixed | Add email as test user at GCP console |

---

## Running the UI

```powershell
streamlit run ui.py
# Opens at http://localhost:8501

# Tabs:
# 1. Search     β€” configure and run the full pipeline
# 2. Results    β€” view all assessed jobs with scores
# 3. Job Details β€” expand any job for full AI breakdown
# 4. Deep Research β€” Odysseus engine to research companies
```