Public/Invoke-EdgeWeightEvaluation.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Invoke-EdgeWeightEvaluation { <# .SYNOPSIS AI-powered batch evaluation of edge weights. .DESCRIPTION Sends batches of edges to an LLM to assign relationship weights (0.0-1.0). Weight measures how strong the relationship is, independent of confidence (which measures whether the edge exists at all). Only evaluates edges that don't already have a weight assigned. .PARAMETER Model AI model to use. Default: gemini-2.5-flash. .PARAMETER BatchSize Number of edges per API call. Default: 15. .PARAMETER Status Only evaluate edges with this status. Default: approved. .PARAMETER MaxBatches Stop after this many batches (0 = unlimited). Default: 0. .PARAMETER Force Re-evaluate edges that already have weights. .PARAMETER ApiKey API key override. .PARAMETER RepoRoot Path to the repository root. .EXAMPLE Invoke-EdgeWeightEvaluation # Evaluate all approved edges without weights. .EXAMPLE Invoke-EdgeWeightEvaluation -Force -MaxBatches 10 # Re-evaluate first 150 approved edges. .EXAMPLE Invoke-EdgeWeightEvaluation -Status proposed # Evaluate proposed edges. #> [CmdletBinding(SupportsShouldProcess)] param( [string]$Model = 'gemini-2.5-flash', [int]$BatchSize = 30, [ValidateSet('proposed', 'approved', 'rejected', '')] [string]$Status = 'approved', [int]$MaxBatches = 0, [switch]$Force, [string]$ApiKey = '', [string]$RepoRoot = $script:RepoRoot ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $TaxDir = Get-TaxonomyDir $EdgesPath = Join-Path $TaxDir 'edges.json' if (-not (Test-Path $EdgesPath)) { Write-Fail 'No edges.json found.' return } # ── Load data ── $EdgesData = Get-Content -Raw -Path $EdgesPath | ConvertFrom-Json $AllEdges = $EdgesData.edges # ── Build label + description lookup ── $Labels = @{} $Descriptions = @{} foreach ($PovKey in @('accelerationist', 'safetyist', 'skeptic', 'situations')) { $FilePath = Join-Path $TaxDir "$PovKey.json" if (-not (Test-Path $FilePath)) { continue } $FileData = Get-Content -Raw -Path $FilePath | ConvertFrom-Json foreach ($Node in $FileData.nodes) { $Labels[$Node.id] = $Node.label if ($Node.PSObject.Properties['description'] -and $Node.description) { $Desc = $Node.description if ($Desc.Length -gt 120) { $Desc = $Desc.Substring(0, 120) + '...' } $Descriptions[$Node.id] = $Desc } } } # ── Filter to edges needing weight ── $Candidates = [System.Collections.Generic.List[PSObject]]::new() for ($i = 0; $i -lt $AllEdges.Count; $i++) { $E = $AllEdges[$i] if ($Status -and $E.status -ne $Status) { continue } if (-not $Force -and $E.PSObject.Properties['weight'] -and $null -ne $E.weight) { continue } $Candidates.Add([PSCustomObject]@{ Index = $i; Edge = $E }) } $TotalCandidates = $Candidates.Count Write-Info "Found $TotalCandidates edges needing weight evaluation (status=$Status, force=$Force)" if ($TotalCandidates -eq 0) { Write-Info 'Nothing to evaluate.' return } # ── Resolve API key ── $Backend = if ($Model -match '^gemini') { 'gemini' } elseif ($Model -match '^claude') { 'claude' } else { 'groq' } $ResolvedKey = Resolve-AIApiKey -ExplicitKey $ApiKey -Backend $Backend if ([string]::IsNullOrWhiteSpace($ResolvedKey)) { Write-Fail "No API key found for $Backend. Set `$env:$($Backend.ToUpper())_API_KEY." return } # ── Load prompt template ── $PromptTemplate = Get-Prompt -Name 'edge-weight-evaluation' -AllowUnresolved # ── Batch processing ── $TotalBatches = [Math]::Ceiling($TotalCandidates / $BatchSize) if ($MaxBatches -gt 0) { $TotalBatches = [Math]::Min($TotalBatches, $MaxBatches) } $EvaluatedCount = 0 $ErrorCount = 0 for ($b = 0; $b -lt $TotalBatches; $b++) { $Start = $b * $BatchSize $End = [Math]::Min($Start + $BatchSize, $TotalCandidates) - 1 $Batch = $Candidates[$Start..$End] # Build edge descriptions with node context $EdgeLines = [System.Collections.Generic.List[string]]::new() foreach ($Item in $Batch) { $E = $Item.Edge $SrcLabel = if ($Labels.ContainsKey($E.source)) { $Labels[$E.source] } else { $E.source } $TgtLabel = if ($Labels.ContainsKey($E.target)) { $Labels[$E.target] } else { $E.target } $SrcDesc = if ($Descriptions.ContainsKey($E.source)) { " — $($Descriptions[$E.source])" } else { '' } $TgtDesc = if ($Descriptions.ContainsKey($E.target)) { " — $($Descriptions[$E.target])" } else { '' } $Dir = if ($E.bidirectional) { '↔' } else { '→' } $EdgeLines.Add(" index=$($Item.Index) | $($E.type) | `"$SrcLabel`" ($($E.source))$SrcDesc $Dir `"$TgtLabel`" ($($E.target))$TgtDesc | Rationale: $($E.rationale)") } $FullPrompt = $PromptTemplate -replace '\{\{EDGES\}\}', ($EdgeLines -join "`n") Write-Progress -Activity 'Evaluating edge weights' ` -Status "Batch $($b + 1) / $TotalBatches — $EvaluatedCount evaluated so far" ` -PercentComplete ([int](($b / $TotalBatches) * 100)) if (-not $PSCmdlet.ShouldProcess("Batch $($b + 1) ($($Batch.Count) edges)", 'Evaluate weights')) { continue } try { $Response = Invoke-AIApi ` -Prompt $FullPrompt ` -Model $Model ` -ApiKey $ResolvedKey ` -Temperature 0.2 ` -MaxTokens 4096 ` -TimeoutSec 120 if ($null -eq $Response) { Write-Warning "Batch $($b + 1): API returned null" $ErrorCount++ continue } $Text = $Response.Text -replace '^\s*```json\s*', '' -replace '\s*```\s*$', '' $Results = $null try { $Results = $Text | ConvertFrom-Json } catch { $Repaired = Repair-TruncatedJson -Text $Text if ($Repaired) { $Results = $Repaired | ConvertFrom-Json } else { Write-Warning "Batch $($b + 1): Failed to parse response" $ErrorCount++ continue } } if ($Results -and $Results.Count -gt 0) { foreach ($R in $Results) { $Idx = [int]$R.index $W = [double]$R.weight if ($W -lt 0.0 -or $W -gt 1.0) { Write-Warning " edg-$($Idx + 1): weight $W out of range, clamping" $W = [Math]::Max(0.0, [Math]::Min(1.0, $W)) } $AllEdges[$Idx] | Add-Member -NotePropertyName 'weight' -NotePropertyValue $W -Force $EvaluatedCount++ $SrcLabel = if ($Labels.ContainsKey($AllEdges[$Idx].source)) { $Labels[$AllEdges[$Idx].source] } else { $AllEdges[$Idx].source } $TgtLabel = if ($Labels.ContainsKey($AllEdges[$Idx].target)) { $Labels[$AllEdges[$Idx].target] } else { $AllEdges[$Idx].target } Write-Info " edg-$($Idx + 1): w=$($W.ToString('F2')) — $SrcLabel → $TgtLabel" } } } catch { Write-Warning "Batch $($b + 1): Exception — $_" $ErrorCount++ } # Checkpoint every 10 batches if (($b + 1) % 10 -eq 0) { $EdgesData.edges = $AllEdges $EdgesData.last_modified = (Get-Date).ToString('yyyy-MM-dd') $Json = $EdgesData | ConvertTo-Json -Depth 20 Write-Utf8NoBom -Path $EdgesPath -Value $Json Write-Info "Checkpoint at batch $($b + 1): $EvaluatedCount evaluated" } } Write-Progress -Activity 'Evaluating edge weights' -Completed # ── Final save ── $EdgesData.edges = $AllEdges $EdgesData.last_modified = (Get-Date).ToString('yyyy-MM-dd') $Json = $EdgesData | ConvertTo-Json -Depth 20 Write-Utf8NoBom -Path $EdgesPath -Value $Json Write-OK "Done: $EvaluatedCount edges evaluated, $ErrorCount errors" } |