A production-grade multi-agent LLM application built with LangGraph, deployed on Azure Web App via Azure Container Registry (ACR) with full Azure DevOps CI/CD pipeline.
Smart Job Application Coach is an end-to-end LLMOps project that leverages a 3-agent sequential LangGraph pipeline to help job seekers analyze their resume against a job description, get tailored resume advice, and generate a personalized cover letter — all powered by GPT-4o with structured outputs.
This project demonstrates:
- Multi-agent orchestration using LangGraph
- Structured LLM outputs using Pydantic models
- Production API with FastAPI
- Full containerization with Docker
- LLMOps deployment on Azure Web App via ACR
- Automated CI/CD pipeline with Azure DevOps
User (Browser)
│
▼
FastAPI Backend (Azure Web App)
│
▼
LangGraph Sequential Pipeline
│
├──▶ Agent 1: Skill Gap Analyzer
│ └── Reads resume + JD
│ └── Returns structured SkillGapAnalysis
│
├──▶ Agent 2: Resume Advisor
│ └── Takes gap analysis
│ └── Returns section-by-section ResumeAdvice
│
└──▶ Agent 3: Cover Letter Writer
└── Takes resume + JD + gap analysis
└── Returns personalized CoverLetter
| Agent | Role | Input | Output |
|---|---|---|---|
| Skill Gap Analyzer | Compares resume vs JD | Resume + JD | SkillGapAnalysis |
| Resume Advisor | Suggests improvements | Gap analysis + Resume | ResumeAdvice |
| Cover Letter Writer | Writes cover letter | All prior context | CoverLetter |
class JobCoachState(TypedDict):
resume_text: str
job_description: str
applicant_name: str
gap_analysis: Optional[SkillGapAnalysis]
resume_advice: Optional[ResumeAdvice]
cover_letter: Optional[CoverLetter]
error: Optional[str]Each agent receives the full shared state as a dict and returns only the keys it updates — LangGraph merges them automatically.
| Layer | Technology |
|---|---|
| LLM Framework | LangGraph + LangChain |
| LLM Model | GPT-4o (gpt-4o-2024-08-06) |
| Structured Output | Pydantic v2 |
| Backend API | FastAPI |
| Frontend | Vanilla HTML/JS (single file) |
| Containerization | Docker |
| Container Registry | Azure Container Registry (ACR) |
| Cloud Deployment | Azure Web App (Linux Container) |
| CI/CD | Azure DevOps Pipelines |
| Package Manager | uv |
LangGraph-Job-Coach-Azure-WebApps/
│
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI app & endpoints
│ ├── graph.py # LangGraph pipeline definition
│ │
│ ├── agents/
│ │ ├── __init__.py
│ │ ├── skill_gap.py # Agent 1 — Skill Gap Analyzer
│ │ ├── resume_advisor.py # Agent 2 — Resume Advisor
│ │ └── cover_letter.py # Agent 3 — Cover Letter Writer
│ │
│ ├── models/
│ │ ├── __init__.py
│ │ └── schemas.py # Pydantic models & LangGraph State
│ │
│ └── static/
│ └── index.html # Frontend UI
│
├── Dockerfile
├── azure-pipelines.yml
├── pyproject.toml
├── .env.example
└── README.md
Automated pipeline via Azure DevOps triggered on every push to main or dev.
Push to Repo
│
▼
┌─────────────────────────────┐
│ STAGE 1 — CI │
│ ├── Login to ACR │
│ ├── Build Docker Image │
│ └── Push Image to ACR │
└─────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ STAGE 2 — CD │
│ ├── Deploy to Azure Web App │
│ ├── Set environment vars │
│ ├── Restart Web App │
│ └── Health Check /health │
└─────────────────────────────┘
- Docker image tagged with both
BuildIdandlatest - CD stage only runs if CI succeeds (
condition: succeeded('CI')) - Health check hits
/healthendpoint — pipeline fails if response is not200 OPENAI_API_KEYinjected securely from Azure DevOps variable group (never stored in code or image)
| Method | Endpoint | Description |
|---|---|---|
GET |
/ |
Frontend UI |
GET |
/health |
Health check |
POST |
/analyze |
Run full 3-agent pipeline |
{
"resume_text": "Full resume text...",
"job_description": "Job description text...",
"applicant_name": "John Doe"
}{
"gap_analysis": {
"missing_technical_skills": ["..."],
"missing_soft_skills": ["..."],
"experience_gaps": ["..."],
"matching_strengths": ["..."],
"overall_match_score": 72,
"summary": "..."
},
"resume_advice": {
"suggestions": ["..."],
"priority_actions": ["..."],
"keywords_to_add": ["..."]
},
"cover_letter": {
"subject_line": "...",
"body": "...",
"key_selling_points": ["..."]
}
}- Python 3.11+
uvpackage manager- Docker Desktop
- OpenAI API key
# Clone repo
git clone https://github.com/<your-username>/LangGraph-Job-Coach-Azure-WebApps.git
cd LangGraph-Job-Coach-Azure-WebApps
# Create and activate virtual environment
uv venv --python 3.11
.venv\Scripts\Activate.ps1 # Windows
source .venv/bin/activate # Linux/Mac
# Install dependencies
uv pip install -e .
# Set environment variable
cp .env.example .env
# Edit .env and add your OPENAI_API_KEY
# Run the app
uvicorn app.main:app --reload --port 8000Open http://localhost:8000
docker build -t job-coach:latest .
docker run -p 8000:8000 \
-e OPENAI_API_KEY=your-key-here \
job-coach:latest- Azure Container Registry (ACR) — stores Docker images
- Azure App Service Plan — Linux B1 tier
- Azure Web App — pulls container from ACR and runs it
# Login to ACR
az acr login --name <your-acr-name>
# Tag and push image
docker tag job-coach:latest <your-acr-name>.azurecr.io/job-coach:latest
docker push <your-acr-name>.azurecr.io/job-coach:latest
# Create App Service Plan
az appservice plan create \
--name <plan-name> \
--resource-group <your-resource-group> \
--is-linux \
--sku B1
# Create Web App
az webapp create \
--resource-group <your-resource-group> \
--plan <plan-name> \
--name <your-webapp-name> \
--deployment-container-image-name <your-acr-name>.azurecr.io/job-coach:latest
# Configure container settings
az webapp config container set \
--resource-group <your-resource-group> \
--name <your-webapp-name> \
--container-image-name <your-acr-name>.azurecr.io/job-coach:latest \
--container-registry-url https://<your-acr-name>.azurecr.io
# Set environment variables
az webapp config appsettings set \
--resource-group <your-resource-group> \
--name <your-webapp-name> \
--settings OPENAI_API_KEY="<your-key>" WEBSITES_PORT=8000| Variable | Description |
|---|---|
OPENAI_API_KEY |
OpenAI API key for GPT-4o |
WEBSITES_PORT |
Port exposed by container (8000) |
Create a .env file from the provided template:
cp .env.example .env- Structured outputs — every agent returns a validated Pydantic model, not raw text
- State management — TypedDict shared state flows through all agents with automatic merging
- Sequential agent pipeline — strict data dependency enforced by LangGraph edges
- LLM initialization at request time — avoids env var loading race conditions at startup
- Containerized deployment — reproducible environment from dev to production
- Secret management — API keys injected via Azure DevOps variable groups, never in code or image
MIT License
