src/plugins/PluginUtilities.ps1
# # Copyright (c), Adam Edwards # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # function GetClassDefinition( [string] $pluginName, [string] $description, [System.Collections.Generic.Dictionary[string,Modulus.ChatGPS.Plugins.PowerShellPluginFunction]] $scriptTable ) { $renderedMethods = RenderClassMethods $scriptTable $classDefinition = RenderClassDefinition $pluginName $description $renderedMethods $classDefinition } function RenderClassDefinition( [string] $pluginName, [string] $description, [string] $renderedMethods ) { $invokeScript = GetInvokeScript $pluginTemplate = @' [System.ComponentModel.Description("{2}")] class {0} : Modulus.ChatGPS.Plugins.PowerShellNativePluginBase {{ {0}([System.Collections.Generic.Dictionary[string,Modulus.ChatGPS.Plugins.PowerShellPluginFunction]] $scriptBlocks) : base("{0}", "{2}", $scriptBlocks) {{ }} {1} [string] $InvokeBlock = {{ {3} }} }} '@ $pluginTemplate -f $pluginName, $renderedMethods, $description, $invokeScript } function RenderClassMethods( [System.Collections.Generic.Dictionary[string,Modulus.ChatGPS.Plugins.PowerShellPluginFunction]] $methodTable ) { $rendered = foreach ( $methodName in $methodTable.Keys ) { RenderMethod $methodName $methodTable[$methodName] } $rendered -join "`n`n" } function RenderMethod( [string] $pluginMethodName, [Modulus.ChatGPS.Plugins.PowerShellPluginFunction] $scriptBlock ) { $targetOutputDescription = if ( $scriptBlock.outputDescription ) { $scriptBlock.outputDescription } else { "" } $targetDescription = if ( $scriptBlock.description ) { $scriptBlock.description } else { "This method invokes PowerShell code to perform its function." } $targetPluginMethodName = if ( $pluginMethodName ) { $pluginMethodName } else { $scriptBlock.name } $methodParameterList = RenderMethodParameterList $scriptBlock.GetParameterTable() RenderResolvedMethod $scriptBlock.Name $targetPluginMethodName $targetDescription $scriptBlock.OutputType $targetOutputDescription $methodParameterList $methodTable[$methodName].ScriptBlock } function RenderResolvedMethod( [string] $nativeMethodName, [string] $pluginMethodName, [string] $description, [string] $outputType = 'System.String', [string] $outputDescription = $null, [string] $methodParameterList = $null, [string] $methodBlock ) { $descriptionAttributeText ="This PowerShell function's functionality can be described as follows: $($description);The output it returns can be described as follows: $($outputDescription)" -replace "'", "''" $descriptionAttribute = "[System.ComponentModel.Description('$descriptionAttributeText')]" $kernelAttribute = "[Microsoft.SemanticKernel.KernelFunctionAttribute('$pluginMethodName')]" $resolvedMethod = @" $descriptionAttribute $kernelAttribute [$outputType] $nativeMethodName( $methodParameterList ) { `$parameterList= [System.Collections.Generic.Dictionary[string,object]]::new() foreach ( `$parameterName in `$PSBoundParameters.Keys ) { `$parameterList.Add(`$parameterName, `$PSBoundParameters[`$parameterName]) } `$scriptBlock = `$this.scriptBlocks['$nativeMethodName'] `$result = try { new-item -Path function:Invoke-PowerShellScript -Value `$this.InvokeBlock | out-null Invoke-PowerShellScript `$scriptBlock.ScriptBlock `$parameterList } catch { throw } return `$result } "@ $resolvedMethod } function RenderMethodParameterList( [System.Collections.Generic.Dictionary[string,string]] $parameterTable ) { if ( $null -eq $parameterTable ) { throw [ArgumentException]::new("Script method parameter table may be empty but can never be set to a null reference."); } $parameterList = foreach ( $parameterName in $parameterTable.Keys ) { "[$($parameterTable[$parameterName])] $parameterName" } $parameterList -join ', ' } function Invoke-PowerShellScript { param( [string] $script, [System.Collections.Generic.Dictionary[string,object]] $parameters, [int] $timeoutMs = 3600000, [bool] $jsonOutput = $false ) [ScriptBlock]::Create($script) | out-null $parameterJson = $parameters | convertto-json -compress -depth 5 $parameterJsonBytes = [System.Text.Encoding]::Unicode.GetBytes($parameterJson) $parameterJsonEncoded = [Convert]::ToBase64String($parameterJsonBytes) $jsonOutputConverter = $jsonOutput ? ' | convertto-json -compress -depth 5' : '' $commandTemplate = 'function prompt {{}};$parameterJsonEncoded = ''{0}''; $parameterJsonBytes = [Convert]::FromBase64String($parameterJsonEncoded); $parameterJson = [System.Text.Encoding]::Unicode.GetString($parameterJsonBytes); $parameters = @{{}}; $parameterObject = $parameterJson | convertfrom-json ; $parameterObject | get-member -membertype noteproperty | foreach {{ $parameters.add($_.name, $parameterObject.$($_.name)) }}; $commandBlock = {{ {1} }}; $result = (& $commandBlock @parameters) {2}; $finalResult = $result | out-string; $bytes = [System.Text.Encoding]::Unicode.GetBytes($finalResult);[Convert]::ToBase64String($bytes);exit' $shellCommand = $commandTemplate -f $parameterJsonEncoded, $script, $jsonOutputConverter $shellCommandBytes = [System.Text.Encoding]::Unicode.GetBytes($shellCommand) $encodedShellCommand = [Convert]::ToBase64String($shellCommandBytes) $shellInput = "set-strictmode -version 2;`$decodedCommandBytes = [Convert]::FromBase64String('$encodedShellCommand'); `$decodedCommand = [System.Text.Encoding]::Unicode.GetString(`$decodedCommandBytes); `$decodedCommand | invoke-expression" $shellOutput = $shellInput | pwsh -noprofile -noninteractive -nologo -File '-' $resultLine = ($shellOutput | select -last 1) $decodedBytes = [Convert]::FromBase64String($resultLine) [System.Text.Encoding]::Unicode.GetString($decodedBytes) } function GetInvokeScript { (get-command Invoke-PowerShellScript).ScriptBlock.ToString() } function GetGenerationScriptLocation { "$psscriptroot/GeneratePlugin.ps1" } function GetScriptBlockParameterInfo([ScriptBlock] $scriptBlock) { $result = [System.Collections.Generic.Dictionary[string,string]]::new() if ( $scriptBlock.ast.ParamBlock -and ( $scriptBlock.ast.ParamBlock | get-member parameters -erroraction ignore ) ) { foreach ( $parameter in $scriptBlock.ast.ParamBlock.parameters ) { $result.Add($parameter.name, $parameter.StaticType) } } $result } function NewPowerShellPluginFunction([string] $methodName, [ScriptBlock] $scriptBlock, [string] $description, [string] $OutputType = 'System.String', [string] $outputDescription = $null) { $parameterInfo = GetScriptBlockParameterInfo $scriptBlock [Modulus.ChatGPS.Plugins.PowerShellPluginFunction]::new($methodName, $scriptBlock, $parameterInfo, $description, $OutputType, $outputDescription) } |