Skip to content

Repository files navigation

Jobseeker AI — Localized Cybersecurity Resume Automation

v0.0.3-beta — Security hardening, full job description pipeline, FSM/JSON strictness, dependency sweep to latest, Python 3.13.

Python FastAPI Streamlit vLLM Qdrant Redis Celery Playwright Docker PyTorch CUDA License


Overview

Jobseeker AI is a fully local, privacy-first pipeline that:

  1. Scrapes cybersecurity job listings from major boards via Playwright
  2. Parses your resume (PDF / DOCX / TXT / JSON) into a structured schema
  3. Matches jobs to your resume using semantic vector search + hard metadata filtering (certs, clearance, skills)
  4. Rewrites your resume for each job using Foundation-Sec-8B AWQ with Outlines-constrained JSON output (zero hallucination)
  5. Submits the tailored application via Playwright DOM automation
  6. Tracks submission history through a Streamlit dashboard

No data ever leaves your machine.


Architecture

┌──────────────┐    ┌──────────────┐    ┌───────────────┐
│  Streamlit   │───▶│   Celery     │───▶│   Playwright  │
│  Dashboard   │    │   Workers    │    │  Submission   │
│  :8501       │    │              │    │               │
└──────┬───────┘    └──────┬───────┘    └───────────────┘
       │                   │
       ▼                   ▼
┌──────────────┐    ┌──────────────┐    ┌───────────────┐
│  Orchestrator│───▶│    vLLM      │    │    Qdrant     │
│  (FastAPI)   │    │  :8000       │    │   :6333       │
│  :8001       │    │  Foundation- │    │  Vector DB    │
└──────────────┘    │  Sec-8B AWQ  │    └───────────────┘
                    └──────────────┘

Service Responsibilities

Service Port Role
Streamlit 8501 User dashboard — upload, match, review, submit
Orchestrator (FastAPI) 8001 REST API — coordinates all services
vLLM 8000 Foundation-Sec-8B inference engine
Qdrant 6333 Vector storage for jobs and resumes
Redis 6379 Celery broker + result backend
Celery Workers Async scraping and submission tasks

Hardware Requirements

Component Minimum
GPU NVIDIA RTX 4070 (12 GB VRAM) or better
RAM 32 GB system RAM
Storage 50 GB free (model cache + Qdrant data)
OS Linux (Ubuntu 22.04+)
NVIDIA Driver 535+
Docker 24+ with nvidia-container-toolkit

VRAM Budget (12 GB)

Component Allocation
System / PyTorch overhead 2.0 GB
Embedding model (mxbai-embed-large-v1) 1.5 GB
vLLM generative engine 8.4 GB
Buffer 0.1 GB

Quick Start

1. Prerequisites

# Install NVIDIA Container Toolkit
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo systemctl restart docker

# Verify GPU access
docker run --rm --gpus all nvidia/cuda:12.1-base nvidia-smi

2. Setup

git clone https://github.com/Zenetusken/Jobseeker.git
cd Jobseeker

# Copy and edit environment config
cp .env.example .env

# Build and start all services
make up

First launch downloads the AWQ-quantized Foundation-Sec-8B (~5 GB). Allow 5–15 minutes.

3. Access

Interface URL
Streamlit Dashboard http://localhost:8501
API Docs (Swagger) http://localhost:8001/docs
vLLM API http://localhost:8000/v1
Qdrant Dashboard http://localhost:6333/dashboard

Workflow

Step 1 — Upload Your Resume

My Resumes → Upload PDF / DOCX / TXT, or paste a structured JSON resume.

Step 2 — Add Job Listings

Job Board → Browse auto-scraped jobs (runs every 6 h by default) or ingest manually via API.

Step 3 — Match

Staged Applications → Select resume → Run Matching.
Vector similarity search over all ingested jobs, filtered by hard criteria (clearance, required certs, skills).

Step 4 — Tailor

Click Tailor Resume on any match. Foundation-Sec-8B rewrites every bullet point for the target job.
Output is constrained by an Outlines FSM — malformed JSON is structurally impossible.

Step 5 — Review & Submit

Review & Approve → Side-by-side diff of every change → Enter application URL → Approve & Submit.
Playwright fills the form fields, attaches files, and submits in the background.

Step 6 — Track

Application History → Live status per submission (queued / submitted / failed).


Key Design Decisions

Decision Rationale
Foundation-Sec-8B AWQ Cybersecurity-specialist LLM (CTIBench-RCM 75.3%), fits 8.4 GB, Llama 3.1 JSON backbone
vLLM + Marlin kernels PagedAttention + Marlin gives 10.9× AWQ throughput
Outlines pre-generation FSM Zero-retry structured output — invalid JSON is impossible
Temperature = 0.0, seed = 42 Fully deterministic — no hallucinated credentials, reproducible diffs
Qdrant over Chroma/FAISS Native payload filtering, persistent storage, Rust-backed performance
Celery + Redis Mature async task queue with scheduling, retries, and result tracking
mxbai-embed-large-v1 1024-dim, MRL-trained — best-in-class retrieval on MTEB at this size

Configuration

All options live in .env (copied from .env.example):

# Model
VLLM_MODEL_NAME=fdtn-ai/Foundation-Sec-8B-Reasoning
VLLM_GPU_MEMORY_UTILIZATION=0.7

# Qdrant
QDRANT_URL=http://qdrant:6333
QDRANT_COLLECTION_JOBS=job_descriptions
QDRANT_COLLECTION_RESUMES=resumes

# Redis / Celery
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=change-me-strong-redis-password   # required in production

# Scraper
SCRAPER_SCHEDULE_HOURS=6
SCRAPER_SOURCES=indeed,linkedin,dice

# Playwright
PLAYWRIGHT_HEADLESS=true
PLAYWRIGHT_TIMEOUT_MS=30000
PLAYWRIGHT_STEALTH_ENABLED=true

# Security — REQUIRED for production (change ALL values)
API_KEY=change-me-to-a-long-random-secret   # X-API-Key header auth; empty = dev mode
API_DEBUG=false                              # set true to enable /docs, /redoc
ALLOWED_ORIGINS=                            # blank = deny all cross-origin requests
QDRANT_API_KEY=change-me-qdrant-api-key

# Upload / validation limits
MAX_UPLOAD_BYTES=10485760    # 10 MB
MAX_DESCRIPTION_LENGTH=50000
MAX_BATCH_SIZE=100

Generate secrets with:

python3 -c "import secrets; print(secrets.token_hex(32))"

Makefile Commands

make up             # Build and start all services
make down           # Stop all services
make logs           # Tail all service logs
make logs-vllm      # Stream vLLM engine logs only
make status         # Health check across all services
make init-db        # Initialize Qdrant collections
make reset-db       # WARNING: wipe all stored jobs and resumes
make clean          # Remove containers, volumes, and build cache
make check-secrets  # Verify API_KEY, REDIS_PASSWORD, QDRANT_API_KEY are not placeholders

Testing

The project ships with a comprehensive pytest suite: 386 tests, 77% coverage (Playwright browser automation excluded by design, zero warnings).

# Install test dependencies
pip install -r requirements.test.txt

# Run full suite with coverage
python -m pytest tests/

# Run a specific tier
python -m pytest tests/tier1_critical/
python -m pytest tests/tier2_services/
python -m pytest tests/tier3_api/
python -m pytest tests/tier4_wiring/

Test Structure

tests/
├── conftest.py                      # Session fixtures + 3rd-party warning filters
├── tier1_critical/                  # Pure-logic unit tests (no mocks)
│   ├── test_dom_mapper.py
│   ├── test_metadata_extractor.py
│   ├── test_outlines_constraint.py  # Schema validity, FSM self-containment, prompt hints
│   ├── test_pdf_generator.py
│   ├── test_resume_schema.py
│   ├── test_rewrite_schema.py
│   └── test_security_validators.py  # SSRF guard, upload size, filename sanitizer, UUID
├── tier2_services/                  # Service unit tests (mocked Qdrant / embeddings / vLLM)
│   ├── test_embedding.py
│   ├── test_ingest.py
│   ├── test_matcher.py
│   ├── test_parser.py
│   ├── test_pdf_generator.py
│   ├── test_qdrant_init.py
│   ├── test_rewriter.py
│   ├── test_scraper.py              # HTML→Markdown, description fetching, pipeline integration
│   ├── test_settings.py             # SettingsConfigDict migration, zero Pydantic warnings
│   └── test_tasks.py
├── tier3_api/                       # FastAPI integration tests (httpx ASGITransport)
│   ├── test_api_auth.py             # API key enforcement (401/200, dev-mode bypass)
│   ├── test_api_jobs.py
│   ├── test_api_match.py
│   ├── test_api_resumes.py
│   ├── test_api_rewrite.py
│   └── test_api_submit.py
└── tier4_wiring/                    # End-to-end pipeline wiring tests
    └── test_integration_wiring.py

Coverage Summary

