Skip to content

Repository files navigation

Wayfarer — AI-Powered Job Search Automation Platform

Wayfarer Social Preview

A locally-hosted, RAG-driven job search platform built in three connected stages: a web search agent, a RAG-based ATS resume checker with redlining, and a live job-posting matcher. We write and own the retrieval, scoring, and matching logic directly instead of delegating it to an agent's context window.

Built to be used for the author's own job search. Calls free-tier APIs when local inference would blow the 4 GB VRAM budget.

Current features: Fresher Mode, employment-type classification, LinkedIn integration, background pipeline maintenance, Tesseract OCR for embedded images, post age grading (fresh/stale/re-stamped/ghost badges), pipeline analytics (Kanban board + stats), cover letter + follow-up email drafting, application tracker with notification bell, and a unified minimalist UI with tactical dark mode.

GitHub stars GitHub license Docker Python React TypeScript Tests


One-Command Setup

curl -sSL https://raw.githubusercontent.com/anubhavsanket/wayfarer/main/setup.sh | bash

Or clone manually:

git clone https://github.com/anubhavsanket/wayfarer.git
cd wayfarer
bash setup.sh

The script clones the repo, creates .env, pulls Docker images, starts all 5 services, and pulls the embedding model. Takes about 2 minutes on first run.

After setup, open http://localhost:3000 and go to the Settings tab to enter your API keys, or edit .env directly.


Table of contents

  1. What Wayfarer does
  2. Architecture
  3. Quick start
  4. Local development
  5. Configuration
  6. API reference
  7. The job board registry
  8. Testing
  9. Release highlights
  10. Known limitations

What Wayfarer does

Stage What it answers Key endpoint
1. Web Search Agent "Search the web for X and synthesize an answer with sources." POST /api/v1/search
2. ATS Resume Checker "Check my resume against this JD, tell me exactly what to change and why." POST /api/v1/resume/check
3. Job Matcher "Show me live postings ranked by fit, with apply links." GET /api/v1/jobs/match

Every output keeps you in the loop. Wayfarer evaluates, ranks, and drafts. You submit applications, fill forms, and click through to third-party sites yourself.

Differentiators

  • Owned RAG pipeline — embeddings, vector store, and hybrid scoring run in-process. The retrieval code is hand-written rather than scaffolded by an agent.
  • Honest substitution — each keyword suggestion in Stage 2 traces to a real resume bullet. Gaps get flagged, never silently filled.
  • Confidence-tiered redlinesVerified / Reworded / Gap. The Gap tier shows up in the output, never dropped.
  • Multi-provider inference — rate-limit-aware router with NVIDIA NIM → OpenRouter → local Ollama fallback. Free tiers only.
  • Post age gradingfresh / stale / re-stamped / ghost badges on JobMatch cards using SQLite post_history persistence (first-seen tracking, content-hash comparison, open-jobs pattern).
  • Job board registryconfig/job_boards.yaml drives Stage 3 discovery (bluedoor REST, LinkedIn HTML guest API).
  • Fresher Mode — filter postings to entry-level/junior roles using a small local LLM (qwen3:0.6b) for experience-level classification.
  • Posting age gradingfresh / stale / re-stamped / ghost badges on JobMatch cards using post_history persistence (first-seen tracking, content hash comparison). Integrated with open-jobs pattern.
  • Pipeline analytics — Kanban dashboard (/tracker) with conversion funnel stats (interview rate, avg match score, days-in-stage, source breakdown, notification bell for new/updated applications).
  • Cover letter + follow-up — Stage-aware draft generation with tone control, using ResumeGraph for token-efficient, grounded prompts.
  • Application tracker — Save/apply tracking with pipeline status updates, notes, resume linkage, followed by cover-letter and follow-up drafts.
  • Tesseract OCR — extracts text from embedded images in DOCX resumes (profile photos, diagrams, infographics) so the checker reads parts a plain parser would miss.
  • Unified UI aesthetic — clean, minimalist interface, tactical dark mode, animated score bars, sticker badges, anime.js entrance animations.

Architecture

                    ┌─────────────────────────────┐
                    │        LLM Router            │
                    │  NVIDIA NIM ⇄ OpenRouter      │
                    │  rate-limit-aware fallback    │
                    └───────────────┬───────────────┘
                                    │
        ┌───────────────────────────┼───────────────────────────┐
        │                           │                           │
