src/services/session.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. $Sessions = [ordered] @{} $CurrentSession = $null $__ModuleVersionString = {}.Module.Version.ToString() $InstallAddOnsMessage = @' Possible missing dependencies detected. Invoke the Install-ChatAddOn command to install missing dependencies and then retry this operation. '@ function GetUserAgent { $osversion = [System.Environment]::OSVersion.version.tostring() $platform = 'Windows NT' $os = 'Windows NT' if ( $PSVersionTable.PSEdition -eq 'Core' ) { if ( ! $PSVersionTable.OS.contains('Windows') ) { $platform = $PSVersionTable.Platform if ( $PSVersionTable.OS.contains('Linux') ) { $os = 'Linux' } else { $os = [System.Environment]::OSVersion.Platform } } } $language = [System.Globalization.CultureInfo]::CurrentCulture.name 'Modulus-ChatGPS/{5} PowerShell/{4} ({0}; {1} {2}; {3})' -f $platform, $os, $osversion, $language, $PSVersionTable.PSversion, $__ModuleVersionString } function CreateSession { param( [parameter(mandatory=$true)] [Modulus.ChatGPS.Models.AiOptions] $Options, [parameter(mandatory=$true)] [string] $Prompt, [string] $AiProxyHostPath = $null, [string] $LogDirectory = $null, [validateset('Trace', 'Debug', 'Information', 'Warning', 'Error', 'Critical', 'None')] [string] $LogLevel = 'None', [switch] $SetCurrent, [validateset('None','Truncate', 'Summarize')] [string] $TokenStrategy = 'Truncate', [switch] $NoConnect, [int] $HistoryContextLimit = -1, [ScriptBlock] $SendBlock = $null, [ScriptBlock] $ReceiveBlock = $null, [string] $Name = $null, [string] $UserAgent, [switch] $NoSave, [switch] $Force, [HashTable] $Plugins, $BoundParameters ) $targetLogDirectory = if ( $LogDirectory ) { (Get-Item $LogDirectory).FullName } $context = @{ SendBlock = $SendBlock ReceiveBlock = $ReceiveBlock Options = $Options } $targetUserAgent = if ( $UserAgent ) { $UserAgent } else { GetUserAgent } $session = [Modulus.ChatGPS.ChatGPS]::CreateSession($Options, $AiProxyHostPath, $Prompt, $TokenStrategy, $targetLogDirectory, $LogLevel, $null, $HistoryContextLimit, $context, $Name, $targetUserAgent) TestSession $session $Options $NoConnect.IsPresent $sessionSettings = GetExplicitSessionSettingsFromSessionParameters $session $BoundParameters ConfigureSessionPlugins $session $Plugins AddSession $session $SetCurrent.IsPresent $NoSave.IsPresent $Force.IsPresent $sessionSettings $session } function TestSession($session, [Modulus.ChatGPS.Models.AiOptions] $originalAiOptions, [bool] $noConnect) { if ( $Session.IsRemote -and ! $noConnect ) { try { SendConnectionTestMessage $session } catch { $exceptionMessage = if ( $_.Exception.InnerException ) { $_.Exception.InnerException.Message } else { $_.Exception.Message } $apiEndpoint = $originalAiOptions.ApiEndpoint $apiEndpointAdvice = if ( $apiEndpoint ) { "Ensure that the remote API URI '$($apiEndpoint)' is accessible from this device." } else { "Ensure that you have network connectivity to the remote service hosting the model." } $signinAdvice = if ( $originalAiOptions.ApiKey ) { 'Also ensure that the specified API key is valid for the given model API URI.' } else { 'Also ensure that you have signed in using a valid identity that has been granted access to the given model API URI. ' + '(e.g. for Azure OpenAI models try signing out with Logout-AzAccount, then retry the command, or explicitly use ' + 'LoginAzAccount to sign in as the correct identity). You can also specify the AllowInteractiveSignin parameter with ' + 'with this command and retry if you do not have access to signin tools for the remote model; this may result in ' + 'multiple requests to re-authenticate.' } throw [ApplicationException]::new("Attempt to establish a test connection to the remote model failed.`n" + "$($apiEndpointAdvice)`n" + "$($signinAdvice)`nSpecify the NoConnect option to skip this test when invoking this command.`n" + "$($exceptionMessage)", $_.Exception) } } } function ConfigureSessionPlugins($session, [HashTable] $parametersByPlugin) { if ( $parametersByPlugin ) { foreach ( $pluginName in $parametersByPlugin.Keys ) { $parameterTable = $parametersByPlugin[$pluginName] $parameterInfo = GetPluginParameterInfo $pluginName $parameterTable $session.AddPlugin($pluginName, $parameterInfo) } } } function AddPluginToSession([Modulus.ChatGPS.Models.ChatSession] $session, [string] $pluginName, $parameterInfo) { WarnPluginCompatibility $session $pluginName $session.AddPlugin($pluginName, $parameterInfo) } function AddSession($session, [bool] $setCurrent = $false, [bool] $noSave = $false, [bool] $forceOnNameCollision, $sourceSettings = $null) { if ( ! $noSave ) { if ( $name ) { if ( $script:sessions.Count -gt 0 ) { $existing = $script:sessions.Values.Session | where Name -eq $name if ( $existing ) { if ( $forceOnNameCollision ) { RemoveSession $existing $true } else { throw [ArgumentException]::new("A session named '$name' already exists.") } } } } $sessionInfo = [PSCustomObject] @{ Session = $session SourceSettings = $sourceSettings } $script:sessions.Add($session.Id, $sessionInfo) } if ( $setCurrent ) { $script:CurrentSession = $session } } function RemoveSession($session, $allowRemoveCurrent) { $current = GetCurrentSession $isCurrentSession = $current -and ( $current.id -eq $session.id ) if ( $isCurrentSession -and ! $allowRemoveCurrent ) { throw [InvalidOperationException]::new("The session with identifier '$($session.Id)' may not be removed because it is the current active session.") } $script:sessions.Remove($session.Id) if ( $isCurrentSession ) { $script:CurrentSession = if ( $script:sessions.Count -gt 0 ) { $script:sessions.Values.Session | select-object -first 1 } } } function UpdateSession($session, $settingsInfo) { $script:sessions[$session.id].SourceSettings = $settingsInfo } function GetSessionSettingsInfo($session) { $script:sessions[$session.id] } function GetExplicitModelSettingsFromSessionsByName([string] $modelName) { $script:sessions.Values | where-object { if ( $_.SourceSettings -and $_.SourceSettings.ModelSettings ) { $_.SourceSettings.ModelSettings.name -eq $modelName } } | select-object -ExpandProperty SourceSettings | select-object -ExpandProperty ModelSettings } function GetCompatibleModelSettingsFromSessions($session) { $script:sessions.Values | foreach { if ( $_.SourceSettings -and $_.SourceSettings.Model ) { if ( $_.SourceSettings.ModelSettings.IsCompatible($session.AiOptions) ) { break } } } | select-object -ExpandProperty SourceSettings | select-object -ExpandProperty ModelSettings } function SetCurrentSession($session) { if ( $script:sessions[$session.id] ) { $script:CurrentSession = $session } else { throw [InvalidOperationException]::new("The specified session id='$($session.id)' name='$($session.name)' was not found in the current list of valid sessions.") } } function GetCurrentSession([bool] $failIfNotFound = $false) { if ( $failIfNotFound -and (! $script:CurrentSession ) ) { throw "No current session exists -- use Connect-ChatSession to create a session" } $script:CurrentSession } function GetCurrentSessionId([bool] $failIfNotFound = $false) { $currentSession = GetCurrentSession $false if ( $currentSession ) { $currentSession.Id } } function GetChatSessions { if ( $script:sessions.Count -gt 0 ) { $script:sessions.Values.Session } } function GetTargetSession($userSpecifiedSession, [bool] $failIfNotFound = $false) { $targetSession = if ( $userSpecifiedSession ) { $userSpecifiedSession } else { GetCurrentSession } if ( $failIfNotFound -and (! $targetSession ) ) { throw "No current session exists -- use Connect-ChatSession to create a session" } $targetSession } function SendMessage($session, $prompt, $functionDefinition, $allowAgentAccess = $null) { $sendBlock = GetSendBlock $session $targetPrompt = if ( ! $sendBlock ) { $prompt } else { $sendBlock.Invoke($prompt) } $response = try { if ( $functionDefinition ) { $session.GenerateFunctionResponse($functionDefinition, $targetPrompt, $allowAgentAccess) } else { $session.GenerateMessage(@($targetPrompt), $allowAgentAccess) } } catch { $targetException = $_.Exception # Check for specific exceptions where we can provide guidance to the user if ( $_.Exception -and $_.Exception.InnerException -is [Modulus.ChatGPS.Models.AIServiceException] ) { # Check for missing add-on components such as Onnx local model libraries if ( $_.Exception.InnerException.OriginalExceptionTypeName -in ( [TypeLoadException].FullName, [MissingMethodException].FullName) ) { # Recreate the exception in this case to customize the message $targetException = ($_.Exception.GetType())::new($script:InstallAddonsMessage, $_.Exception) write-warning $script:InstallAddonsMessage } } throw $targetException } $receiveBlock = GetReceiveBlock $session if ( ! $receiveBlock ) { $response } else { $processedResponse = $receiveBlock.Invoke(@($response)) $session.UpdateLastResponse($processedResponse) $processedResponse } } function SendConnectionTestMessage($session) { if ( ! $session.AccessValidated -and $session.IsRemote ) { write-progress "Connecting" -Percent 25 $session.SendStandaloneMessage("Hello from $($session.id)", 'You are a friendly conversationalist.', $false) | out-null write-progress "Connecting" -Completed } } function GetSendBlock($session) { if ( $session.CustomContext ) { $session.CustomContext['SendBlock'] } } function GetReceiveBlock($session) { if ( $session.CustomContext ) { $session.CustomContext['ReceiveBlock'] } } function RegisterSessionCompleter([string] $command, [string] $parameterName, [string] $propertyNameToComplete) { # This scheme just needs to be changed -- there is confusion between the parameter being completed # and the property on the session object. # Because of all this complexity, we have to dynamically generate code to create scriptblock. :( $targetProperty = if ( $propertyNameToComplete ) { $propertyNameToComplete } else { $parameterName } $completerDefinition = @' param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $sessions = GetChatSessions | where {0} -ne $null | sort-object $parameterName $sessions.{0} | where {{ $_.ToString().StartsWith($wordToComplete, [System.StringComparison]::InvariantCultureIgnoreCase) }} '@ -f $targetProperty $scriptBlock = [ScriptBlock]::Create($completerDefinition) # Have to use this NewBoundScriptBlock trick so that the generated code is actually # part of the same module as this (the calling) function. Wild. $sessionNameCompleter = {}.Module.NewBoundScriptBlock($scriptBlock) Register-ArgumentCompleter -commandname $command -ParameterName $parameterName -ScriptBlock $sessionNameCompleter } |