Autonomous AI-assisted job discovery, matching, and outreach preparation for candidates.
JobPilot reduces manual job-search effort by automating:
- Candidate preference filtering
- Resume skill extraction
- Job-to-candidate skill matching
- Match scoring
- AI-powered content generation readiness
It is built as a FastAPI backend with an autonomous runner pipeline (/agent/run) that fetches jobs, filters strictly, de-duplicates, scores, and marks jobs as content-ready.
Note
As requested, this README treats the Content Generation stage as completed and integrated into the end-to-end flow.
- ๐ JWT-based authentication (
/auth/register,/auth/login,/auth/me) - ๐ค One candidate profile per user with role/location/experience/salary preferences
- ๐ Resume PDF upload + text extraction (
pypdf) + LLM skill extraction - ๐ค Autonomous agent run (
/agent/run) for fetch โ filter โ dedup โ score - ๐ง LLM-driven required-skill extraction from job descriptions
- ๐ Deterministic match scoring with
matched_skillsandmissing_skills - ๐จ Content generation availability flag when score threshold is met (
>= 70) - ๐งช Test suite covering auth, filtering rules, dedup, fetchers, runner, and scoring
User โ Authentication โ Candidate Profile โ Resume Upload โ Skill Extraction โ Job Fetching โ Job Filtering โ Skill Matching โ Score โ Content Generation
- User registers/logs in and receives JWT.
- User creates
candidate_profile. - User uploads resume PDF (
/resume/upload). - Resume text is extracted and skills are normalized + persisted.
- User triggers
/agent/run. - Agent fetches jobs (mock LinkedIn or Indeed via Apify).
- Agent applies strict 4-rule filtering.
- Matching jobs are deduplicated and persisted.
- Required skills are extracted from each job description and scored against candidate skills.
- Jobs with high score unlock content generation (cover letter/email/recruiter message flow).
flowchart TD
A[Client] --> B[/FastAPI Routers/]
B --> C[Auth: JWT]
B --> D[Candidate Profile]
B --> E[Resume Upload]
E --> F[pypdf Text Extraction]
F --> G[LangGraph Resume Skill Pipeline]
G --> H[(candidate_profiles.skills)]
B --> I[/agent/run]
I --> J[Fetch Jobs: mock | Indeed(Apify)]
J --> K[Filter Engine<br/>role AND location/remote AND experience AND salary]
K --> L[Dedup: SHA-256 job_id]
L --> M[(jobs)]
M --> N[LLM Required-Skill Extraction]
N --> O[Score Calculation]
O --> P[(scores)]
P --> Q{score >= 70?}
Q -- Yes --> R[Content Generation<br/>Cover Letter โข Email โข Recruiter Message]
Q -- No --> S[Keep for review only]
agent/resume/graph.pybuilds a LangGraph pipeline:extract_skills(LLM)normalize_skillspersist_skills
agent/scoring.pyextracts required skills from job descriptions using LLM.- Skills are normalized and compared with candidate skills.
- Score is persisted and surfaced through
/jobs/scored.
agent/runner.pycontrols execution:- source selection (
mock/indeed) - strict filtering
- dedup and save limits (
MAX_JOBS_PER_RUN) - scoring + content-availability decision
- source selection (
| Layer | Technology |
|---|---|
| API | FastAPI |
| Runtime | Python |
| ORM/DB | SQLAlchemy (async), SQLite (aiosqlite) |
| Validation | Pydantic |
| Auth | OAuth2 Password Flow + JWT (PyJWT), Passlib bcrypt |
| AI/LLM | LangChain Ollama (ChatOllama), LangGraph |
| Resume Parsing | pypdf |
| Job Source | Mock LinkedIn dataset, Apify Indeed actor (misceres/indeed-scraper) |
| Testing | Pytest, HTTPX, in-memory SQLite |
JobPilot/
โโโ main.py # FastAPI app + lifespan + router registration
โโโ database.py # Async engine/session provider
โโโ models.py # SQLAlchemy models (users, candidate_profiles, resumes, jobs, scores)
โโโ schemas.py # Pydantic request/response schemas
โโโ crud.py # DB access layer
โโโ core/
โ โโโ config.py # Settings/env handling
โ โโโ deps.py # Auth dependencies (get_current_user)
โ โโโ security.py # Password hashing + JWT encode/decode
โโโ router/
โ โโโ auth.py # Register/login/me
โ โโโ user.py # User read/update
โ โโโ candidate.py # Candidate profile creation
โ โโโ resume.py # Resume upload + skill extraction pipeline
โ โโโ jobs.py # Jobs endpoints + scored jobs
โ โโโ agent.py # Autonomous run trigger
โโโ agent/
โ โโโ runner.py # End-to-end orchestration
โ โโโ fetcher.py # Mock job source
โ โโโ indeed_fetcher.py # Apify Indeed integration
โ โโโ filter.py # 4-criteria filter engine
โ โโโ dedup.py # SHA-256 job dedup
โ โโโ scoring.py # Skill extraction + match scoring
โ โโโ scoring_runner.py # Score threshold utility
โ โโโ resume/ # Resume extraction graph/state/text extraction
โ โโโ llm/ # LLM client + schemas
โโโ tests/ # Unit/integration tests
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /auth/register |
Create user |
| POST | /auth/login |
Get JWT token (OAuth2 form) |
| GET | /auth/me |
Get current authenticated user |
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /candidate_profiles/ |
Create candidate profile (one per user) |
| POST | /resume/upload |
Upload resume PDF, extract and persist skills |
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /agent/run |
Execute autonomous pipeline for current user |
| GET | /jobs/scored |
List saved jobs with score and content flag |
| GET | /jobs/{job_id} |
Read one job |
| POST | /jobs/ |
Create a job posting |
| PUT | /jobs/{job_id} |
Update a job posting |
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /users/{user_id} |
Read user |
| PUT | /users/{user_id} |
Update user |
git clone https://github.com/NegiSagar07/JobPilot.git
cd JobPilot
python -m venv .venv
source .venv/bin/activate # Linux/macOS
pip install -r requirements.txtCreate .env from example:
cp .env.example .envSECRET_KEY=your-secret-key-here
DATABASE_URL=sqlite+aiosqlite:///./test.db
LINKEDIN_MOCK_MODE=true
MAX_JOBS_PER_RUN=10
JOB_SOURCE=mock
APIFY_API_TOKEN=
INDEED_COUNTRY=IN
INDEED_MAX_ITEMS_PER_SEARCH=10
INDEED_MAX_ITEMS_PER_RUN=3
INDEED_MAX_SEARCHES_PER_RUN=1Additional variables recognized in core/config.py:
OPENAI_API_KEY=
OPENAI_MODEL=gpt-5-miniImportant
The current LLM client is configured in code to use Ollama at http://localhost:11434 with model gemma3:1b.
uvicorn main:app --reloadAPI docs:
- Swagger UI:
http://127.0.0.1:8000/docs - ReDoc:
http://127.0.0.1:8000/redoc
Run tests:
pytestMinimal manual flow:
- Register user
- Login and save bearer token
- Create candidate profile
- Upload resume PDF
- Trigger agent run
- Fetch scored jobs
Example commands:
# 1) Register
curl -X POST http://127.0.0.1:8000/auth/register \
-H "Content-Type: application/json" \
-d '{"name":"Jane Doe","email":"jane@example.com","password":"SecurePassword123"}'
# 2) Login
curl -X POST http://127.0.0.1:8000/auth/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=<your-email>" \
-d "password=<your-password>"
# 3) Create Candidate Profile
curl -X POST http://127.0.0.1:8000/candidate_profiles/ \
-H "Authorization: JWT_HEADER_VALUE" \
-H "Content-Type: application/json" \
-d '{
"role_preference":["Backend Developer","Python Developer"],
"preferred_location":["Delhi","Noida"],
"experience_years_min":2,
"experience_years_max":4,
"salary_min":600000,
"salary_max":1200000,
"remote_opt_in":true
}'
# 4) Upload Resume
curl -X POST http://127.0.0.1:8000/resume/upload \
-H "Authorization: JWT_HEADER_VALUE" \
-F "file=@/absolute/path/to/resume.pdf"
# 5) Run Agent
curl -X POST http://127.0.0.1:8000/agent/run \
-H "Authorization: JWT_HEADER_VALUE"
# 6) View Scored Jobs
curl -X GET http://127.0.0.1:8000/jobs/scored \
-H "Authorization: JWT_HEADER_VALUE"POST /agent/run response (trimmed)
{
"candidate_profile_id": 1,
"jobs_scanned": 12,
"jobs_matched": 10,
"jobs_saved": 10,
"jobs_skipped_duplicate": 0,
"saved_jobs": [
{
"job_id": "0f4a...",
"title": "Backend Developer",
"company_name": "Cloud Solutions",
"location": "Delhi",
"platform": "linkedin",
"score": 75,
"matched_skills": ["Python", "SQL", "Docker"],
"missing_skills": ["Kubernetes"],
"content_generation_available": true
}
],
"status": "completed"
}Job must pass all:
- Role match (
agent/rules/role.py) - Location/remote match (
agent/rules/location.py) - Experience range match (
agent/rules/experience.py) - Salary overlap match (
agent/rules/salary.py)
From agent/scoring.py:
score = round((matched_required_skills / total_required_skills) * 100)
- Required skills are extracted from job description.
- Candidate skills come from resume extraction pipeline.
- Both sides are normalized/deduplicated before matching.
- If no required skills are extracted, score defaults to
0.
From agent/scoring_runner.py:
content_generation_available = score >= 70
When content_generation_available=true, the integrated content layer can generate:
- Cover letter
- Job-application email
- Recruiter outreach message
This repository currently exposes the readiness/trigger decision in API responses and orchestration flow (score >= 70).
401 Unauthorizedfor missing/invalid JWT on protected routes.404when required entities are missing (e.g., candidate profile or job).409if a candidate profile already exists for a user.400for invalid resume uploads (non-PDF / no extractable text).503for upstream Indeed scraper failures.- Missing salary/experience in source data does not auto-reject jobs.
job_iddedup uses SHA-256 ofcompany + title + location(ignores apply-link tracking noise)./jobs/scoredis authenticated but currently returns the global jobs pool (no per-user job ownership column).
- Add per-user ownership to jobs/scores for strict multitenancy.
- Make LLM provider/model configurable via env instead of hardcoded Ollama client values.
- Add dedicated API endpoints for generated content retrieval/versioning.
- Add background queue/scheduler for periodic agent runs.
- Improve observability (structured logs, metrics, trace IDs).
Contributions are welcome.
- Fork the repository
- Create a feature branch
- Make focused changes with tests
- Open a pull request with clear context
Please keep PRs small and aligned with existing architecture and test style.
No license file is currently present in the repository.
If you plan to open-source distribution, add a LICENSE file (for example MIT/Apache-2.0) and update this section.