┌───────▼────────┐      ┌───────────▼──────────┐     ┌───────────▼──────────┐
│   Stage 1       │      │       Stage 2         │     │       Stage 3         │
│  Search Agent   │      │  Resume/ATS Checker    │     │   Job Matcher         │
│                 │      │                        │     │                       │
│ Search API      │      │ Unstructured +         │     │ bluedoor + LinkedIn   │
│ (Tavily/Brave)  │      │ pdfplumber fallback    │     │ Fresher Mode          │
│ + Crawl4AI      │      │ Tesseract OCR          │     │ Hybrid scoring        │
│ fetch/clean     │      │ Confidence-tiered      │     │ Legitimacy checks     │
│                 │      │ redline generator      │     │ Background refresh    │
└─────────────────┘      └────────────────────────┘     └───────────────────────┘
        │                           │                           │
        └───────────────────────────┴───────────────────────────┘
                                    │
                        ┌───────────▼───────────┐
                        │        Qdrant          │
                        │  search_cache           │
                        │  resume_sections        │
                        │  job_postings           │
                        └─────────────────────────┘

Shared infrastructure (build once, used in all 3 stages):

  • LLM Routerbackend/app/llm_router.py. Every stage calls inference through this module, never directly against a provider. Supports 5 providers: NVIDIA NIM, OpenRouter, Ollama (local), LM Studio (local), and any custom OpenAI-compatible endpoint. Tracks per-provider rate limits and falls back automatically. Sanitizes multimodal payloads for text-only models.
  • Embedding layernomic-embed-text via Ollama, kept local. Embeddings are cheap enough on a GTX 1650 that they don't need router fallback.
  • Qdrant — one persistent store, three separate collections (search_cache, resume_sections, job_postings). Sharing the embedding space enables Stage 2 ↔ Stage 3 reuse.
  • Confidence scoringbackend/app/core/confidence.py. The Verified / Reworded / Gap classifier from the real-estate RAG project. Both Stage 2 and Stage 3 call the same match_keyword_to_bullet function.
  • Resume graphbackend/app/core/resume_graph.py. Graph-based structured resume memory for token-efficient per-posting matching.
  • RAG enginebackend/app/core/rag_engine.py. LlamaIndex-based RAG pipeline with Qdrant vector store integration.

Project layout

wayfarer/
├── backend/                         # FastAPI service
│   ├── app/
│   │   ├── main.py                  # FastAPI entry, health, lifespan
│   │   ├── config.py                # Pydantic Settings (.env-driven)
│   │   ├── context.py               # Per-request ContextVar for header overrides
│   │   ├── llm_router.py            # Multi-provider inference router
│   │   ├── exceptions.py            # Error handling, validation helpers
│   │   ├── vector_store.py          # Multi-collection Qdrant wrapper
│   │   ├── routers/
│   │   │   ├── health.py            # GET /health
│   │   │   ├── stage1.py            # POST /api/v1/search
│   │   │   ├── stage2.py            # POST /api/v1/resume/check, save
│   │   │   └── stage3.py            # POST /api/v1/jobs/match, refresh
│   │   ├── models/
│   │   │   ├── schemas.py           # Pydantic request/response models
│   │   │   └── job_boards.py        # Board registry models + connector
│   │   ├── core/
│   │   │   ├── confidence.py        # Tier classifier + match_keyword_to_bullet
│   │   │   ├── resume_graph.py      # Graph-based structured resume memory
│   │   │   └── rag_engine.py        # LlamaIndex RAG pipeline over Qdrant
│   │   ├── services/
│   │   │   ├── search_api.py        # Tavily + Brave clients
│   │   │   ├── web_fetch.py         # Crawl4AI concurrency-capped fetcher
│   │   │   ├── search_service.py    # Stage 1 orchestrator
│   │   │   ├── resume_parser.py     # PDF/DOCX parsing + Tesseract OCR + ATS sim
│   │   │   ├── ats_checker.py       # Stage 2 orchestrator
│   │   │   ├── resume_saver.py      # Save with/without overwrite
│   │   │   ├── resume_store.py      # Upload persistence
│   │   │   ├── job_matcher.py       # Stage 3 orchestrator + Fresher Mode + age grading
│   │   │   ├── grading.py           # Posting age grade (fresh/stale/re-stamped/ghost)
│   │   │   ├── cover_letter.py      # Cover letter draft with ResumeGraph
│   │   │   ├── follow_up.py         # Stage-aware follow-up email draft
│   │   │   ├── jobs_queue.py        # Redis-backed background refresh queue
│   │   │   └── legitimacy.py        # Ghost / no-sponsorship checks
│   │   └── utils/
│   │       └── cache.py             # Content-hash memoization
│   ├── tests/                       # 91 tests (unit + integration)
│   ├── requirements.txt
│   ├── pytest.ini
│   └── Dockerfile
├── frontend/                        # React + Vite + TypeScript + Tailwind
│   ├── src/
│   │   ├── App.tsx                  # Tabbed UI shell (Search/Resume/Jobs/Settings)
│   │   ├── pages/
│   │   │   ├── Search.tsx           # Stage 1 UI
│   │   │   ├── ResumeCheck.tsx      # Stage 2 UI
│   │   │   ├── JobMatch.tsx         # Stage 3 UI + Fresher Mode + age-grade badges
│   │   │   ├── Tracker.tsx          # Pipeline analytics (Kanban + stats)
│   │   │   └── Settings.tsx         # API keys + resume upload
│   │   ├── components/
│   │   │   ├── ui/                  # Button, Card, badge (Sticker), progress (ScoreBar)
│   │   │   ├── Reveal.tsx           # Anime.js entrance animations
│   │   │   └── LoadingIndicator.tsx # Animated loading dots
│   │   ├── stores/
│   │   │   ├── settings.ts          # localStorage-backed API key store
│   │   │   └── theme.ts             # Dark/light theme hook
│   │   ├── lib/
│   │   │   ├── api.ts               # Typed API client
│   │   │   ├── types.ts             # Shared TypeScript types
│   │   │   └── animations.ts        # Anime.js animation helpers
│   │   ├── styles/
│   │   │   ├── globals.css          # Unified minimalist design tokens
│   │   │   └── legacy-neo-brutalist.css # Legacy design system backup
│   │   └── test/setup.ts            # Vitest setup with matchMedia mock
│   ├── vite.config.ts               # Vite + Vitest config
│   ├── tailwind.config.js           # Custom palette + shadow system
│   ├── Dockerfile + nginx.conf
│   └── package.json
├── config/
│   ├── settings.yaml                # Main config (informational)
│   └── job_boards.yaml              # Stage 3 board registry
├── docker-compose.yml               # 5-service stack
├── setup.sh                         # One-command setup
├── .env.example                     # Copy to .env, fill in keys
├── benchmark.py                     # Optional performance benchmark
└── README.md

Quick start (Docker Compose)

Prerequisites

  • Docker + Docker Compose (v2+)
  • An NVIDIA GPU is optional but recommended (router falls back to API inference without one)

1. Clone and configure

git clone https://github.com/anubhavsanket/wayfarer.git
cd wayfarer
cp .env.example .env
# edit .env and fill in at least ONE of:
#   NVIDIA_NIM_API_KEY / OPENROUTER_API_KEY
#   TAVILY_API_KEY (for Stage 1 search)

2. Bring up the stack

docker compose up --build

This starts five services:

Service Port Purpose
api 8000 FastAPI backend
qdrant 6333/6334 Vector store (REST / gRPC)
ollama 11434 Local inference + embeddings
redis 6379 Background job queue
frontend 3000 React UI served by nginx

3. Pull models (first time only)

# Embedding model (required for all stages)
docker compose exec ollama ollama pull nomic-embed-text

# Chat model (only if using Ollama for inference)
docker compose exec ollama ollama pull llama3.2:3b

# Fresher Mode classifier (optional, only needed for experience-level filtering)
docker compose exec ollama ollama pull qwen3:0.6b

4. Verify

curl http://localhost:8000/health
# → 200 OK with per-dependency status

