Public/Invoke-AIByUsage.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Invoke-AIByUsage { <# .SYNOPSIS Invoke an AI model by UsageID. Resolves parameters from ai-usages.json and delegates to Invoke-AIApi. .DESCRIPTION Central entry point for the UsageID pattern in PowerShell. Reads the registry at repo-root/ai-usages.json, resolves the UsageID (with _extends chain), renders any {{var}} templates from -Values, applies the optional -Override hashtable, then splats the resulting parameters into Invoke-AIApi. Delegating to Invoke-AIApi preserves the existing multi-backend routing (Gemini/Claude/Groq/OpenAI/Azure), retry with fallback model chains, key rotation via Resolve-AIApiKey, and pre-flight token check. Invoke-AIByUsage is a config resolver, not a replacement executor. Design mirrors the TypeScript server-side generateTextByUsage pattern (t/1262) — see docs/design/adr/ for rationale. .PARAMETER UsageId Identifier from ai-usages.json (e.g., 'enrichment.metadata-extraction'). Tab-completes against the top-level keys in ai-usages.json. .PARAMETER Values Hashtable of template placeholder values. Every {{var}} in messageTemplate/systemMessageTemplate must have a matching key or an ActionableError is thrown at render time. .PARAMETER Override Optional hashtable of per-call parameter overrides. Keys supported: model, temperature, maxTokens, timeoutMs, jsonMode, responseSchema, systemMessage. Useful for experimentation without touching ai-usages.json. .PARAMETER ApiKey Explicit API key. If empty, Invoke-AIApi resolves via Resolve-AIApiKey. .PARAMETER FallbackModels Explicit fallback chain. If omitted, Invoke-AIApi uses the chain declared in ai-models.json for the resolved model. .OUTPUTS The result object from Invoke-AIApi (six-property .Text/.Backend/etc). .EXAMPLE Invoke-AIByUsage -UsageId 'enrichment.metadata-extraction' -Values @{ source_url = 'https://example.com/doc' fallback_title = 'Untitled' markdown_text = $Doc.Content } .EXAMPLE Invoke-AIByUsage -UsageId 'enrichment.vernacular-description' ` -Values @{ node_id='acc-b-001'; description='...'; category='belief' } ` -Override @{ temperature = 0.3 } .LINK Show-AITriadHelp .LINK Invoke-BDIWeightAssignment .LINK Invoke-EdgeWeightEvaluation .LINK Invoke-VernacularBatch .LINK Invoke-AphorismBatch .LINK New-SyntheticCorpus #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [ArgumentCompleter({ param($cmd, $param, $wordToComplete) $reg = Get-UsageRegistry -ErrorAction SilentlyContinue if (-not $reg) { return } $reg.PSObject.Properties.Name | Where-Object { $_ -notmatch '^_' -and $_ -like "$wordToComplete*" } | Sort-Object })] [ValidateNotNullOrEmpty()] [string]$UsageId, [hashtable]$Values = @{}, [hashtable]$Override = @{}, [string]$ApiKey = '', [string[]]$FallbackModels ) Set-StrictMode -Version Latest # Resolve config with _extends chain $config = Get-UsageConfig -UsageId $UsageId # Apply -Override on top of resolved config foreach ($k in @($Override.Keys)) { $config[$k] = $Override[$k] } # t/1552 — Lint: {{placeholders}} in a non-template field are the classic # silent-no-substitution footgun (t/1550#3). Warn naming the field and # its intended *Template counterpart. Warning not error — a literal # {{...}} in prompt text is conceivable, so don't hard-fail. $literalFields = @{ systemMessage = 'systemMessageTemplate'; message = 'messageTemplate' } foreach ($k in $literalFields.Keys) { if ($config.ContainsKey($k) -and $config[$k] -and ([string]$config[$k]) -match '\{\{[^{}]+\}\}') { Write-Warning "UsageID '$UsageId' field '$k' contains {{placeholder}} syntax but is not rendered — placeholders in this field are passed to the model literally. Rename the field to '$($literalFields[$k])' to enable substitution, or remove the {{...}} if the literal text is intentional." } } # Render templates $systemMessage = '' if ($config.ContainsKey('systemMessageTemplate') -and $config['systemMessageTemplate']) { $systemMessage = Convert-UsageTemplate -Template ([string]$config['systemMessageTemplate']) -Values $Values -UsageIdContext $UsageId } elseif ($config.ContainsKey('systemMessage') -and $config['systemMessage']) { $systemMessage = [string]$config['systemMessage'] } $userMessage = '' if ($config.ContainsKey('messageTemplate') -and $config['messageTemplate']) { $userMessage = Convert-UsageTemplate -Template ([string]$config['messageTemplate']) -Values $Values -UsageIdContext $UsageId } elseif ($config.ContainsKey('message') -and $config['message']) { $userMessage = [string]$config['message'] } if ([string]::IsNullOrWhiteSpace($userMessage)) { throw (New-ActionableError -PassThru ` -Goal 'Invoke AI by UsageID' ` -Problem "UsageID '$UsageId' produced an empty user message after template rendering" ` -Location 'Invoke-AIByUsage' ` -NextSteps @( "Confirm messageTemplate or message is defined for '$UsageId' in ai-usages.json", 'Confirm -Values populates every {{var}} in the template' )) } if (-not $config.ContainsKey('model')) { throw (New-ActionableError -PassThru ` -Goal 'Invoke AI by UsageID' ` -Problem "UsageID '$UsageId' does not declare a model" ` -Location 'Invoke-AIByUsage' ` -NextSteps @("Add a 'model' field to the '$UsageId' entry in ai-usages.json")) } # Build Invoke-AIApi parameter splat $invokeParams = @{ Prompt = $userMessage Model = [string]$config['model'] } if ($systemMessage) { $invokeParams['SystemInstruction'] = $systemMessage } if (-not [string]::IsNullOrWhiteSpace($ApiKey)) { $invokeParams['ApiKey'] = $ApiKey } if ($config.ContainsKey('temperature')) { $invokeParams['Temperature'] = [double]$config['temperature'] } if ($config.ContainsKey('maxTokens')) { $invokeParams['MaxTokens'] = [int]$config['maxTokens'] } if ($config.ContainsKey('timeoutMs')) { $invokeParams['TimeoutSec'] = [int]([math]::Ceiling(([int]$config['timeoutMs']) / 1000.0)) } if ($config.ContainsKey('jsonMode') -and [bool]$config['jsonMode']) { $invokeParams['JsonMode'] = [switch]::Present } if ($config.ContainsKey('responseSchema')) { # Response schema may be a hashtable or PSCustomObject from JSON; coerce to hashtable $schema = $config['responseSchema'] if ($schema -is [hashtable]) { $invokeParams['ResponseSchema'] = $schema } elseif ($schema -is [PSCustomObject]) { $h = @{} foreach ($p in $schema.PSObject.Properties) { $h[$p.Name] = $p.Value } $invokeParams['ResponseSchema'] = $h } } if ($PSBoundParameters.ContainsKey('FallbackModels')) { $invokeParams['FallbackModels'] = $FallbackModels } Write-Verbose "Invoke-AIByUsage: UsageID='$UsageId' → Model='$($invokeParams.Model)' Temp=$($invokeParams['Temperature']) MaxTokens=$($invokeParams['MaxTokens'])" return (Invoke-AIApi @invokeParams) } |