psgpt.ps1
# Initialize the conversation file path as a file path in the current directory named conversation-{iso8601 current date}.json $global:conversationFilePath = "conversation-" + (Get-Date -Format "yyyy-MM-ddTHH-mm-ss") + ".json" function Invoke-ChatGptAPI { param ( [string] $model, [PSCustomObject[]] $messages, [string] $apiKey ) if (!$apiKey) { Write-Output "Error: OpenAI API Key not found in environment variables. Set the CHATGPT_API_KEY environment variable with your API key." return } $header = @{ "Content-Type" = "application/json" "Authorization" = "Bearer $apiKey" } $body = @{ "model" = $model # convert messages into PSCustomObject array if there are any # otherwise, set it to an empty array "messages" = if ($messages) { @($messages) } else { @() } } # convert messages into array $body.messages = @($body.messages) $json = $body | ConvertTo-Json $response = Invoke-RestMethod -Uri "https://api.openai.com/v1/chat/completions" -Method POST -Headers $header -Body $json return $response } function DeserializeObject { param ( [string] $content ) #Convert JSON file to an object $object = $content | ConvertFrom-Json if (!$object) { $object = [PSCustomObject]@{} } return $object } function Get-Conversation { param ( [string] $filePath ) if (-not (Test-Path $filePath)) { New-Item -Path $filePath -ItemType File -Force "{}" | Set-Content $filePath } $content = Get-Content $filePath $conversation = DeserializeObject $content # initialize $conversation and $conversation.messages if it is null if (!$conversation -or !$conversation.messages) { $conversation = [PSCustomObject]@{ messages = @() } } return $conversation } function Save-Conversation { param ( [string] $filePath, [object] $conversation ) $conversation | ConvertTo-Json | Set-Content $filePath Write-Output "Conversation saved to $filePath." } function Get-UserInput { return Read-Host "user" } function Write-Message { param ( [string] $role, [string] $content ) #if content is null, set it to empty string if (!$content) { $content = "" } # convert markdown to VT100 encoded string $vt100EncodedString = ($content | ConvertFrom-Markdown -AsVT100EncodedString).VT100EncodedString.Trim() # print the message Write-Output "$($role):`n$vt100EncodedString" } function Invoke-PsGpt-Command { param ( [string] $command ) # Get the command name $commandName = $command.Split(" ")[0] # Get the arguments $arguments = $command.Split(" ")[1..$command.Length] # Invoke the command if ($commandName -eq "/help" -or $commandName -eq "/h") { Write-Output "Commands:" Write-Output "/clear, /cls, /c: Clear the console." Write-Output "/delete, /del, /d: Delete the conversation file." Write-Output "/edit: Edit the conversation file." Write-Output "/exit, /quit, /q: Exit the script." Write-Output "/help, /h: Show this help message." Write-Output "/save, /s: Save the conversation to the file." Write-Output "/saveas, /sa: Save the conversation to a new file." Write-Output "/wq, /wq!, /x: Save the conversation to the file and exit the script." } elseif ($commandName -eq "/save" -or $commandName -eq "/s") { # Save the conversation to the file Save-Conversation -filePath $arguments[0] -conversation $conversation } elseif ($commandName -eq "/saveas" -or $commandName -eq "/sa") { $newFilePath = $arguments[1] # Prompt if arguments are not provided if (!$newFilePath) { $newFilePath = Read-Host "File path" } # Save the conversation to a new file Save-Conversation -filePath $newFilePath -conversation $conversation # Update the conversation file path $global:conversationFilePath = $newFilePath } elseif ($commandName -eq "/wq" -or $commandName -eq "/wq!" -or $commandName -eq "/x") { # Save the conversation and exit the script Save-Conversation -filePath $global:conversationFilePath -conversation $conversation break } elseif ($commandName -eq "/exit" -or $commandName -eq "/quit" -or $commandName -eq "/q") { # Exit the script break } elseif ($commandName -eq "/clear" -or $commandName -eq "/cls" -or $commandName -eq "/c") { # Clear the console Clear-Host } elseif ($commandName -eq "/edit") { # Edit the conversation file Invoke-Expression ("notepad.exe " + $arguments[0]) } elseif ($commandName -eq "/delete" -or $commandName -eq "/del" -or $commandName -eq "/d") { # Delete the conversation file Remove-Item $arguments[0] } else { Write-Output "Error: Command not found." } } function Invoke-PsGPT { # print version Write-Output "PsGPT v0.0.8" # (Get-Content -Path "psgpt.psd1" -Raw) -match "ModuleVersion = '(.*)'" | Out-Null; $Matches[1] # Get the file path for the conversation file $filePath = $args[0] # Use named arguments if ($args[0] -eq "-filePath") { $filePath = $args[1] } # filePath is not a mandatory field, if it is empty the $conversationFilePath global variable will be set instead if (!$filePath) { $filePath = $global:conversationFilePath } else { $global:conversationFilePath = $filePath } # Load the conversation from the file $conversation = Get-Conversation -filePath $global:conversationFilePath # if the conversation is an array of two elements # the first element is the conversation file path # the second element is the conversation object if ($conversation.Length -eq 2) { $global:conversationFilePath = $conversation[0] $conversation = $conversation[1] } # Print the conversation if it is not empty if ($conversation.messages) { $conversation.messages | ForEach-Object { # Invoke Write-Message function Write-Message $_.role $_.content } } # Load the ChatGPT API key from an environment variable $apiKey = [System.Environment]::GetEnvironmentVariable("CHATGPT_API_KEY", "User") # Check if the API key is set if (!$apiKey) { Write-Output "Error: OpenAI API Key not found in environment variables. Set the CHATGPT_API_KEY environment variable with your API key." return } # Main loop to wait for user input while ($true) { # Get user input from the console, allow multiline input so if user presses enter # It will not submit the message, unless the user submits an empty message as the last message $userInput = "" while ($true) { # Read the user input $tmpInput = Read-Host "user" if ($tmpInput) { $userInput += $tmpInput + "`n" } else { break } } # If the user input is empty, skip the rest of the loop if (!$userInput) { continue } $userInput = $userInput.Trim() # If the user input starts with a slash, it is a command if ($userInput.StartsWith("/")) { # Invoke the command Invoke-PsGpt-Command $userInput } else { # Add the user input to the conversation # initialize the conversation if it is empty if (!$conversation.messages) { $conversation.messages = @() } $conversation.messages += [PSCustomObject]@{ role = "user" content = $userInput } # Call the ChatGPT API $assistantResponse = Invoke-ChatGptAPI -model "gpt-3.5-turbo" -messages $conversation.messages -apiKey $apiKey # Parse each assistant response and trim the message, then print it $assistantResponse.choices | ForEach-Object { $_.message.content = $_.message.content.Trim() # Add the assistant response to the conversation $conversation.messages += $_.message Write-Message $_.message.role $_.message.content } } } } Invoke-PsGPT @args |