Skip to main content

AI_INTEGRATION — staff-service

Sibling: APPLICATION_LOGIC · SECURITY_MODEL · OBSERVABILITY

Strategic anchors: 02 §10 AI Architecture · 04 Event-Driven Architecture · docs/08-ai-architecture.md

staff-service is AI-augmented, not AI-driven. Every AI surface is advisory: managers retain authority, every AI-generated recommendation carries provenance, and no schedule, assignment, or termination is ever executed by an AI without an explicit human action. This document enumerates the AI surfaces, their inputs, outputs, model choices, and audit posture.


1. AI Surfaces

SurfaceDirectionPurposeMode
Shift Optimization Suggestioninbound (consumer)Receive ai.suggestion.shift_optimization.v1 from ai-orchestrator-serviceadvisory
Staffing Gap Forecastoutbound (request)Ask ai-orchestrator for "in the next 24 h, where will we be short?"advisory
Anomalous Punch Detectionedge (Electron)ONNX edge model flags suspicious clock-in patterns at the front deskadvisory + alert
Handoff-Note Summarizationoutbound (request)Summarize last shift's handoff notes for the next-shift staff (multi-language)rendering
Skill / Position Auto-Tagoutbound (request)Suggest Position and skillIds for a new staff record from a free-form descriptiondrafting

All AI usage flows through ai-orchestrator-service (cloud) or the in-process OnnxRuntime (edge). staff-service never calls Vertex AI / external LLMs directly. This is enforced by the dependency-graph guard on src/infrastructure/.


2. Shift Optimization Suggestion (inbound)

Subject. melmastoon.ai.suggestion.shift_optimization.v1 (consumed; producer is ai-orchestrator-service).

Why it exists. A weekly batch (cloud-side) consumes anonymized, tenant-scoped attendance + booking + occupancy data and proposes a one-week schedule that reduces forecasted gaps and over-staffing. Tenant data isolation is enforced at the orchestrator boundary; staff-service only stores the result.

Storage. Persisted in staff.shift_suggestions (DATA_MODEL §4.11) with ai_provenance jsonb:

{
"source": "cloud",
"model": "vertex-text-bison@002",
"promptHash": "sha256:abc…",
"modelVersion": "v3.4.1",
"generatedAt": "2026-04-22T03:11:09Z",
"datasetWindow": { "from": "2026-03-01", "to": "2026-04-22" },
"humanInLoopRequired": true
}

Surface. GET /api/v1/shift-suggestions returns the most recent advisory. There is no :apply endpoint. Managers transcribe to manual POST /shifts or POST /shifts/{id}/assignments if they choose to act on it.

TTL. 14 d. After expiry the suggestion is purged by a scheduled job; never re-emitted automatically.

Audit. A staff.audit_events row is written each time a manager views a suggestion (action='ai.shift_suggestion.viewed') and each time they act on one in part or whole (action='ai.shift_suggestion.applied', with before referencing the suggestion ID). This is required by 02 §10 AI provenance.


3. Staffing Gap Forecast (outbound, on-demand)

Trigger. Manager invokes GET /api/v1/forecast/staffing-gaps?propertyId=&horizonHours=24.

Flow.

  1. staff-service aggregates the next-24h schedule + active assignments + recent attendance + leave windows into a feature payload (no PII; staff IDs only).
  2. Calls ai-orchestrator-service POST /v1/inference/staffing-gap with the feature payload + correlation-id.
  3. Receives a structured response: { gaps: [{ shiftId, expectedDeficit, confidence }], generatedAt }.
  4. Returns to caller, persisting only metadata + correlation-id (the prediction itself is not materialized as a ShiftSuggestion; it is ephemeral).

Latency budget. p95 800 ms end-to-end (orchestrator does the heavy lifting; we add < 50 ms).

Failure mode. Orchestrator timeout / 5xx → return 200 with { gaps: [], degraded: true, reason: 'forecast_unavailable' }. Never block the operator.


4. Anomalous Punch Detection (edge)

Where. Electron desktop (front-desk). The model runs in-process via onnxruntime-node (per 02 §10 Edge AI).

Model. staff-anomalous-punch.onnx, ~ 8 MB, distributed via the Electron auto-update channel and cached in the OS userdata path. Versioned as model_version in the punch metadata.

Inputs (feature vector). Per candidate punch:

  • Time of day (24 h cyclical encoding)
  • Day of week (cyclical)
  • Time delta to scheduled shift start/end (minutes)
  • Prior 24-hour punch density per staff at this device
  • PIN-attempt count in last 60 s at this device
  • Was the previous staff ID at this kiosk the same? (boolean)
  • Property tz / locale tag (one-hot)

No PII features. No staff name or email. The feature vector is reproducible from the local SQLite mirror.

Output. A scalar anomalyScore ∈ [0, 1]. UI thresholds:

  • < 0.3 → no UI signal.
  • 0.3 ≤ score < 0.7 → soft warning to operator: "Confirm staff identity?"
  • ≥ 0.7 → require manager-override path (POST /clock/punch:manager-override); plain PIN is rejected on the device with a friendly message.

Audit. Every punch carries metadata.anomalyScore and metadata.modelVersion in the published clock.in.v1 event. This is sufficient to retrospectively analyze drift.

False-positive policy. A score is never auto-blocking on its own; the manager-override path is always available and takes < 5 s with a manager PIN. We tolerate FPR ≤ 5% to keep the surface unobtrusive. Drift monitoring is described in OBSERVABILITY §8.


5. Handoff-Note Summarization (outbound, rendering)

Trigger. Electron renders the next-shift hand-off panel. If 5 or more new notes exist since the previous shift end, the desktop calls bff-backoffice-servicestaff-serviceai-orchestrator-service for a summary.

Inputs. Last N hand-off notes (max 50) for (propertyId, positionId) since prevShift.endedAt. Each note is sanitized (URLs / IDs masked) before send.

Output. A 3-bullet summary in the staff member's preferredLocale. Cached server-side for 30 minutes per (propertyId, positionId, since) key.

Provenance. Returned to UI with aiProvenance block; the UI displays a "summarized by AI" badge per 02 §10.

Failure mode. Any error → return raw notes; no degradation of operator workflow.


6. Skill / Position Auto-Tag (outbound, drafting)

Trigger. During POST /api/v1/staff from the Backoffice UI, a "Suggest position" button calls a draft endpoint:

POST /api/v1/ai/draft-staff-tags

{
"freeFormDescription": "Has 5 years experience as front desk + speaks Pashto, Dari, English; trained in fire safety",
"propertyId": "ppt_01HZ…"
}

Output.

{
"suggestedPositionId": "pos_01HZ…FRONT_DESK",
"suggestedDepartmentId": "dpt_01HZ…FRONT_OFFICE",
"suggestedSkillIds": ["skl_01HZ…OPERA_PMS"],
"suggestedSpokenLanguages": [
{ "code": "ps", "proficiency": "fluent" },
{ "code": "fa", "proficiency": "fluent" },
{ "code": "en", "proficiency": "conversational" }
],
"suggestedCertifications": [{ "type": "fire_safety" }],
"confidence": 0.81,
"aiProvenance": { "source": "cloud", "model": "vertex-text-bison@002", "promptHash": "sha256:…" }
}

Use. Suggestions populate the form; the manager edits and submits. The CreateStaff use case does not automatically use these — they are pre-fills only.


7. Audit & Provenance

For every AI-derived surface (read or applied), staff-service writes to staff.audit_events:

ActionWhenbefore / after
ai.shift_suggestion.viewedGET /shift-suggestionsbefore=null, after={ suggestionId }
ai.shift_suggestion.appliedA POST /shifts or :assignments call carries X-Source-Suggestion: sgg_…before={ suggestionId }, after={ shiftId, assignmentId }
ai.staffing_gap.queriedGET /forecast/staffing-gapsafter={ correlationId, degraded }
ai.handoff_summary.requestedInternal callafter={ propertyId, positionId, since, modelVersion }
ai.staff_tags.requestedPOST /ai/draft-staff-tagsafter={ correlationId, modelVersion, confidence }
ai.edge.anomalous_punch.detectedEdge model triggers ≥ 0.3after={ clockEntryId?, anomalyScore, modelVersion }

All AI events are queryable from BigQuery (cold export) for audit and bias review.


8. Bias / Fairness Considerations

The Shift Optimization model can over- or under-allocate based on attendance patterns that correlate with protected characteristics (cultural / religious observance, parental status). Mitigations:

  • Suggestion features explicitly exclude givenName, familyName, gender (we do not collect it), age, religion, ethnicity.
  • Approved leave windows are always honored by the suggester (input feature).
  • Manager review is required before any application.
  • A monthly fairness report (BigQuery) computes assignment counts per (staffId, position) and flags suggestions that would produce > 1.5σ deviation across staff at the same position. This report is reviewed by the Platform Ops team per 02 §16 Compliance.

9. Cost Posture

SurfaceCost driverM0 monthly targetMitigation
Shift Optimization (weekly)LLM batch inference (cloud)< $5 / property / monthWeekly cadence; coarse output
Staffing Gap ForecastLLM on-demand< 100 calls / day / propertyHeavy server-side caching
Anomalous Punch DetectionEdge inference$0 (on-device)Distributed via Electron auto-update
Handoff SummarizationLLM on-demand< 30 calls / day / property30-min response cache
Skill Auto-TagLLM on-demand< 10 calls / day / propertyOne-shot per new staff

A budget guardrail in ai-orchestrator-service short-circuits staff-service requests if the tenant's daily AI spend exceeds the configured cap (tenant.ai.daily_cap_usd). When short-circuited, our calls receive { degraded: true, reason: 'budget_exhausted' } and we render the non-AI baseline.


10. Disabling AI per Tenant

Tenants may opt out of AI surfaces in tenant-service settings (ai.staff.shift_optimization=false, ai.staff.handoff_summary=false, etc.). When opted out:

  • Inbound ai.suggestion.shift_optimization.v1 events for that tenant are dropped at the consumer (still acknowledged to Pub/Sub, never persisted).
  • Outbound calls short-circuit to the non-AI baseline.
  • The Electron edge model still loads (it does not require tenant-side data outside the device) but the UI hides anomaly warnings.

This is enforced by the AiTenantTogglePolicy in src/application/policies/.