Public/Invoke-AITDebate.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. <# .SYNOPSIS Runs a structured multi-perspective AI debate using the shared debate library. .DESCRIPTION Orchestrates a full debate with Prometheus (accelerationist), Sentinel (safetyist), and Cassandra (skeptic) POVers. Produces debate transcript, diagnostics, and harvest output files. Uses the same prompts, logic, and argumentation framework as the Taxonomy Editor's debate tool. .EXAMPLE Invoke-AITDebate -Topic "Should the US impose AI licensing?" -Turns 3 .EXAMPLE Invoke-AITDebate -Topic "Scaling limits" -Name "Scaling Debate" -Rounds 4 -Model gemini-2.5-flash .EXAMPLE Invoke-AITDebate -DocPath ../ai-triad-data/sources/my-doc/snapshot.md -Name "My Doc Debate" .EXAMPLE Invoke-AITDebate -CrossCuttingNodeId sit-005 -Clarify -Probe #> function Invoke-AITDebate { [CmdletBinding(DefaultParameterSetName = 'Topic')] param( [Parameter(Mandatory, ParameterSetName = 'Topic', Position = 0)] [string]$Topic, [Parameter(Mandatory, ParameterSetName = 'Document')] [ValidateScript({ Test-Path $_ })] [Alias('DocumentPath')] [string]$DocPath, [Parameter(Mandatory, ParameterSetName = 'Url')] [string]$Url, [Parameter(Mandatory, ParameterSetName = 'CrossCutting')] [string]$CrossCuttingNodeId, [Parameter()] [string]$Name, [Parameter()] [ValidateSet('prometheus', 'sentinel', 'cassandra')] [string[]]$Debaters = @('prometheus', 'sentinel', 'cassandra'), [Parameter()] [ValidateScript({ Test-AIModelId $_ })] [ArgumentCompleter({ param($cmd, $param, $word) $script:ValidModelIds | Where-Object { $_ -like "$word*" } })] [string]$Model, [Parameter()] [ValidateRange(1, 20)] [int]$Rounds = 3, [Parameter()] [ValidateSet('brief', 'medium', 'detailed')] [string]$ResponseLength = 'medium', [Parameter()] [ValidateSet('structured', 'socratic', 'deliberation')] [string]$Protocol = 'structured', [Parameter()] [switch]$Clarify, [Parameter()] [switch]$Probe, [Parameter()] [int]$ProbeEvery = 2, [Parameter()] [string]$OutputDirectory, [Parameter()] [ValidateSet('json', 'markdown')] [string]$OutputFormat = 'json', [Parameter()] [string]$ApiKey, [Parameter()] [double]$Temperature = 0.3 ) Set-StrictMode -Version Latest # ── Validate prerequisites ──────────────────────────── if ($Debaters.Count -lt 2) { throw "At least 2 debaters are required. Got: $($Debaters -join ', ')" } # Verify npx is available (prefer .cmd on Windows — .ps1 can't be launched via Process.Start) $NpxCmd = Get-Command npx.cmd -ErrorAction SilentlyContinue if (-not $NpxCmd) { $NpxCmd = Get-Command npx -ErrorAction SilentlyContinue } if (-not $NpxCmd) { throw @" npx (Node.js package runner) is not installed. Required to run the debate CLI engine. Install Node.js from https://nodejs.org (v18+), then verify: npx --version "@ } # Resolve model if ($Model) { $ResolvedModel = $Model } elseif ($env:AI_MODEL) { $ResolvedModel = $env:AI_MODEL } else { $ResolvedModel = 'gemini-2.5-flash' } # ── Resolve output directory ────────────────────────── if (-not $OutputDirectory) { try { $DebatesDir = Get-DebatesDir $OutputDirectory = Join-Path $DebatesDir 'cli-runs' } catch { $OutputDirectory = Join-Path $PWD 'debates' } } if (-not (Test-Path $OutputDirectory)) { try { $null = New-Item -Path $OutputDirectory -ItemType Directory -Force -ErrorAction Stop } catch { throw "Failed to create output directory '$OutputDirectory': $_`nCheck that the parent directory exists and you have write permissions." } } # ── Generate slug ───────────────────────────────────── $DebateTopic = switch ($PSCmdlet.ParameterSetName) { 'Topic' { $Topic } 'Document' { "Document debate: $(Split-Path $DocPath -Leaf)" } 'Url' { "URL debate: $Url" } 'CrossCutting' { "Cross-cutting: $CrossCuttingNodeId" } } if ($Name) { $SlugSource = $Name } else { $SlugSource = $DebateTopic } $Slug = New-Slug -Text $SlugSource # ── Build config JSON ───────────────────────────────── $Config = @{ activePovers = $Debaters model = $ResolvedModel rounds = $Rounds responseLength = $ResponseLength protocolId = $Protocol enableClarification = [bool]$Clarify enableProbing = [bool]$Probe probingInterval = $ProbeEvery outputDir = (Resolve-Path $OutputDirectory).Path outputFormat = $OutputFormat slug = $Slug temperature = $Temperature } if ($Name) { $Config.name = $Name } switch ($PSCmdlet.ParameterSetName) { 'Topic' { $Config.topic = $Topic } 'Document' { $Config.docPath = (Resolve-Path $DocPath).Path } 'Url' { $Config.url = $Url } 'CrossCutting' { $Config.crossCuttingId = $CrossCuttingNodeId } } if ($ApiKey) { $Config.apiKey = $ApiKey } # ── Write config temp file ──────────────────────────── try { $ConfigPath = [System.IO.Path]::GetTempFileName() $Config | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigPath -Encoding UTF8 -ErrorAction Stop } catch { throw "Failed to write debate config to temp file: $_`nCheck that $([System.IO.Path]::GetTempPath()) is writable and has free space." } try { # ── Locate CLI ──────────────────────────────────── $RepoRoot = Split-Path $PSScriptRoot -Parent | Split-Path -Parent | Split-Path -Parent $CliPath = Join-Path (Join-Path (Join-Path $RepoRoot 'lib') 'debate') 'cli.ts' if (-not (Test-Path $CliPath)) { throw @" Debate CLI not found at: $CliPath Expected repo structure: lib/debate/cli.ts Computed repo root: $RepoRoot This usually means: (1) repo not checked out correctly, (2) lib/debate was not built, or (3) running from a non-standard location. Verify the file exists: Get-Item '$CliPath' "@ } Write-Verbose "Running debate CLI: npx tsx $CliPath --config $ConfigPath" Write-Verbose "Model: $ResolvedModel | Rounds: $Rounds | Debaters: $($Debaters -join ', ')" # ── Run the Node.js CLI ─────────────────────────── $StdOut = [System.Collections.Generic.List[string]]::new() $StdErr = [System.Collections.Generic.List[string]]::new() $Psi = [System.Diagnostics.ProcessStartInfo]::new() $Psi.FileName = $NpxCmd.Source $Psi.Arguments = "tsx `"$CliPath`" --config `"$ConfigPath`"" $Psi.WorkingDirectory = $RepoRoot $Psi.RedirectStandardOutput = $true $Psi.RedirectStandardError = $true $Psi.UseShellExecute = $false $Psi.CreateNoWindow = $true try { $Proc = [System.Diagnostics.Process]::Start($Psi) } catch { throw "Failed to start debate CLI process (npx tsx): $_`nVerify Node.js is installed and npx is in your PATH: npx --version" } # Stream stderr for progress while (-not $Proc.StandardError.EndOfStream) { $Line = $Proc.StandardError.ReadLine() if ($Line) { $StdErr.Add($Line) Write-Host $Line -ForegroundColor DarkGray } } $StdOutText = $Proc.StandardOutput.ReadToEnd() # Wait with timeout (10 minutes max for a full debate) if (-not $Proc.WaitForExit(600000)) { try { $Proc.Kill() } catch { } throw @" Debate CLI process timed out after 10 minutes. This may indicate: the AI API is unresponsive, the model is overloaded, or the debate has too many rounds. Try: reduce -Rounds, use a faster -Model, or check your API key and network connectivity. Stderr output: $($StdErr -join "`n" | Select-Object -Last 20) "@ } if ($StdOutText) { $StdOut.Add($StdOutText) } # ── Parse result ────────────────────────────────── $ResultJson = $StdOut -join "`n" if ($Proc.ExitCode -ne 0 -and -not $ResultJson) { throw @" Debate CLI failed with exit code $($Proc.ExitCode). Stderr: $($StdErr -join "`n" | Select-Object -Last 20) Troubleshooting: 1. Check API key: ensure GEMINI_API_KEY (or ANTHROPIC_API_KEY/GROQ_API_KEY) is set 2. Check model: verify '$ResolvedModel' is a valid model in ai-models.json 3. Run with -Verbose for more detail "@ } if (-not $ResultJson) { throw @" Debate CLI produced no output (exit code: $($Proc.ExitCode)). Stderr: $($StdErr -join "`n" | Select-Object -Last 20) This usually means the CLI crashed before producing results. Run with -Verbose for debugging. "@ } try { $Result = $ResultJson | ConvertFrom-Json -ErrorAction Stop } catch { throw @" Failed to parse debate CLI response as JSON: $_ First 300 chars of output: $($ResultJson.Substring(0, [Math]::Min(300, $ResultJson.Length))) This usually means the CLI produced non-JSON output. Check stderr above for errors. "@ } if (-not $Result.success) { throw "Debate failed: $($Result.error)" } # ── Return structured result ────────────────────── [PSCustomObject]@{ DebateId = $Result.debateId Name = $Result.name Slug = $Result.slug Topic = $Result.topic DebateFile = $Result.files.debate TranscriptFile = $Result.files.transcript DiagnosticsFile = $Result.files.diagnostics HarvestFile = $Result.files.harvest MarkdownFile = $Result.files.markdown Rounds = $Result.stats.rounds Entries = $Result.stats.entries ApiCalls = $Result.stats.apiCalls TotalTimeMs = $Result.stats.totalTimeMs ClaimsAccepted = $Result.stats.claimsAccepted ClaimsRejected = $Result.stats.claimsRejected Success = $true } } finally { Remove-Item -Path $ConfigPath -ErrorAction SilentlyContinue } } |