Private/AI/Invoke-TechAgent.PromptAndLogging.ps1
|
function Resolve-TTAgentExecutionMode { [CmdletBinding()] param( [string]$ModeFromParam, $ConfigObject, [bool]$ParamWasBound ) if ($ParamWasBound -and -not [string]::IsNullOrWhiteSpace($ModeFromParam)) { return $ModeFromParam.Trim().ToLowerInvariant() } $configMode = $null if ($null -ne $ConfigObject) { $modeProperty = $ConfigObject.PSObject.Properties['executionMode'] if ($null -ne $modeProperty -and -not [string]::IsNullOrWhiteSpace([string]$modeProperty.Value)) { $configMode = [string]$modeProperty.Value } } if ([string]::IsNullOrWhiteSpace($configMode)) { return 'chat' } switch ($configMode.Trim().ToLowerInvariant()) { 'plan' { return 'plan' } 'analyze' { return 'analyze' } 'chat' { return 'chat' } default { return 'chat' } } } function Resolve-TTAgentOutputContract { [CmdletBinding()] param( [string]$ContractFromParam, $ConfigObject, [bool]$ParamWasBound ) if ($ParamWasBound -and -not [string]::IsNullOrWhiteSpace($ContractFromParam)) { return $ContractFromParam.Trim().ToLowerInvariant() } $configContract = $null if ($null -ne $ConfigObject) { $contractProperty = $ConfigObject.PSObject.Properties['outputContract'] if ($null -ne $contractProperty -and -not [string]::IsNullOrWhiteSpace([string]$contractProperty.Value)) { $configContract = [string]$contractProperty.Value } } if ([string]::IsNullOrWhiteSpace($configContract)) { return 'markdown' } switch ($configContract.Trim().ToLowerInvariant()) { 'plain-text' { return 'plain-text' } 'json' { return 'json' } default { return 'markdown' } } } function Resolve-TTAgentQualityProfile { [CmdletBinding()] param( [string]$ProfileFromParam, $ConfigObject, [bool]$ParamWasBound ) if ($ParamWasBound -and -not [string]::IsNullOrWhiteSpace($ProfileFromParam)) { return $ProfileFromParam.Trim().ToLowerInvariant() } $configProfile = $null if ($null -ne $ConfigObject) { $profileProperty = $ConfigObject.PSObject.Properties['qualityProfile'] if ($null -ne $profileProperty -and -not [string]::IsNullOrWhiteSpace([string]$profileProperty.Value)) { $configProfile = [string]$profileProperty.Value } } if ([string]::IsNullOrWhiteSpace($configProfile)) { return 'balanced' } switch ($configProfile.Trim().ToLowerInvariant()) { 'precise' { return 'precise' } 'creative' { return 'creative' } default { return 'balanced' } } } function Invoke-TTAgentPromptPreflight { [CmdletBinding()] param( [string]$PromptText, [string]$Mode ) $score = 0 $warnings = [System.Collections.Generic.List[string]]::new() $critical = [System.Collections.Generic.List[string]]::new() if ([string]::IsNullOrWhiteSpace($PromptText)) { $critical.Add('Prompt is empty.') return [ordered]@{ Score = 0; Warnings = @($warnings); Critical = @($critical) } } $normalizedPrompt = $PromptText.Trim() $hasTaskVerb = [regex]::IsMatch( $normalizedPrompt, '(?i)\b(create|write|update|edit|modify|fix|analy[sz]e|review|refactor|investigate|summari[sz]e|plan|implement|find|fetch|get|retrieve|collect|report|disable|enable|reset|remove|delete|provision|deprovision|offboard)\b') if ($hasTaskVerb) { $score += 20 } else { $warnings.Add('Missing clear task verb (for example: update, analyze, fix, plan).') } $hasPathOrSystemTarget = [regex]::IsMatch( $normalizedPrompt, '(?i)([A-Za-z]:\\[^\r\n]+|\b(file|function|class|module|script|command|service|endpoint|api|workflow|active\s*directory|ad\b|account|user|computer|group|ou|organizational\s+unit|exchange\s*online|entra|azure\s*ad)\b)') $hasWebTarget = [regex]::IsMatch( $normalizedPrompt, '(?i)(https?://\S+|\b(url|uri|website|web\s*site|webpage|site|domain|host|weather\.gov)\b)') $isWeatherIntent = [regex]::IsMatch( $normalizedPrompt, '(?i)\b(weather|forecast|temperature|precipitation|wind|noaa)\b') $hasWeatherLocationTarget = [regex]::IsMatch( $normalizedPrompt, '(?i)((?<!\d)\d{5}(?:-\d{4})?(?!\d)|\b[A-Za-z][A-Za-z\-''\.\s]+,\s*[A-Za-z][A-Za-z\-''\.\s]+\b)') $hasConcreteTarget = $hasPathOrSystemTarget -or $hasWebTarget -or ($isWeatherIntent -and $hasWeatherLocationTarget) if ($hasConcreteTarget) { $score += 20 } else { $warnings.Add('Missing concrete target (file, function, module, system, URL, website, or path).') } $hasExpectedOutcome = [regex]::IsMatch( $normalizedPrompt, '(?i)\b(expected|outcome|output|result|return|produce|final|success\b|done\b)') if ($hasExpectedOutcome) { $score += 20 } else { $warnings.Add('Missing expected outcome details (what successful output should look like).') } $hasConstraints = [regex]::IsMatch( $normalizedPrompt, '(?i)\b(must|should|do not|don''t|avoid|constraint|format|style|security|strict|exact path|preserve|no edits)') if ($hasConstraints) { $score += 20 } else { $warnings.Add('Missing explicit constraints or preferences (style, safety, formatting, scope).') } if ($normalizedPrompt.Length -ge 80) { $score += 20 } else { $warnings.Add('Prompt is short; add context (environment, errors, affected behavior).') } if ($normalizedPrompt.Length -lt 25) { $critical.Add('Prompt is too short for reliable execution.') } if ($Mode -eq 'execute' -and -not $hasConcreteTarget) { $critical.Add('Execution mode requires a concrete target to avoid ambiguous changes.') } if ($Mode -eq 'execute' -and -not $hasExpectedOutcome) { $critical.Add('Execution mode requires expected outcome details.') } return [ordered]@{ Score = $score Warnings = @($warnings) Critical = @($critical) } } function New-TTAgentAutoPromptHint { [CmdletBinding()] param( [string]$PromptText, [string]$Mode, [string]$OutputContract, [int]$WarningCount, [int]$CriticalCount ) if ([string]::IsNullOrWhiteSpace($PromptText)) { return $null } if ($WarningCount -le 0 -and $CriticalCount -le 0) { return $null } $normalized = $PromptText.Trim() $taskVerbMatch = [regex]::Match( $normalized, '(?i)\b(create|write|update|edit|modify|fix|analy[sz]e|review|refactor|investigate|summari[sz]e|plan|implement|find|fetch|get|retrieve|collect|report)\b') $taskVerb = if ($taskVerbMatch.Success) { $taskVerbMatch.Value } elseif ([regex]::IsMatch($normalized, '(?i)\b(weather|forecast)\b')) { 'fetch' } else { 'analyze' } $urlMatch = [regex]::Match($normalized, '(?i)https?://\S+') $pathMatch = [regex]::Match($normalized, '(?i)[A-Za-z]:\\[^\s"''`\r\n]+') $locationMatch = [regex]::Match( $normalized, '(?i)\b(?:for|in|at|near)\s+(?<location>[A-Za-z][A-Za-z0-9''.,/-]*(?:\s+[A-Za-z0-9''.,/-]+){0,6})(?=\s+(?:from|on|using|with|and|return|output|show|summarize|today|tomorrow|next)\b|$)' ) $targetHint = if ($urlMatch.Success) { "the data from $($urlMatch.Value)" } elseif ($pathMatch.Success) { "the file at $($pathMatch.Value)" } elseif ($locationMatch.Success) { $locationName = $locationMatch.Groups['location'].Value.Trim().TrimEnd(',', '.') "the weather forecast for $locationName using the official NOAA API (api.weather.gov)" } elseif ([regex]::IsMatch($normalized, '(?i)\bweather\b')) { 'the weather forecast for the specified location using the official NOAA API (api.weather.gov)' } else { 'the specific file, service, module, or URL' } $sourceHint = if ($urlMatch.Success) { "Use $($urlMatch.Value) as the primary source." } elseif ([regex]::IsMatch($normalized, '(?i)\b(website|web\s*site|webpage|url|uri|domain|host|web)\b')) { 'Use the specified website or URL as the primary source.' } else { 'Use only the minimum required tools and keep the scope bounded.' } $contractHint = switch ($OutputContract) { 'json' { 'Return valid JSON object or array text only.' } 'plain-text' { 'Return plain text only (no markdown).' } default { 'Return the final answer in markdown.' } } $modeConstraint = if ($Mode -eq 'execute') { 'Constraints: execute only bounded steps and avoid unrelated changes.' } else { "Constraints: respect mode '$Mode'." } return ( '{0} {1}. {2} {3} {4}' -f $taskVerb, $targetHint, $sourceHint, $contractHint, $modeConstraint ) } function Test-TTAgentPostflightGoal { [CmdletBinding()] param( [string]$PromptText, [string]$ResponseText, [int]$PreflightScore ) if ([string]::IsNullOrWhiteSpace($PromptText)) { return @{ Achieved = $false; Reason = 'Prompt was empty.' } } if ([string]::IsNullOrWhiteSpace($ResponseText)) { return @{ Achieved = $false; Reason = 'No response text was produced.' } } $promptLower = $PromptText.Trim().ToLowerInvariant() $responseLower = $ResponseText.Trim().ToLowerInvariant() $hasCompletionSignal = ( [regex]::IsMatch($responseLower, '(?im)^\s*##\s*result\b') -or [regex]::IsMatch($responseLower, '(?im)^\s*(created|updated|wrote)\s+file\s*:') -or [regex]::IsMatch($responseLower, '(?is)```(?:powershell|pwsh)?\s*.+?```') -or $responseLower.Contains('script contents') -or $responseLower.Contains('successfully') ) $hasUncertaintySignal = [regex]::IsMatch( $responseLower, '(?i)(\bi need more (?:detail|information)\b|\bclarification needed\b|\bnot enough information\b|\bi (?:am|''m) unable to\b|\bi could not\b|\bi cannot\b|\bi can''t\b|\bplease provide\b|\brequire (?:more|additional) (?:details|information)\b)' ) if ($hasUncertaintySignal -and -not $hasCompletionSignal) { return @{ Achieved = $false; Reason = 'The response requested more clarification or reported inability to complete the task.' } } if ($PreflightScore -lt 60 -and $ResponseText.Trim().Length -lt 80) { return @{ Achieved = $false; Reason = 'The response was too short to clearly satisfy the prompt.' } } if ($promptLower.Contains('weather') -or $promptLower.Contains('forecast')) { $weatherTerms = @('weather', 'forecast', 'temperature', 'humidity', 'wind', 'rain', 'conditions', 'precipitation') $hasWeatherResponseSignal = $false foreach ($term in $weatherTerms) { if ($responseLower.Contains($term)) { $hasWeatherResponseSignal = $true break } } if (-not $hasWeatherResponseSignal) { return @{ Achieved = $false; Reason = 'The response did not include weather or forecast information.' } } } $isPowerShellScriptPrompt = ( ($promptLower.Contains('powershell') -or $promptLower.Contains('.ps1')) -and $promptLower.Contains('script') ) if ($isPowerShellScriptPrompt) { $responseFenceCount = [regex]::Matches($ResponseText, '```').Count if (($responseFenceCount % 2) -ne 0) { return @{ Achieved = $false; Reason = 'The response appears to contain an unclosed markdown code fence.' } } $codeBlockMatch = [regex]::Match($ResponseText, '(?is)```(?:powershell|pwsh)?\s*(?<code>.*?)```') $candidateCode = if ($codeBlockMatch.Success) { $codeBlockMatch.Groups['code'].Value } else { $ResponseText } if (($promptLower.Contains('stand alone') -or $promptLower.Contains('standalone') -or $promptLower.Contains('no external helper')) -and $candidateCode -match '(?im)^\s*write-comment\b') { return @{ Achieved = $false; Reason = 'The script references external helper commands (Write-Comment), which violates standalone/no-helper intent.' } } if ($promptLower.Contains('syntactically correct')) { $openBraceCount = [regex]::Matches($candidateCode, '\{').Count $closeBraceCount = [regex]::Matches($candidateCode, '\}').Count if ($openBraceCount -ne $closeBraceCount) { return @{ Achieved = $false; Reason = 'The script output appears structurally incomplete (mismatched braces).' } } $openParenCount = [regex]::Matches($candidateCode, '\(').Count $closeParenCount = [regex]::Matches($candidateCode, '\)').Count if ($openParenCount -ne $closeParenCount) { return @{ Achieved = $false; Reason = 'The script output appears structurally incomplete (mismatched parentheses).' } } } } return @{ Achieved = $true; Reason = '' } } function Expand-TTAgentOptionZipFollowUpPrompt { [CmdletBinding()] param( [string]$PromptText, [string]$Mode ) if ([string]::IsNullOrWhiteSpace($PromptText)) { return $PromptText } if ($Mode -ne 'execute') { return $PromptText } $trimmed = $PromptText.Trim() $zipOnlyMatch = [regex]::Match( $trimmed, '(?i)^(?:option\s*)?b\s*[:\-]?\s*(?<zip>\d{5}(?:-\d{4})?)\s*$' ) if (-not $zipOnlyMatch.Success) { return $PromptText } $zip = $zipOnlyMatch.Groups['zip'].Value return ( "Continue the pending weather forecast task using alternate location ZIP code $zip. " + "Call GET-NOAA-FORECAST with zipCode='$zip' and include Friday through Sunday night periods. " + 'Return the final answer in markdown. Constraints: execute only bounded steps and avoid unrelated changes.' ) } function Resolve-TTAgentExpectedOutputPath { [CmdletBinding()] param( [string]$PromptText ) $trimDetectedPath = { param([string]$CandidatePath) if ([string]::IsNullOrWhiteSpace($CandidatePath)) { return $null } $trimmed = $CandidatePath.Trim().TrimEnd('.', ',', ';', ':', ')', ']', '}') if ([string]::IsNullOrWhiteSpace($trimmed)) { return $null } return $trimmed } $promptIndicatesWriteIntent = { param([string]$Text) if ([string]::IsNullOrWhiteSpace($Text)) { return $false } return [regex]::IsMatch( $Text, '(?is)\b(write|rewrite|update|edit|modify|insert|create)\b|\buse\s+write(?:-|=|\s*)file\b|\bwrite(?:-|=|\s*)file\b') } if ([string]::IsNullOrWhiteSpace($PromptText)) { return $null } $directPathMatches = [regex]::Matches( $PromptText, '(?i)(?<path>[A-Za-z]:\\[^\s"''`\r\n]*?\.help\.txt)\b') if ($directPathMatches.Count -gt 0) { $directPath = & $trimDetectedPath -CandidatePath $directPathMatches[$directPathMatches.Count - 1].Groups['path'].Value if (-not [string]::IsNullOrWhiteSpace($directPath)) { return $directPath } } if (& $promptIndicatesWriteIntent -Text $PromptText) { $genericPathMatches = [regex]::Matches( $PromptText, '(?i)(?<path>[A-Za-z]:\\[^"''`\r\n]*\.[A-Za-z0-9]{1,16})(?=\s|$|[)\],;:])') if ($genericPathMatches.Count -gt 0) { for ($i = $genericPathMatches.Count - 1; $i -ge 0; $i--) { $candidate = & $trimDetectedPath -CandidatePath $genericPathMatches[$i].Groups['path'].Value if ([string]::IsNullOrWhiteSpace($candidate)) { continue } if ($candidate.EndsWith('\\', [System.StringComparison]::Ordinal)) { continue } return $candidate } } } $fileNameMatch = [regex]::Match( $PromptText, '(?is)\b(?:name\s+(?:it|the\s+file|the\s+script\s+file|script\s+file)|file\s+should\s+be\s+named|named)\s+["'']?(?<name>[^\s"''`\\/:*?<>|]+?\.[A-Za-z0-9]{1,16})\b') if (-not $fileNameMatch.Success) { return $null } $fileName = $fileNameMatch.Groups['name'].Value.Trim() if ([string]::IsNullOrWhiteSpace($fileName)) { return $null } $pathMatches = [regex]::Matches($PromptText, '(?i)[A-Za-z]:\\[^\s"''`\r\n]+') if ($pathMatches.Count -eq 0) { return $null } $candidateDirs = @() foreach ($match in $pathMatches) { $candidatePath = [string]$match.Value if ([string]::IsNullOrWhiteSpace($candidatePath)) { continue } $candidatePath = $candidatePath.Trim().TrimEnd('.', ',', ';') if ($candidatePath -match '(?i)\.[A-Za-z0-9]{1,5}$') { continue } $candidateDirs += $candidatePath } if ($candidateDirs.Count -eq 0) { return $null } $targetDirectory = $candidateDirs | Where-Object { $_ -match '(?i)\\en-US$' } | Select-Object -Last 1 if ([string]::IsNullOrWhiteSpace($targetDirectory)) { $targetDirectory = $candidateDirs | Select-Object -Last 1 } if ([string]::IsNullOrWhiteSpace($targetDirectory)) { return $null } return (Join-Path -Path $targetDirectory -ChildPath $fileName) } function Write-TTAgentMarkdownLog { [CmdletBinding()] param( [string]$Path, [string]$Status, [string]$PromptText, [string]$ModelName, [int]$IterationLimit, [bool]$DestructiveAuthorized, [string]$SignedFilePolicyValue, [string]$AutoRetryOnRecursionMode, [string]$ExecutionMode, [string]$OutputContract, [string]$QualityProfile, [string]$PromptSource, [int]$PreflightScore, [string[]]$PreflightWarnings, [string[]]$PreflightCritical, [string]$ExpectedOutputPath, [string]$StdOut, [string]$StdErr, [string]$ErrorText, [string]$RecoveryReason, [bool]$PostflightAchieved, [string]$PostflightReason, [int]$ResponseLength, [bool]$KnownFailureDetected, [bool]$ExpectedOutputExists, [int]$ExitCode, [string]$TranscriptFile, [DateTime]$StartedUtc, [DateTime]$CompletedUtc ) if ([string]::IsNullOrWhiteSpace($Path)) { return } $dir = Split-Path -Parent $Path if (-not [string]::IsNullOrWhiteSpace($dir)) { $null = New-Item -ItemType Directory -Path $dir -Force } $renderedOutput = if ([string]::IsNullOrWhiteSpace($StdOut)) { '(none)' } else { $StdOut.TrimEnd() } $rawError = if ([string]::IsNullOrWhiteSpace($StdErr)) { '(none)' } else { $StdErr.TrimEnd() } $rawException = if ([string]::IsNullOrWhiteSpace($ErrorText)) { '(none)' } else { $ErrorText.TrimEnd() } $rawRecoveryReason = if ([string]::IsNullOrWhiteSpace($RecoveryReason)) { '(none)' } else { $RecoveryReason.TrimEnd() } $rawPostflightReason = if ([string]::IsNullOrWhiteSpace($PostflightReason)) { '(none)' } else { $PostflightReason.TrimEnd() } $preflightWarnings = @($PreflightWarnings) $preflightCritical = @($PreflightCritical) $preflightWarningsText = if ($preflightWarnings.Count -gt 0) { ($preflightWarnings -join [Environment]::NewLine) } else { '(none)' } $preflightCriticalText = if ($preflightCritical.Count -gt 0) { ($preflightCritical -join [Environment]::NewLine) } else { '(none)' } $expectedOutputPathText = if ([string]::IsNullOrWhiteSpace($ExpectedOutputPath)) { '(none)' } else { $ExpectedOutputPath } $postflightStatus = if ($PostflightAchieved) { 'Achieved' } else { 'NotAchieved' } $expectedOutputExistsText = if ([string]::IsNullOrWhiteSpace($ExpectedOutputPath)) { '(n/a)' } else { [string]$ExpectedOutputExists } $lines = @( '# Tech Agent Run' '' ('- Status: {0}' -f $Status) ('- StartedUtc: {0}' -f $StartedUtc.ToString('o')) ('- CompletedUtc: {0}' -f $CompletedUtc.ToString('o')) ('- Model: {0}' -f $(if ([string]::IsNullOrWhiteSpace($ModelName)) { '(default)' } else { $ModelName })) ('- MaxIterations: {0}' -f $IterationLimit) ('- ConfirmDestructive: {0}' -f $DestructiveAuthorized) ('- SignedFilePolicy: {0}' -f $(if ([string]::IsNullOrWhiteSpace($SignedFilePolicyValue)) { '(default)' } else { $SignedFilePolicyValue })) ('- AutoRetryOnRecursion: {0}' -f $AutoRetryOnRecursionMode) ('- ExitCode: {0}' -f $ExitCode) ('- TranscriptPath: {0}' -f $(if ([string]::IsNullOrWhiteSpace($TranscriptFile)) { '(none)' } else { $TranscriptFile })) '' '## Prompt' '' '```text' $PromptText '```' '' '## Preflight' '' '~~~~text' ('Mode: {0}' -f $ExecutionMode) ('OutputContract: {0}' -f $OutputContract) ('QualityProfile: {0}' -f $QualityProfile) ('PromptSource: {0}' -f $PromptSource) ('Score: {0}/100' -f $PreflightScore) ('WarningsCount: {0}' -f $preflightWarnings.Count) 'Warnings:' $preflightWarningsText ('CriticalCount: {0}' -f $preflightCritical.Count) 'Critical:' $preflightCriticalText ('ExpectedOutputPath: {0}' -f $expectedOutputPathText) '~~~~' '' '## Output' '' $renderedOutput '' '## Error Output' '' '~~~~text' $rawError '~~~~' '' '## Exception' '' '~~~~text' $rawException '~~~~' '' '## Postflight' '' '~~~~text' ('Status: {0}' -f $postflightStatus) ('ResponseLengthChars: {0}' -f $ResponseLength) ('KnownFailurePrefixDetected: {0}' -f $KnownFailureDetected) ('ExpectedOutputExists: {0}' -f $expectedOutputExistsText) 'Reason:' $rawPostflightReason '~~~~' '' '## Recovery' '' '~~~~text' $rawRecoveryReason '~~~~' ) Set-Content -Path $Path -Value ($lines -join [Environment]::NewLine) -Encoding utf8BOM } # SIG # Begin signature block # MIIfAgYJKoZIhvcNAQcCoIIe8zCCHu8CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC1wp7PxLgt3o0D # keZWPLtoy3gCTsNOpe2feqa3fzsH6qCCGEowggUMMIIC9KADAgECAhAR+U4xG7FH # qkyqS9NIt7l5MA0GCSqGSIb3DQEBCwUAMB4xHDAaBgNVBAMME1ZBRFRFSyBDb2Rl # IFNpZ25pbmcwHhcNMjUxMjE5MTk1NDIxWhcNMjYxMjE5MjAwNDIxWjAeMRwwGgYD # VQQDDBNWQURURUsgQ29kZSBTaWduaW5nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A # MIICCgKCAgEA3pzzZIUEY92GDldMWuzvbLeivHOuMupgpwbezoG5v90KeuN03S5d # nM/eom/PcIz08+fGZF04ueuCS6b48q1qFnylwg/C/TkcVRo0WFcKoFGT8yGxdfXi # caHtapZfbSRh73r7qR7w0CioVveNBVgfMsTgE0WKcuwxemvIe/ptmkfzwAiw/IAC # Ib0E0BjiX4PySbwWy/QKy/qMXYY19xpRItVTKNBtXzADUtzPzUcFqJU83vM2gZFs # Or0MhPvM7xEVkOWZFBAWAubbMCJ3rmwyVv9keVDJChhCeLSz2XR11VGDOEA2OO90 # Y30WfY9aOI2sCfQcKMeJ9ypkHl0xORdhUwZ3Wz48d3yJDXGkduPm2vl05RvnA4T6 # 29HVZTmMdvP2475/8nLxCte9IB7TobAOGl6P1NuwplAMKM8qyZh62Br23vcx1fXZ # TJlKCxBFx1nTa6VlIJk+UbM4ZPm954peB/fIqEacm8LkZ0cPwmLE5ckW7hfK4Trs # o+RaudU1sKeA+FvpOWgsPccVRWcEYyGkwbyTB3xrIBXA+YckbANZ0XL7fv7x29hn # gXbZipGu3DnTISiFB43V4MhNDKZYfbWdxze0SwLe8KzIaKnwlwRgvXDMwXgk99Mi # EbYa3DvA/5ZWikLW9PxBFD7Vdr8ZiG/tRC9I2Y6fnb+PVoZKc/2xsW0CAwEAAaNG # MEQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQW # BBRfYLVE8caSc990rnrIHUjoB7X/KjANBgkqhkiG9w0BAQsFAAOCAgEAiGB2Wmk3 # QBtd1LcynmxHzmu+X4Y5DIpMMNC2ahsqZtPUVcGqmb5IFbVuAdQphL6PSrDjaAR8 # 1S8uTfUnMa119LmIb7di7TlH2F5K3530h5x8JMj5EErl0xmZyJtSg7BTiBA/UrMz # 6WCf8wWIG2/4NbV6aAyFwIojfAcKoO8ng44Dal/oLGzLO3FDE5AWhcda/FbqVjSJ # 1zMfiW8odd4LgbmoyEI024KkwOkkPyJQ2Ugn6HMqlFLazAmBBpyS7wxdaAGrl18n # 6bS7QuAwCd9hitdMMitG8YyWL6tKeRSbuTP5E+ASbu0Ga8/fxRO5ZSQhO6/5ro1j # PGe1/Kr49Uyuf9VSCZdNIZAyjjeVAoxmV0IfxQLKz6VOG0kGDYkFGskvllIpQbQg # WLuPLJxoskJsoJllk7MjZJwrpr08+3FQnLkRuisjDOc3l4VxFUsUe4fnJhMUONXT # Sk7vdspgxirNbLmXU4yYWdsizz3nMUR0zebUW29A+HYme16hzrMPOeyoQjy4I5XX # 3wXAFdworfPEr/ozDFrdXKgbLwZopymKbBwv6wtT7+1zVhJXr+jGVQ1TWr6R+8ea # tIOFnY7HqGaxe5XB7HzOwJKdj+bpHAfXft1vUoiKr16VajLigcYCG8MdwC3sngO3 # JDyv2V+YMfsYBmItMGBwvizlQ6557NbK95EwggWNMIIEdaADAgECAhAOmxiO+dAt # 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa # Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E # MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy # unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF # xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 # 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB # MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR # WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 # nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB # YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S # UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x # q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB # NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP # TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC # AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc # Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov # Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy # oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW # juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF # mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z # twGpn1eqXijiuZQwgga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqG # SIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkx # CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4 # RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYg # MjAyNSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphB # cr48RsAcrHXbo0ZodLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6p # vF4uGjwjqNjfEvUi6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHe # HYNnQxqXmRinvuNgxVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEd # gkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjU # jsZvkgFkriK9tUKJm/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bR # VFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeS # LsJygoLPp66bkDX1ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIV # NSaz7BX8VtYGqLt9MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL # 6s36czwzsucuoKs7Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2Zd # SoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFU # eEY0qVjPKOWug/G6X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/ # BAgwBgEB/wIBADAdBgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0j # BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud # JQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0 # cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0 # cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E # PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz # dGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEw # DQYJKoZIhvcNAQELBQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/ # T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQ # E7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9r # EVKChHyfpzee5kH0F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y # 1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gx # dEkMx1NKU4uHQcKfZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3t # y9qIijanrUR3anzEwlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcy # tL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEB # YTptMSbhdhGQDpOXgpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud # /v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiS # uEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZP # ubdcMIIG7TCCBNWgAwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsF # ADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNV # BAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hB # MjU2IDIwMjUgQ0ExMB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVowYzEL # MAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJE # aWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUg # MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMr # V7pvUf+GcAoB38o3zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8 # dE2/pPvOx/Vj8TchTySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qome7M # rxVyfQO9sMx6ZAWjFDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ//nBZ # ZREr4h/GI6Dxb2UoyrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouTMYFO # nHoRh6+86Ltc5zjPKHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8DD+n # igNJFmt6LAHvH3KSuNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnpJeIt # K/DhKbPxTTuGoX7wJNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP51ho1 # zBp+xUIZkpSFA8vWdoUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49kPmk # 8iyyizNDIXj//cOgrY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5PWPsW # eupWs7NpChUk555K096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7YufAk # prxMiXAJQ1XCmnCfgPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0G # A1UdDgQWBBTkO/zyMe39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQG # fHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYB # BQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz # cC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2lj # ZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEy # NTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hB # MjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcB # MA0GCSqGSIb3DQEBCwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP2zWL # pQq1b4URGnwWBdEZD9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgj # g8K8elC4+oWCqnU/ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3Q # YIUP2S3HQvHG1FDu+WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5 # bdrPbF6MRYs03h4obEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDFkxUG # tMTaiLR9wjxUxu2hECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+zJNE # suEB7O7/cuvTQasnM9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6U # Arb+BOVAkg2oOvol/DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxlRcGG # 0LIhp6GvReQGgMgYxQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWV # FjF7mcr4C34Mj3ocCVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJgbaP5 # t2nGj/ULLi49xTcBZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOCIUjs # arfNZzGCBg4wggYKAgEBMDIwHjEcMBoGA1UEAwwTVkFEVEVLIENvZGUgU2lnbmlu # ZwIQEflOMRuxR6pMqkvTSLe5eTANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3 # AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisG # AQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCCFBtx6Pcng # szxofkY8CPS53Pv/JNVjNjjnKMq2dlAXkTANBgkqhkiG9w0BAQEFAASCAgBTbvtw # w8Q8A1cJE5LZ7d96uYlbmY9gFByd4b/lLDZLucQa4uKSJZZ2tTkOV4DpBOHB01UM # b64K24FUalo2f/W3qHJJQ1u/FGzo8EWqwDWrT7iWaOn7+13ezHWM6Mn8F9GgAsxh # 0m6K7nB8pEOZJfOmbRrdUpr6SaRNguuXG24nJsT8453qgdUUUlax+n8UnVKhL5UE # rgr/opS9LnCjxDxbExAwYVJhaWbLMtipxZTUbUtDXpm1/yd5vES2HoMUQVXoMSZE # RS7VLib6NkIpLSnBiR5kU5uKlObKuSHnh7tmGpyl4qgxbm5KKqNOV3zAUTyJONf0 # QQqn0wlIlpNrDgBQwl5WDd+DrtD4bCjF+H7VTcc2JI1siucbZ4pcGGMyEuleYQbX # mdIH9hUEftMMH+aOBijDoQKB/4VFhxzzbOSxKQ1Hi8T0W1NTiAG93N6E19yEuztD # SIhw8KKRSdl1stB2H16P/yUaZpmsLsl+e+lJih/zASlqNP8BcneqTHr+Rt/IYQY/ # zPsRhmuEVQf90d4AsDA3e+D3D8giSAmEhK4JaOMYPOwY1lY6E8659WNb1TPTY6wo # /FDInURFTrampo+wAcUUp4oYJoAPC18bKiZtXK2O4LGHfDBS2223ygkWbSOgCF1K # HQuNK67ev/2yeExO30n6jUffxnRTFpA+W5OVcKGCAyYwggMiBgkqhkiG9w0BCQYx # ggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg # SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcg # UlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeVdGgwDQYJYIZI # AWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJ # BTEPFw0yNjA4MzAwMTIyMDhaMC8GCSqGSIb3DQEJBDEiBCBqg6kaltJZ3qHOKu8p # ni5Wo5OaiN5jbKQVVeSCeDDP9zANBgkqhkiG9w0BAQEFAASCAgB5jEFRuVMGWlOl # ibYu2r9kamlPB66tN8HxGDBZKv/a2+UY56WZ+sGBtUHjSnfVx4j1x2PmIyclZbYy # b1hoGq75cEUm/ZzimiGZeFJ20pYUKIIF7Gu3HYWu2yIa8zl7r3Ax/1aNzBLEZIV5 # JcAohMgKkeDFzgmIk34JXoIlRrPjdF2Cd0lQ3uCFSL3XDkEsGqUZfDwAwzMpvc0H # QCK0baYdYH/xXoniE9oai9wsunU7qpErDDI0M85lTpaACUKTU8RWZIpNUf2eac55 # sYaWs8l2VYGEvytQz4O7ygrSWD3ZKf37HDET4/EWybz5gEaMq9FvMtQJNSwji6Wb # E9cT7IgeceXWE/XCj1CsM7hvUSau62wztR9usFYJnxWXeynomSIe9E9smPTzIAG1 # BM9nCBXTcEfNYHwZS7SFj/6kREVuhPZ/7HSFjkAGlLel4eLf+3fRo8ct6T9fTeoT # ew3YgfBLp29Z4L3BztC8livsESLGQ17EXkXlrOaU5pNRPck9RBjrgH9jkAFojWDP # JOK31FbGwXK44zPO48DjupsvvkLyUha+THCHmQLnu2mBGqmLN9oBkkaQCqZL0Iam # OZ0dTwg4fu4WQesOtlvaA34AXKrLnTC86a21LYfrU+mWZJr0sQtfZv9u778g74vK # o79JXx2tpHoeQNAfWVt/LJF/8narrg== # SIG # End signature block |