gigafibre-fsm/services/roster-solver
louispaulb a6508845a7 feat(dispatch): VRP route optimizer (OR-Tools) — "Optimiser" mode
Real vehicle-routing optimization behind the Suggérer UI, reusing our
existing OR-Tools solver service (no new stack). Fixes the greedy's
structural limits (sector-splitting, no global optimization).

- roster-solver: new route_solver.py (OR-Tools Routing / VRPTW).
  Minimizes real travel; skills = hard filter (VehicleVar ∈ allowed∪{-1};
  SetAllowedVehiclesForIndex has a broken Span typemap in ortools 9.15);
  on-site service time within each tech's shift window; optional per-job
  time windows; unfittable jobs left unassigned (drop penalty) instead of
  infeasible; specialist bias via skill order (per-vehicle arc cost).
  New POST /route endpoint. Dockerfile now COPYs route_solver.py.
  Unit-tested: skills respected, sectors consolidated, edge cases safe.
- hub: POST /roster/optimize-routes → proxies to solver /route.
- ops SPA: " Optimiser" strategy. Greedy buckets jobs into days +
  placeholders, then the solver re-optimizes each day (assignment +
  routes) among shifted techs; result maps into the same review dialog
  (occupation bars, route map, swap/merge reused). Graceful fallback to
  greedy if the solver is unreachable (never drops jobs). shiftWindowMin
  derives each vehicle's shift from templates.

Phase 2 (later): OSRM/Mapbox road-time matrix for exact travel times.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:24:08 -04:00
..
.gitignore Roster AI (planification) + prise de rendez-vous client 2026-06-03 16:42:44 -04:00
app.py feat(dispatch): VRP route optimizer (OR-Tools) — "Optimiser" mode 2026-07-02 11:24:08 -04:00
Dockerfile feat(dispatch): VRP route optimizer (OR-Tools) — "Optimiser" mode 2026-07-02 11:24:08 -04:00
README.md Roster AI (planification) + prise de rendez-vous client 2026-06-03 16:42:44 -04:00
requirements.txt Roster AI (planification) + prise de rendez-vous client 2026-06-03 16:42:44 -04:00
route_solver.py feat(dispatch): VRP route optimizer (OR-Tools) — "Optimiser" mode 2026-07-02 11:24:08 -04:00
sample_request.json Roster AI (planification) + prise de rendez-vous client 2026-06-03 16:42:44 -04:00
solver.py Roster AI (planification) + prise de rendez-vous client 2026-06-03 16:42:44 -04:00
test_solver.py Roster AI (planification) + prise de rendez-vous client 2026-06-03 16:42:44 -04:00

Roster AI — solveur d'horaires (OR-Tools CP-SAT)

Microservice d'optimisation sous contraintes (« façon Timefold », mais Python sans JVM) qui génère des horaires de techniciens. Agnostique du backend : il ne connaît ni Frappe ni ERPNext — on lui passe des techs + dispos + besoins de couverture, il rend des assignations optimales.

Place dans l'architecture

[Ops « Planification »]  →  [targo-hub lib/roster.js]  →  [roster-solver /solve]
                                      │  lit techs/jobs (Dispatch Technician/Job, ERPNext)
                                      └→ écrit les assignations validées

Le hub rassemble les entrées (techs, compétences, dispos, congés, besoins par jour/zone), appelle /solve, puis écrit les assignations retenues.

API

  • GET /health
  • POST /solve → corps :
{
  "horizon": { "start": "2026-06-08", "days": 7 },
  "shift_templates": [
    { "id": "jour", "name": "Jour 8h-16h", "hours": 8 }
  ],
  "technicians": [
    { "id": "T001", "name": "Marc", "skills": ["fibre"],
      "max_hours_week": 40, "max_days": 5, "cost_per_h": 38,
      "zone_home": "Montréal", "preferred_off": ["sam","dim"],
      "unavailable": ["2026-06-10"] }   // dates de congé/pause
  ],
  "coverage": [
    { "date": "2026-06-08", "shift": "jour", "zone": "Montréal",
      "required": 2, "required_skills": ["fibre"] }
  ],
  "weights": { "uncovered": 1000, "fairness": 5, "cost": 1, "preference": 8, "continuity": 4 },
  "max_seconds": 10
}

Réponse : assignments[], coverage_report[] (requis vs assigné vs shortfall), tech_hours{}, spread_hours, total_shortfall, explanations[], status.

Modèle de contraintes

Dures (faisabilité) :

  • 1 shift max par tech par jour
  • compétences : un tech ne compte pour un poste que s'il a toutes les compétences requises
  • disponibilité : pas d'assignation un jour de congé/pause (unavailable)
  • max_hours_week, max_days

Souples (objectif pondéré, minimisé) :

  • uncovered — postes non couverts (priorité écrasante → on voit les manques au lieu d'échouer)
  • fairness — écart d'heures max-min entre techs
  • cost — coût horaire total
  • preference — pénalité de travail un jour « préféré off »
  • continuitybonus si la zone du poste = zone d'attache du tech (moins de déplacement)

Couverture en soft = le solveur rend toujours le meilleur horaire possible même en sous-effectif, et rapporte les manques (ta demande « dispo vs requis »).

Dév

python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
python test_solver.py        # démo + vérifs sur sample_request.json
uvicorn app:app --port 8090  # serveur