Local-first LLM application for resume vs job description analysis.
- FastAPI service scaffold with
/healthendpoint - Streamlit shell page for API connectivity checks
- SQLAlchemy 2 + Alembic migration setup
- Initial test suite and dev tooling configuration
- Docker + Docker Compose for reproducible local runs
- FastAPI, Pydantic, SQLAlchemy 2, Alembic
- Streamlit
- Ollama (local model serving)
- pytest, Ruff, mypy, bandit, pip-audit
- Create and activate a virtualenv:
python3.11 -m venv .venvsource .venv/bin/activate
- Install dependencies:
python -m pip install -U pip && pip install -e .[dev]
- Run API:
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
- Run UI (new terminal):
streamlit run app/ui/streamlit_app.py
- Start stack:
docker compose up --build
- API:
http://localhost:8000/health - UI:
http://localhost:8501
docker-compose.yml points API to host Ollama using http://host.docker.internal:11434.
- Install Ollama and start it locally.
- Pull recommended model for this machine class (M1 8GB):
ollama pull qwen2.5:3b
- Optional quality model (slower):
ollama pull llama3.1:8b
- Default project model:
qwen2.5:3b - You can switch models without code changes using env var
OLLAMA_MODEL. - Example overrides:
OLLAMA_MODEL=qwen2.5:3bOLLAMA_MODEL=qwen3.5:4bOLLAMA_MODEL=llama3.1:8b
- All tests:
pytest -q - Single file:
pytest tests/integration/test_health.py -q - Single test:
pytest tests/integration/test_health.py::test_health_endpoint_returns_expected_shape -q - Lint:
ruff check . - Format:
ruff format . - Type-check:
mypy app tests - Security:
bandit -r app && pip-audit
- Install hooks once per clone:
pre-commit install - Run all hooks on demand:
pre-commit run --all-files - Included hooks:
ruff-checkruff-formatmypy
- Create migration:
alembic revision --autogenerate -m "describe change" - Apply migration:
alembic upgrade head
Run this after starting the API:
curl -X POST "http://localhost:8000/analyze" \
-H "Content-Type: application/json" \
-d '{
"resume_text": "Backend engineer with Python, FastAPI, Docker, AWS, SQL, and production microservices experience.",
"job_description_text": "Hiring backend engineer with Python, FastAPI, Docker, AWS, SQL, testing, and microservices skills."
}'The response includes:
total_score: weighted ATS-style scorecategories: per-category score, weight, rationale, matched keywordsskill_gaps:missing,partial, andmatchedskill analysisrequest_id,latency_ms, and version metadata
curl -X POST "http://localhost:8000/analyze/upload" \
-F "resume_file=@examples/resume.txt" \
-F "job_description_file=@examples/jd.txt"Upload behavior:
- Allowed file types:
.txt,.pdf,.docx - Max file size is controlled by
MAX_UPLOAD_BYTES(default2000000) - Raw input text is not stored in DB; only derived scores and SHA-256 hashes are saved
You can provide each side as either text or file:
- Resume:
resume_textorresume_file - JD:
job_description_textorjob_description_file
Endpoint:
POST /analyze/flexible(multipart form)
This powers the Streamlit UI flow where users choose paste or upload independently for resume and JD.
curl -X POST "http://localhost:8000/rewrite" \
-H "Content-Type: application/json" \
-d '{
"resume_text": "Backend engineer with FastAPI and Docker experience building production APIs.",
"job_description_text": "Need backend engineer with FastAPI, Docker, AWS, and strong communication.",
"bullets": ["Built API endpoints", "Managed deployment process"]
}'Behavior:
- Uses Ollama model from
OLLAMA_MODEL. - Enforces structured JSON output validation.
- On timeout/invalid model output, returns deterministic fallback bullets with warnings.
curl -X POST "http://localhost:8000/summary" \
-H "Content-Type: application/json" \
-d '{
"resume_text": "Backend engineer with FastAPI and Docker experience building production APIs.",
"job_description_text": "Need backend engineer with FastAPI, Docker, AWS, and strong communication.",
"target_role": "Senior Backend Engineer"
}'curl -X POST "http://localhost:8000/interview" \
-H "Content-Type: application/json" \
-d '{
"resume_text": "Backend engineer with FastAPI and Docker experience building production APIs.",
"job_description_text": "Need backend engineer with FastAPI, Docker, AWS, and strong communication.",
"target_role": "Senior Backend Engineer",
"question_count": 9
}'Both endpoints validate LLM JSON outputs and fall back to deterministic responses when needed.
curl -X POST "http://localhost:8000/report" \
-H "Content-Type: application/json" \
-d '{
"resume_text": "Backend engineer with FastAPI and Docker experience building production APIs.",
"job_description_text": "Need backend engineer with FastAPI, Docker, AWS, and strong communication.",
"target_role": "Senior Backend Engineer",
"bullets": ["Built API endpoints", "Managed deployment process"],
"question_count": 9
}'The /report response combines:
- deterministic analysis score + gaps
- rewritten bullets
- tailored summary
- interview questions
- aggregated warnings and metadata
Use multipart form for mixed inputs (resume_text or resume_file, and job_description_text or
job_description_file):
curl -X POST "http://localhost:8000/report/flexible" \
-F "resume_file=@examples/resume.txt" \
-F "job_description_file=@examples/jd.txt" \
-F "target_role=" \
-F "bullets_text=" \
-F "question_count=9"Behavior:
- If
target_roleis blank, the API infers a role from JD text. - If
bullets_textis blank, the API infers bullets from resume text.
- Provide only Resume + JD (paste or upload).
- Click Analyze Match to run deterministic scoring plus lightweight LLM insights.
- Use tabs for on-demand LLM features:
- Application Optimizer (
/optimize) for summary + bullets + positioning notes. - Interview (
/interview) for role-aligned questions.
- Application Optimizer (
- Export current app state from Export tab:
joblens_export.jsonjoblens_export.md
curl -X POST "http://localhost:8000/analyze/insights" \
-H "Content-Type: application/json" \
-d '{
"resume_text": "Backend engineer with FastAPI and Docker experience building production APIs.",
"job_description_text": "Need backend engineer with FastAPI, Docker, AWS, and strong communication.",
"total_score": 64,
"skills_to_polish": ["testing"],
"core_topics": ["Skills Match"]
}'curl -X POST "http://localhost:8000/optimize" \
-H "Content-Type: application/json" \
-d '{
"resume_text": "Backend engineer with FastAPI and Docker experience building production APIs.",
"job_description_text": "Need backend engineer with FastAPI, Docker, AWS, and strong communication."
}'Flexible variant for text or upload inputs:
POST /optimize/flexible
curl -X POST "http://localhost:8000/interview/flexible" \
-F "resume_text=Backend engineer with API delivery experience" \
-F "job_description_text=Need backend engineer with testing and communication skills" \
-F "question_count=6"The Streamlit UI disables action buttons while requests are in progress to prevent duplicate calls.
- The app is tuned for reliability over speed with local models.
- Recommended defaults in
.env:OLLAMA_MODEL=qwen2.5:3bREQUEST_TIMEOUT_SECONDS=90OLLAMA_MAX_RETRIES=2
- Typical local latency (varies by prompt size):
- Deterministic analyze: sub-second
- Match insights: ~8-25s
- Optimizer/interview: ~15-60s
- For portfolio screenshots, wait for
Insight status: AI-generated insightswhen possible.
GET /history?limit=20- list recent runs.GET /history/{request_id}- detailed run output, breakdown, hashes, and LLM metadata.
- Leave virtualenv:
deactivate - Remove local env and caches:
rm -rf .venv .pytest_cache .mypy_cache .ruff_cache dist build *.egg-info
- Stop containers and remove volumes:
docker compose down -v