Module Coverage
config/settings.py 100%
services/api/security.py 100%
services/api/validators.py 100%
services/automation/dom_mapper.py 100%
services/embeddings/embedding_service.py 100%
services/matching/matcher.py 100%
services/resume/schema.py 100%
services/rewrite/schema.py 100%
services/scraper/ingest.py 100%
services/tasks/* 100%
services/qdrant/init_collections.py 97%
services/api/routes/* 85–91%
services/rewrite/rewriter.py 90%
services/rewrite/outlines_constraint.py 88%
services/automation/submitter.py 10% (requires live Playwright)
services/scraper/scraper.py 12% (requires live Playwright; HTML→Markdown helpers 100%)

Project Structure

Jobseeker/
├── docker-compose.yml          # Service orchestration
├── Dockerfile.orchestrator     # FastAPI + Celery + Playwright image
├── Dockerfile.streamlit        # Streamlit frontend image
├── Makefile                    # Developer commands
├── pytest.ini                  # Test configuration (cov threshold: 77%)
├── requirements.txt            # Core dependencies
├── requirements.test.txt       # Test-only dependencies
├── requirements.streamlit.txt  # Frontend dependencies
├── .env.example                # Environment template
├── config/
│   └── settings.py             # Pydantic BaseSettings
├── services/
│   ├── api/                    # FastAPI orchestrator
│   │   ├── main.py             # App factory, lifespan, rate-limit + security-header middleware
│   │   ├── security.py         # X-API-Key dependency
│   │   ├── validators.py       # SSRF guard, upload size, filename sanitizer, UUID check
│   │   └── routes/             # jobs · resumes · match · rewrite · submit
│   ├── embeddings/             # mxbai-embed-large-v1 singleton
│   ├── qdrant/                 # Collection schema + init
│   ├── scraper/                # Playwright scrapers + metadata extractor
│   ├── resume/                 # PDF/DOCX/TXT parser + Pydantic schema
│   ├── matching/               # Cosine search + hard-filter engine
│   ├── rewrite/                # vLLM rewrite + Outlines FSM constraint
│   ├── tasks/                  # Celery app, scrape task, submit task, batch-match task
│   └── automation/             # Playwright submitter + DOM mapper + PDF gen
├── frontend/
│   ├── app.py                  # Streamlit entry point
│   ├── api_client.py           # Typed HTTP client for orchestrator
│   ├── pages/                  # Dashboard, Job Board, Resumes, Match, Review, History
│   └── components/             # Diff viewer component
└── data/                       # Persistent volumes (gitignored)
    ├── qdrant_storage/
    ├── uploads/
    └── outputs/

What's New in v0.0.3-beta

Area Change
Security API key auth (X-API-Key) on every route; HTTP 401 on mismatch; dev bypass when API_KEY unset
Security SecurityHeadersMiddleware: 5 hardening headers on every response
Security RateLimitMiddleware: per-route sliding-window limits; HTTP 429 + Retry-After
Security SSRF guard (validate_job_url), upload-size guard, filename sanitizer, UUID validator across all routes
Security CORS tightened to explicit origin list, methods, and headers; Swagger UI gated behind API_DEBUG=true
Scraper Two-pass design: real job URLs captured; full HTML descriptions fetched concurrently via asyncio.gather
Scraper _html_to_markdown() converts job detail HTML to clean ATX Markdown (script/style stripped, truncated)
Scraper markdownify==1.2.2 restored; description stubs eliminated throughout the pipeline
FSM/JSON _extract_json_fallback() removed — constraint violation raises RuntimeError immediately
FSM/JSON validate_schema_self_contained() pre-flight on every vLLM request; dangling $refValueError
FSM/JSON build_json_schema_description inlines nested TailoredExperience/TailoredBullet fields in the LLM prompt
Deps All 22 packages bumped to latest; playwright-stealth==2.0.3 API fix (Stealth().apply_stealth_async)
Infra Python 3.13 in both Docker images; vLLM v0.20.2; Qdrant v1.18.0; Redis 8.6.3
Config class Config:model_config = SettingsConfigDict(…) (Pydantic v2 convention)
Tests 362 → 386 tests; 5 new test files; zero warnings

What's New in v0.0.2-beta

Area Change
Qdrant Thread-safe client singleton; all point lookups use client.retrieve() — O(1) vs O(N) scroll
API routes delete_job / delete_resume use PointIdsList (was broken); get_job / get_resume fixed
Matching _get_resume_payload replaced scroll-loop with O(1) retrieve
Rewriting rewrite_resume_for_job() accepts match_score param — skips redundant vector search
History Real submission history persisted to Redis; live Celery state enrichment; full table UI
Scraping playwright-stealth applied to all three scrapers and the form submitter
Batch match New batch_match_new_jobs Celery task auto-dispatched after every job ingest batch
Config celery_broker_url / celery_result_backend derived from redis_host/redis_port @property
Docker GPU reservation added to orchestrator + celery-worker; base image → python:3.12-slim
DRY pdf_generator.py refactored; dead create_outlines_generator() removed; metadata regex cleaned up
Frontend Delete resume button fixed (api_delete()); current_resume_id propagated through session state

Limitations & Roadmap

  • Single-user MVP — multi-user auth and resume namespacing planned for v0.1.0
  • LinkedIn scraping — fragile against anti-bot measures; playwright-stealth mitigates but manual upload is recommended for critical roles
  • DOM mapping — heuristic-based; complex multi-step ATS forms may need manual review
  • PDF generation — basic ReportLab layout; LaTeX / HTML templates planned for v0.1.0
  • Model fallback — if Foundation-Sec-8B is unavailable: VLLM_MODEL_NAME=meta-llama/Llama-3.1-8B-Instruct
  • Rate limiter — in-process, per-worker; not shared across Celery workers or multiple orchestrator replicas. A Redis-backed limiter is planned for v0.1.0

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages