Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
dist/
build/
.eggs/
*.egg
.venv/
venv/
env/

# Node
node_modules/
dist/
.vite/

# IDE
.idea/
.vscode/
*.swp
*.swo

# Environment
.env
.env.local
.env.production

# OS
.DS_Store
Thumbs.db

# Docker
*.log

# Alembic
alembic/versions/*.pyc

# Uploads
uploads/
119 changes: 118 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,118 @@
# app_dotnet-angular-microservices
# AI-Enabled Staffing & Onboarding Portal

An enterprise-grade staffing lifecycle management portal built to manage onboarding of ~700 team members across 5 locations. Features AI-powered candidate matching, role rubric management, SLA tracking, and semantic search — all powered by Claude (Anthropic) via Amazon Bedrock.

## Tech Stack

| Layer | Technology |
|-------|-----------|
| Frontend | React 19 + TypeScript + Material UI + Vite |
| Backend | Python 3.12 + FastAPI |
| Database | PostgreSQL 16 |
| AI | Claude (Anthropic) via Amazon Bedrock |
| Cloud | AWS |
| Auth | Infosys SSO (placeholder) |

## Project Structure

```
staffing-ai-portal/
├── backend/
│ ├── app/
│ │ ├── api/v1/endpoints/ # FastAPI route handlers
│ │ ├── core/ # Security, dependencies
│ │ ├── models/ # SQLAlchemy models
│ │ ├── schemas/ # Pydantic schemas
│ │ ├── services/ # AI, Auth, WMT services
│ │ ├── config.py # Settings
│ │ ├── database.py # DB connection
│ │ └── main.py # FastAPI app
│ ├── alembic/ # Database migrations
│ ├── seed_data.py # Initial data loader
│ ├── requirements.txt # Python dependencies
│ └── Dockerfile
├── frontend/
│ ├── src/
│ │ ├── components/ # Layout, shared components
│ │ ├── pages/ # Dashboard, Requests, Candidates, etc.
│ │ ├── services/ # API client
│ │ ├── types/ # TypeScript types
│ │ ├── App.tsx # Router + theme
│ │ └── main.tsx # Entry point
│ ├── package.json
│ └── Dockerfile
└── docker-compose.yml
```

## Quick Start

### With Docker Compose

```bash
docker-compose up --build
```

- Backend API: http://localhost:8000
- API Docs: http://localhost:8000/docs
- Frontend: http://localhost:5173

### Manual Setup

**Backend:**
```bash
cd backend
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload
```

**Frontend:**
```bash
cd frontend
npm install
npm run dev
```

**Seed Data:**
```bash
cd backend
python seed_data.py
```

## Key Features

- **Staffing Request Lifecycle**: Full lifecycle from WMT request ingestion through onboarding
- **Practice Unit Management**: Tag roles to organizational units (ADM, CIS, QES, etc.) with Unit Anchors
- **Role Rubrics**: Upload HTML rubrics with weighted scoring, pass-band thresholds, auto-reject rules, and specialist-friendly rules — rendered in real-time during interviews
- **SLA Management**: Upload SLA definitions as .xls/.xlsx files, track compliance
- **AI-Powered Features**: Candidate matching, resume parsing, interview question generation, SLA risk prediction
- **Semantic Search**: Natural language search across candidates, roles, and requests via vector embeddings
- **24-Month Rotation Tracking**: Mandatory candidate replacement pipeline management
- **Hybrid Work Compliance**: Track 3+ days/week in-office requirement
- **5-Location Support**: Richardson TX, Raleigh NC, Phoenix AZ (company) + Plano TX, Reston VA (client)

## API Endpoints

| Endpoint | Description |
|----------|-------------|
| `GET /api/v1/dashboard/` | Dashboard summary stats |
| `CRUD /api/v1/staffing-requests/` | Staffing request lifecycle |
| `CRUD /api/v1/candidates/` | Candidate management |
| `CRUD /api/v1/roles/` | Role management with Practice Unit tagging |
| `CRUD /api/v1/practice-units/` | Practice Unit + Unit Anchor management |
| `CRUD /api/v1/rubrics/` | Rubric CRUD + HTML upload |
| `GET /api/v1/rubrics/{id}/html` | Render rubric HTML for interviewers |
| `CRUD /api/v1/sla/` | SLA definitions + XLS file upload |
| `CRUD /api/v1/interviews/` | Interview rounds + rubric-based feedback |
| `POST /api/v1/ai/semantic-search` | AI-powered semantic search |
| `POST /api/v1/ai/match-candidate` | AI candidate-role matching |
| `POST /api/v1/auth/login` | Authentication |

## Phases

This is **Phase 1 (Foundation)**. Upcoming phases:
- Phase 2: AI-Powered Matching & Resume Parsing
- Phase 3: Interview & Offer Management
- Phase 4: Onboarding & Compliance
- Phase 5: Advanced AI & Analytics
- Phase 6: Hardening & Go-Live
12 changes: 12 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
36 changes: 36 additions & 0 deletions backend/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[alembic]
script_location = alembic
sqlalchemy.url = postgresql://staffing_user:staffing_pass@localhost:5432/staffing_portal

[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
39 changes: 39 additions & 0 deletions backend/alembic/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from logging.config import fileConfig

from sqlalchemy import engine_from_config, pool
from alembic import context

from app.database import Base
from app.config import settings

# Import all models
import app.models # noqa: F401

config = context.config
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)

if config.config_file_name is not None:
fileConfig(config.config_file_name)

target_metadata = Base.metadata


def run_migrations_offline() -> None:
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url, target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"})
with context.begin_transaction():
context.run_migrations()


def run_migrations_online() -> None:
connectable = engine_from_config(config.get_section(config.config_ini_section, {}), prefix="sqlalchemy.", poolclass=pool.NullPool)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
26 changes: 26 additions & 0 deletions backend/alembic/script.py.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}


def upgrade() -> None:
${upgrades if upgrades else "pass"}


def downgrade() -> None:
${downgrades if downgrades else "pass"}
1 change: 1 addition & 0 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions backend/app/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions backend/app/api/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions backend/app/api/v1/endpoints/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

57 changes: 57 additions & 0 deletions backend/app/api/v1/endpoints/ai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy.orm import Session

from app.database import get_db
from app.services.ai_service import ai_service

router = APIRouter(prefix="/ai", tags=["AI"])


class MatchRequest(BaseModel):
candidate_summary: str
role_description: str
required_skills: list[str]


class ResumeParseRequest(BaseModel):
resume_text: str


class InterviewQuestionsRequest(BaseModel):
role_title: str
candidate_skills: list[str]
rubric_criteria: list[str] = []


class SemanticSearchRequest(BaseModel):
query: str
corpus: list[dict] = []


@router.post("/match-candidate")
async def match_candidate(data: MatchRequest):
result = await ai_service.match_candidate_to_role(
data.candidate_summary, data.role_description, data.required_skills
)
return result


@router.post("/parse-resume")
async def parse_resume(data: ResumeParseRequest):
result = await ai_service.parse_resume(data.resume_text)
return result


@router.post("/interview-questions")
async def generate_interview_questions(data: InterviewQuestionsRequest):
result = await ai_service.generate_interview_questions(
data.role_title, data.candidate_skills, data.rubric_criteria
)
return {"questions": result}


@router.post("/semantic-search")
async def semantic_search(data: SemanticSearchRequest):
results = await ai_service.semantic_search(data.query, data.corpus)
return {"results": results}
37 changes: 37 additions & 0 deletions backend/app/api/v1/endpoints/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session

from app.database import get_db
from app.schemas.user import Token, UserCreate, UserResponse
from app.services.auth_service import auth_service

router = APIRouter(prefix="/auth", tags=["Authentication"])


@router.post("/login", response_model=Token)
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
user = auth_service.authenticate_local(db, form_data.username, form_data.password)
if user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
token = auth_service.create_token_for_user(user)
return Token(access_token=token)


@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
def register(user_data: UserCreate, db: Session = Depends(get_db)):
from app.models.user import User
existing = db.query(User).filter(User.email == user_data.email).first()
if existing:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Email already registered")
user = auth_service.register_user(db, user_data.email, user_data.full_name, user_data.password, user_data.sso_id)
return user


@router.post("/sso/callback", response_model=Token)
def sso_callback(sso_token: str, db: Session = Depends(get_db)):
"""SSO callback endpoint — Infosys SSO integration (placeholder)."""
user_info = auth_service.authenticate_sso(sso_token)
if user_info is None:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="SSO integration pending")
return Token(access_token="sso-token-placeholder")
Loading