Private/Get-Prompt.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Get-Prompt { <# .SYNOPSIS Loads a .prompt file from the Prompts/ directory with optional placeholder replacement. .DESCRIPTION Reads a prompt template from scripts/AITriad/Prompts/<Name>.prompt, caches the raw text in $script:PromptCache, and optionally substitutes {{KEY}} placeholders with values from the -Replacements hashtable. .PARAMETER Name Base name of the prompt file (without .prompt extension). .PARAMETER Replacements Hashtable of placeholder replacements. Keys are matched as {{KEY}} in the prompt text (case-sensitive). .EXAMPLE Get-Prompt -Name 'pov-summary-system' .EXAMPLE Get-Prompt -Name 'triad-dialogue-system' -Replacements @{ AGENT_NAME = 'Accelerationist'; POV_LABEL = 'Accelerationist' } #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$Name, [hashtable]$Replacements = @{}, [switch]$AllowUnresolved ) if (-not (Get-Variable -Name 'PromptCache' -Scope Script -ErrorAction SilentlyContinue)) { $script:PromptCache = @{} } if (-not $script:PromptCache.ContainsKey($Name)) { $PromptPath = Join-Path (Join-Path $script:ModuleRoot 'Prompts') "$Name.prompt" if (-not (Test-Path $PromptPath)) { throw "Prompt file not found: $PromptPath" } $script:PromptCache[$Name] = (Get-Content -Path $PromptPath -Raw -Encoding UTF8).TrimEnd() } $Text = $script:PromptCache[$Name] foreach ($Key in $Replacements.Keys) { $Text = $Text -replace [regex]::Escape("{{$Key}}"), $Replacements[$Key] } # t/1334 — fragment injection: for any {{name}} placeholder that had no caller # value, check for Prompts/<name>.fragment.prompt and inline its contents. # Single-pass / non-recursive: fragments are NOT re-scanned for nested placeholders. # Caller replacements above have already run, so they take precedence. if (-not (Get-Variable -Name 'PromptFragmentCache' -Scope Script -ErrorAction SilentlyContinue)) { $script:PromptFragmentCache = @{} } $PromptsDir = Join-Path $script:ModuleRoot 'Prompts' $FragmentMatches = [regex]::Matches($Text, '\{\{([A-Za-z0-9_-]+)\}\}') $FragmentReplacements = @{} foreach ($M in $FragmentMatches) { $FragName = $M.Groups[1].Value if ($FragmentReplacements.ContainsKey($FragName)) { continue } # Caller-supplied wins (redundant guard — the replacement above already ran, but # this documents intent explicitly) if ($Replacements.ContainsKey($FragName)) { continue } if (-not $script:PromptFragmentCache.ContainsKey($FragName)) { $FragPath = Join-Path $PromptsDir "$FragName.fragment.prompt" if (Test-Path $FragPath) { $script:PromptFragmentCache[$FragName] = (Get-Content -Path $FragPath -Raw -Encoding UTF8).TrimEnd() } else { # Cache a $null sentinel so we don't stat the disk again for missing fragments $script:PromptFragmentCache[$FragName] = $null } } $FragContent = $script:PromptFragmentCache[$FragName] if ($null -ne $FragContent) { $FragmentReplacements[$FragName] = $FragContent } } foreach ($FragName in $FragmentReplacements.Keys) { $Text = $Text -replace [regex]::Escape("{{$FragName}}"), $FragmentReplacements[$FragName] } # Warn if any placeholders remain unresolved (unless caller expects to substitute later) if (-not $AllowUnresolved -and $Text -match '\{\{[A-Za-z0-9_-]+\}\}') { $Remaining = [regex]::Matches($Text, '\{\{[A-Za-z0-9_-]+\}\}') | ForEach-Object { $_.Value } | Select-Object -Unique Write-Warning "Unresolved placeholders in prompt '$Name': $($Remaining -join ', '). These will appear as literal text in the AI prompt." } return $Text } |