Skip to content

ai: approval prediction — serving path (event listeners + prediction service) #651

Description

@babltiga

Part of #645 (approval-outcome prediction epic — read it first for the big picture).

Goal

The serving path: when a query lands in review, load the org's trained model, extract features, score, and persist the prediction row. Event-driven, fail-safe — a prediction failure must never affect workflow state.

Depends on: core.api services, the feature extractor, and the trainer/model classes.

Steps

  1. ai/api/ApprovalPredictionService.java — interface: void predictForQuery(UUID queryRequestId) (plus void trainAll() used by the scheduled job in the next sub-issue). Implementation ai/internal/DefaultApprovalPredictionService.java:
    • Guards, in order (each persists a skipped=true row with a distinct skipped_reason string): feature disabled → DISABLED; no model row / serving=falseMODEL_NOT_SERVING (covers cold start and the below-AUC case).
    • Happy path: load persisted AI analysis + cost estimate + query attributes + rate counts (all via core.api lookups), extract features, model.predict(...), persist via ApprovalPredictionPersistenceService, publish ApprovalPredictionCompletedEvent.
    • Any unexpected exception → catch (specific types where possible), log ERROR, persist a failed=true sentinel row with error_message. Mirror how the AF-624 estimate path and the AI-analysis path handle failure sentinels.
  2. ai/internal/ApprovalPredictionListener.java — two @ApplicationModuleListener methods:
    • on(QueryReadyForReviewEvent event)predictForQuery(...). This event is published by workflow/internal/QueryReviewStateMachine exactly when a query enters PENDING_REVIEW and never for auto-approved/rejected/break-glass paths — which is precisely the population we want.
    • on(QueryEstimateCompletedEvent event) → the estimate is computed in parallel and may arrive after review-readiness. If a prediction row exists whose feature snapshot recorded estimate_missing=true and the query is still PENDING_REVIEW, recompute and replace the row (this is the only update path). Otherwise no-op.
  3. Register nothing in routing/workflow decision paths — the prediction is advisory-only by construction (see epic).

Acceptance criteria

  • DefaultApprovalPredictionServiceTest (Mockito, no Spring): disabled → skipped(DISABLED); no serving model → skipped(MODEL_NOT_SERVING); happy path persists probability + publishes event; lookup throwing → failed sentinel, no exception propagates.
  • ApprovalPredictionListenerTest: ready-for-review triggers prediction; estimate-completed recomputes only in the estimate_missing + still-pending case (3 no-op branches tested).
  • Integration test ApprovalPredictionIntegrationTest (Testcontainers postgres:18-alpine, model on BehaviorAnomalyDetectionIntegrationTest): seed ≥ 50 decided queries → train (call trainAll() directly) → submit a query to PENDING_REVIEW → assert an approval_predictions row with a probability exists.
  • mvn test -Dtest=ApplicationModulesTest passes (ai → core via api only).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions