VBAF.Center.Router.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS VBAF-Center Phase 5 — Agent Router .DESCRIPTION Takes normalised signals and routes them to the correct trained VBAF agent. Returns the recommended action (0-3). Phase 14 — RED signal override raises minimum action level Phase 15 — Weighted average used instead of simple average Phase 17 — Customer-specific action thresholds from schedule.json Functions: Invoke-VBAFCenterRoute — send signals to correct agent Register-VBAFCenterAgent — register a trained agent Get-VBAFCenterRouteStatus — show all loaded agents Get-VBAFCenterActionExplanation — explain why an action was chosen #> # ============================================================ # AGENT REGISTRY # ============================================================ $script:LoadedAgents = @{} # ============================================================ # REGISTER-VBAFCENTERAGENT # ============================================================ function Register-VBAFCenterAgent { param( [Parameter(Mandatory)] [string] $AgentName, [Parameter(Mandatory)] [object] $Agent, [string] $Description = "" ) $script:LoadedAgents[$AgentName] = @{ Agent = $Agent Description = $Description LoadedAt = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") } Write-Host "Agent registered: $AgentName" -ForegroundColor Green } # ============================================================ # GET-VBAFCENTERACTIONTHRESHOLDS (internal helper — Phase 17 preview) # ============================================================ function Get-VBAFCenterActionThresholds { param( [Parameter(Mandatory)] [string] $CustomerID ) # Defaults — same as original fixed values $thresholds = @{ Action1 = 0.25 Action2 = 0.50 Action3 = 0.75 } # Read from schedule.json if customer-specific thresholds are stored $schedPath = Join-Path $env:USERPROFILE "VBAFCenter\schedules\$CustomerID-schedule.json" if (Test-Path $schedPath) { try { $sched = Get-Content $schedPath -Raw | ConvertFrom-Json if ($null -ne $sched.Action1Threshold) { $thresholds.Action1 = [double] $sched.Action1Threshold } if ($null -ne $sched.Action2Threshold) { $thresholds.Action2 = [double] $sched.Action2Threshold } if ($null -ne $sched.Action3Threshold) { $thresholds.Action3 = [double] $sched.Action3Threshold } } catch { # If schedule unreadable — use defaults silently } } return $thresholds } # ============================================================ # INVOKE-VBAFCENTERROUTE # ============================================================ function Invoke-VBAFCenterRoute { param( [Parameter(Mandatory)] [string] $CustomerID, [Parameter(Mandatory)] [double[]] $NormalisedSignals, [string] $AgentOverride = "", [double] $WeightedAvg = -1, # Phase 15 — from Get-VBAFCenterAllSignals [object[]] $RedSignals = @(), # Phase 14 — signals in Red state [object[]] $YellowSignals = @() # Phase 14 — signals in Yellow state ) # Load customer profile to find agent $profilePath = Join-Path $env:USERPROFILE "VBAFCenter\customers\$CustomerID.json" if (-not (Test-Path $profilePath)) { Write-Host "Customer not found: $CustomerID" -ForegroundColor Red return $null } $profile = Get-Content $profilePath -Raw | ConvertFrom-Json $agentName = if ($AgentOverride -ne "") { $AgentOverride } else { $profile.Agent } # Phase 17 — load customer-specific action thresholds $thresholds = Get-VBAFCenterActionThresholds -CustomerID $CustomerID $customThresholds = $thresholds.Action1 -ne 0.25 -or ` $thresholds.Action2 -ne 0.50 -or ` $thresholds.Action3 -ne 0.75 Write-Host "" Write-Host "Routing to agent: $agentName" -ForegroundColor Cyan Write-Host (" Customer : {0}" -f $CustomerID) -ForegroundColor White Write-Host (" Signals : [{0}]" -f ($NormalisedSignals -join ", ")) -ForegroundColor White if ($customThresholds) { Write-Host (" Thresholds: Action1={0} Action2={1} Action3={2} [customer-specific]" -f ` $thresholds.Action1, $thresholds.Action2, $thresholds.Action3) -ForegroundColor Cyan } # -------------------------------------------------------- # STEP 1 — Calculate baseline average # -------------------------------------------------------- [double] $simpleAvg = 0.0 foreach ($s in $NormalisedSignals) { $simpleAvg += $s } if ($NormalisedSignals.Length -gt 0) { $simpleAvg /= $NormalisedSignals.Length } # Phase 15 — use weighted average if provided, else fall back to simple [double] $avgUsed = if ($WeightedAvg -ge 0) { $WeightedAvg } else { $simpleAvg } $avgLabel = if ($WeightedAvg -ge 0) { "weighted" } else { "simple" } Write-Host (" Avg used : {0:F4} ({1})" -f $avgUsed, $avgLabel) -ForegroundColor White # -------------------------------------------------------- # STEP 2 — Get baseline action from agent or rule-based # -------------------------------------------------------- [int] $baseAction = 0 [string] $agentMode = "" if ($script:LoadedAgents.ContainsKey($agentName)) { $agent = $script:LoadedAgents[$agentName].Agent $baseAction = [int] $agent.Act($NormalisedSignals) $agentMode = "trained agent" } else { $baseAction = if ($avgUsed -lt $thresholds.Action1) { 0 } elseif ($avgUsed -lt $thresholds.Action2) { 1 } elseif ($avgUsed -lt $thresholds.Action3) { 2 } else { 3 } $agentMode = "rule-based fallback" } # -------------------------------------------------------- # STEP 3 — Phase 14 threshold overrides # -------------------------------------------------------- [int] $finalAction = $baseAction [string] $actionReason = ("Average signal {0:F4} => baseline action {1}" -f $avgUsed, $baseAction) [bool] $overrideApplied = $false $redCount = if ($RedSignals) { @($RedSignals).Count } else { 0 } $yellowCount = if ($YellowSignals) { @($YellowSignals).Count } else { 0 } # Rule 1: ANY Red signal raises minimum action to 2 (Reroute) if ($redCount -gt 0 -and $finalAction -lt 2) { $redNames = ($RedSignals | ForEach-Object { $_.SignalName }) -join ", " $finalAction = 2 $actionReason = ("RED signal override: {0} — raised to Reroute" -f $redNames) $overrideApplied = $true } # Rule 2: 2 or more Red signals raises minimum action to 3 (Escalate) if ($redCount -ge 2 -and $finalAction -lt 3) { $redNames = ($RedSignals | ForEach-Object { $_.SignalName }) -join ", " $finalAction = 3 $actionReason = ("MULTIPLE RED signals: {0} — raised to Escalate" -f $redNames) $overrideApplied = $true } # Rule 3: 2 or more Yellow signals raises minimum action to 1 (Reassign) if ($yellowCount -ge 2 -and $finalAction -lt 1) { $yellowNames = ($YellowSignals | ForEach-Object { $_.SignalName }) -join ", " $finalAction = 1 $actionReason = ("YELLOW signals: {0} — raised to Reassign" -f $yellowNames) $overrideApplied = $true } # -------------------------------------------------------- # STEP 4 — Output # -------------------------------------------------------- $actionNames = @("Monitor", "Reassign", "Reroute", "Escalate") $actionColours = @("Green", "Yellow", "DarkRed", "Red") Write-Host (" Agent : {0} ({1})" -f $agentName, $agentMode) -ForegroundColor White if ($overrideApplied) { Write-Host (" Base : {0} — {1}" -f $baseAction, $actionNames[$baseAction]) -ForegroundColor DarkGray Write-Host (" OVERRIDE : {0}" -f $actionReason) -ForegroundColor Red } Write-Host (" Decision : {0} — {1}" -f $finalAction, $actionNames[$finalAction]) ` -ForegroundColor $actionColours[$finalAction] if ($redCount -gt 0) { Write-Host (" Red signals : {0}" -f $redCount) -ForegroundColor Red } if ($yellowCount -gt 0) { Write-Host (" Yellow signals : {0}" -f $yellowCount) -ForegroundColor Yellow } Write-Host "" return [PSCustomObject] @{ CustomerID = $CustomerID AgentName = $agentName AgentMode = $agentMode Signals = $NormalisedSignals SimpleAvg = [Math]::Round($simpleAvg, 4) WeightedAvg = if ($WeightedAvg -ge 0) { [Math]::Round($WeightedAvg, 4) } else { $null } AvgUsed = [Math]::Round($avgUsed, 4) BaseAction = $baseAction FinalAction = $finalAction ActionName = $actionNames[$finalAction] ActionReason = $actionReason OverrideApplied = $overrideApplied RedSignalCount = $redCount YellowSignalCount = $yellowCount Thresholds = $thresholds CustomThresholds = $customThresholds Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") } } # ============================================================ # GET-VBAFCENTERACTIONEXPLANATION (new — Phase 14) # ============================================================ function Get-VBAFCenterActionExplanation { <# .SYNOPSIS Shows a human-readable explanation of the last routing decision. Pass the result object from Invoke-VBAFCenterRoute. .EXAMPLE $result = Invoke-VBAFCenterRoute -CustomerID "TruckCompanyDK" -NormalisedSignals $signals Get-VBAFCenterActionExplanation -RouteResult $result #> param( [Parameter(Mandatory)] [object] $RouteResult ) $actionNames = @("Monitor", "Reassign", "Reroute", "Escalate") $actionColours = @("Green", "Yellow", "DarkRed", "Red") $action = $RouteResult.FinalAction $colour = $actionColours[$action] Write-Host "" Write-Host "Action Explanation: $($RouteResult.CustomerID)" -ForegroundColor Cyan Write-Host (" Decision : {0} — {1}" -f $action, $actionNames[$action]) -ForegroundColor $colour Write-Host (" Reason : {0}" -f $RouteResult.ActionReason) -ForegroundColor White Write-Host "" Write-Host " Signal average:" -ForegroundColor DarkGray if ($null -ne $RouteResult.WeightedAvg) { Write-Host (" Simple avg : {0:F4}" -f $RouteResult.SimpleAvg) -ForegroundColor DarkGray Write-Host (" Weighted avg : {0:F4} (used for decision)" -f $RouteResult.WeightedAvg) -ForegroundColor White } else { Write-Host (" Simple avg : {0:F4} (used for decision)" -f $RouteResult.SimpleAvg) -ForegroundColor White } Write-Host "" Write-Host (" Action thresholds ({0}):" -f ` (if ($RouteResult.CustomThresholds) { "customer-specific" } else { "default" })) -ForegroundColor DarkGray Write-Host (" Reassign above : {0}" -f $RouteResult.Thresholds.Action1) -ForegroundColor DarkGray Write-Host (" Reroute above : {0}" -f $RouteResult.Thresholds.Action2) -ForegroundColor DarkGray Write-Host (" Escalate above : {0}" -f $RouteResult.Thresholds.Action3) -ForegroundColor DarkGray if ($RouteResult.OverrideApplied) { Write-Host "" Write-Host " Threshold override applied:" -ForegroundColor Red Write-Host (" Base action was : {0} — {1}" -f ` $RouteResult.BaseAction, $actionNames[$RouteResult.BaseAction]) -ForegroundColor DarkGray Write-Host (" Raised to : {0} — {1} due to signal colours" -f ` $RouteResult.FinalAction, $actionNames[$RouteResult.FinalAction]) -ForegroundColor Red Write-Host (" Red signals : {0}" -f $RouteResult.RedSignalCount) -ForegroundColor Red Write-Host (" Yellow signals : {0}" -f $RouteResult.YellowSignalCount) -ForegroundColor Yellow } Write-Host "" } # ============================================================ # GET-VBAFCENTERROUTESTATUS # ============================================================ function Get-VBAFCenterRouteStatus { Write-Host "" Write-Host "Agent Router Status:" -ForegroundColor Cyan if ($script:LoadedAgents.Count -eq 0) { Write-Host " No agents loaded yet." -ForegroundColor Yellow Write-Host " Using rule-based fallback for all routes." -ForegroundColor Yellow } else { Write-Host (" {0,-25} {1,-20} {2}" -f "Agent","Loaded At","Description") -ForegroundColor Yellow Write-Host (" {0}" -f ("-" * 65)) -ForegroundColor DarkGray foreach ($key in $script:LoadedAgents.Keys) { $a = $script:LoadedAgents[$key] Write-Host (" {0,-25} {1,-20} {2}" -f $key, $a.LoadedAt, $a.Description) -ForegroundColor White } } Write-Host "" } # ============================================================ # LOAD MESSAGE # ============================================================ Write-Host "VBAF-Center Phase 5 loaded [Agent Router + Phase 14 Overrides + Phase 15 Weights + Phase 17 Thresholds]" -ForegroundColor Cyan Write-Host " Invoke-VBAFCenterRoute — route signals to agent" -ForegroundColor White Write-Host " Register-VBAFCenterAgent — register a trained agent" -ForegroundColor White Write-Host " Get-VBAFCenterRouteStatus — show loaded agents" -ForegroundColor White Write-Host " Get-VBAFCenterActionExplanation — explain why action was chosen" -ForegroundColor Cyan Write-Host "" |