5. Use it

  1. Open http://localhost:3000
  2. Go to Settings tab — enter your API keys and upload your main resume
  3. Go to Search tab — ask any question
  4. Go to Resume Check tab — paste a JD to get ATS analysis (resume is already loaded)
  5. Go to Job Match tab — see live postings ranked by fit (uses your resume automatically)

The resume you upload in Settings carries across all stages, so you upload it once.


Local development (no Docker)

Prerequisites

  • Python 3.11 or 3.12 (3.14 doesn't build the pinned dependencies)
  • Node.js 20+ (only for frontend work)
  • Ollama installed locally
  • Tesseract OCR (for embedded image extraction from DOCX resumes)

1. Create a venv and install backend deps

cd backend
python3.11 -m venv .venv
source .venv/bin/activate  # Linux/Mac
# or .venv\Scripts\activate     # Windows
pip install -r requirements.txt

2. Create a local .env

# backend/.env
OLLAMA_ENDPOINT=http://localhost:11434
QDRANT_HOST=localhost
QDRANT_PORT=6333
REDIS_URL=redis://localhost:6379
LLM_PROVIDER=ollama

3. Pull models

ollama pull nomic-embed-text
# For local LLM inference:
ollama pull llama3.2:3b

4. Run the API

From the project root:

uvicorn backend.app.main:app --reload --host 0.0.0.0 --port 8000

Qdrant falls back to a persistent local store at ./qdrant_data/ automatically.

5. Run the frontend

cd frontend
npm install
npm run dev  # → http://localhost:3000 (proxies /api to :8000)

Configuration

Configuration is layered:

  1. .env — runtime overrides (API keys, hostnames). Highest priority.
  2. Settings tab in the frontend — stores keys in localStorage, sends as request headers. No keys in git.
  3. config/job_boards.yaml — Stage 3 board registry (add a board = config change).

Supported LLM providers

Provider .env setting API key needed? Notes
ollama LLM_PROVIDER=ollama No Local, pull model first. Default provider.
nvidia LLM_PROVIDER=nvidia NVIDIA_NIM_API_KEY Free tier; model IDs must come from the live catalog (older models are EOL'd)
openrouter LLM_PROVIDER=openrouter OPENROUTER_API_KEY Free tier, wide model selection
lmstudio LLM_PROVIDER=lmstudio No Set LMSTUDIO_ENDPOINT + LMSTUDIO_MODEL
custom LLM_PROVIDER=custom CUSTOM_LLM_API_KEY Any OpenAI-compatible endpoint

The router falls back through the provider list whenever the primary fails. Even an explicitly selected provider falls through the chain instead of erroring out.

Job board registry

config/job_boards.yaml drives Stage 3 discovery. Each board declares the search query parameter name it expects (q for bluedoor, keywords for LinkedIn), so no per-board code is needed to route resume-derived keywords to the right provider.


API reference

GET /health

{"status":"ok","dependencies":[{"name":"qdrant","status":"up"},...]}

POST /api/v1/search — Stage 1

curl -X POST http://localhost:8000/api/v1/search \
  -H 'Content-Type: application/json' \
  -d '{"query": "best practices for RAG?", "max_sources": 3}'
{
  "answer": "Based on the provided sources...",
  "citations": [{"id":1, "url":"...", "title":"...", "snippet":"..."}],
  "sub_queries_used": ["best practices for RAG"],
  "cached": false
}

POST /api/v1/resume/check — Stage 2

curl -X POST http://localhost:8000/api/v1/resume/check \
  -F "resume_file=@my_resume.pdf" \
  -F "jd_text=Looking for an ML engineer with Python, PyTorch, and AWS."
{
  "resume_id": "abc123",
  "ats_score": 0.85,
  "structural_issues": [{"location": "table on page 1", "issue": "..."}],
  "keyword_gaps": [
    {"keyword": "pytorch", "tier": "reworded", "confidence": 0.84, "rationale": "..."}
  ]
}

POST /api/v1/resume/save — Stage 2

Default mode is non-destructive (writes a new file). Overwrite requires confirm_overwrite: true.

GET /api/v1/jobs/match — Stage 3

Param Type Default Notes
resume_id string required from /resume/check response
limit int 20 max results
location_mode enum specific_city specific_city / remote_only / hybrid / open_to_relocation
cities csv "" comma-separated cities
remote_ok bool false include remote postings
fresher_only bool false filter to fresher/junior roles only
test bool false return sample data
curl "http://localhost:8000/api/v1/jobs/match?resume_id=abc123&fresher_only=true&limit=10"

POST /api/v1/jobs/refresh — Stage 3 background

Re-fetches from all enabled boards and stores in Qdrant.


The job board registry

Each job source is defined in config/job_boards.yaml. You add a board by adding a YAML entry, no code changes needed. Each board can specify the search query parameter name it expects via query_param (e.g. q for bluedoor, keywords for LinkedIn).

- name: bluedoor
  enabled: true
  type: rest_api
  base_url: "https://api.bluedoor.sh/job-postings/v1/jobs/search"
  auth: api_key
  api_key_env: "BLUEDOOR_API_KEY"
  rate_limit_per_min: 100
  query_param: "q"
  field_mapping:
    title: "$.title"
    company: "$.org_name"
    location: "$.city"
    url: "$.apply_url"
  pagination:
    type: "none"
    param: ""
    max_pages: 1

Testing

# Unit tests (needs Python 3.11 venv, no Docker required)
cd backend
source .venv/bin/activate  # or .venv\Scripts\activate on Windows
python -m pytest tests/test_stage1.py tests/test_stage2.py tests/test_stage3.py -v

# E2E tests (requires all Docker services running)
python -m pytest tests/test_docker_e2e.py -v

Test suite: 91 backend tests + 11 frontend tests, all passing.


Release highlights

Feature Status Description
Fresher Mode Filter postings to entry-level/junior roles via qwen3:0.6b classification
Employment type full_time/contract/freelance/part_time field on JobMatch
LinkedIn integration HTML parsing of guest API (50+ Indian job postings)
Background refresh POST /api/v1/jobs/refresh — dedup, normalize, stale-drop
Pipeline analytics / Tracker Kanban board (/tracker) + stats (interview rate, avg score, conversion funnel, source breakdown, notification bell).
Structured resume memory Graph-based entity extraction for token-efficient matching
Settings dashboard API keys stored in localStorage, not in git
LM Studio / custom Any OpenAI-compatible endpoint works
Side-by-side redlines Original vs. suggested view in Resume Check
One-command setup bash setup.sh or curl link
Tesseract OCR Extract text from embedded images in DOCX resumes
Unified UI Clean, unified, minimalist interface + tactical dark mode
Stored resume Upload once in Settings, used across all stages
OOXML track-changes Resume save outputs Word-compatible tracked changes
Background refresh queue Redis-backed job queue for async board refresh
Per-request overrides Frontend settings sent as X-* headers per-request
Qdrant vector store Migrated from ChromaDB; three collections shared across stages
Targeted job search Resume-derived keywords routed per-board (q / keywords)
Board interleaving Results balanced across providers, title+company embed fallback for empty JDs
Provider fallback chain Explicitly-selected providers still fall through the router chain
PDF resume parsing pdfplumber fallback fixes 500 on PDF uploads
Location-aware matching Frontend location filters now sent to the job matcher API

Known limitations

  • bluedoor.sh descriptions — the API doesn't return JD descriptions in search results. The matcher falls back to embedding "<title> at <company>", so titles still rank accurately even though experience classification is less precise.
  • LinkedIn guest API — returns HTML (parsed via regex), not structured JSON. Keep volume low, personal-use only.
  • NVIDIA model IDs — free-tier models are frequently EOL'd. config.py ships with live catalog IDs, but they may need regenerating each release cycle.
  • VRAM ceiling — keep embeddings + local LLM under 4 GB total. Heavy inference is routed to APIs via the LLM Router.
  • OOXML track-changes — supported for resume save output. The UI redline view is HTML-based.
  • Tesseract OCR accuracy — low-resolution embedded images may produce noisy text. Icons under 80px are skipped.
  • Python 3.11 / 3.12 only — pinned dependencies don't build on 3.14.
  • GPU requirement — the ollama compose service reserves an NVIDIA GPU via deploy.reservations. It runs CPU-only with degraded performance if no GPU is present.

License

Proprietary. Built for the author's personal job search automation and as an AI engineering portfolio piece.

About

AI driven job search platform which judges the resume and JD to tell you how well of a fit you are for the job.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages