From ccf2c187a4acbfdff3a9e594a9188262c09b061e Mon Sep 17 00:00:00 2001 From: Mathias Burger Date: Mon, 6 May 2024 23:18:03 +0200 Subject: [PATCH] Use openai compatible environment variables for configuration and allow configuration using more specific variables that do not interfere with the openai defaults Signed-off-by: Mathias Burger --- .gitignore | 1 + CONTRIBUTING.md | 16 ++++++++ PleasePwsh/PleasePwsh.psm1 | 52 ++++++++++++++++++++--- Readme.md | 14 +++++++ Tests/Configuration.Tests.ps1 | 77 +++++++++++++++++++++++++++++++++++ 5 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 .gitignore create mode 100644 Tests/Configuration.Tests.ps1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62c8935 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec10411..d1d1494 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,3 +42,19 @@ Resolves #7 ``` Furthermore, commits must be signed off according to the [DCO](DCO). + + +### Testing + +Install and set up [Pester](https://pester.dev/docs/quick-start). + +```pwsh +Install-Module Pester -Force +Import-Module Pester -PassThru +``` + +Run the tests with Pester: + +```pwsh +Invoke-Pester -Path .\Tests +``` \ No newline at end of file diff --git a/PleasePwsh/PleasePwsh.psm1 b/PleasePwsh/PleasePwsh.psm1 index f800344..2dabcdf 100644 --- a/PleasePwsh/PleasePwsh.psm1 +++ b/PleasePwsh/PleasePwsh.psm1 @@ -8,6 +8,48 @@ Set-PSDebug -Strict $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" +function Get-OpenAIApiKey { + if ($env:PLEASE_OPENAI_API_KEY) { + return $env:PLEASE_OPENAI_API_KEY + } + if ($env:OPENAI_API_KEY) { + return $env:OPENAI_API_KEY + } + return $null +} + +function Get-OpenAIModel { + if ($env:PLEASE_OPENAI_CHAT_MODEL) { + return $env:PLEASE_OPENAI_CHAT_MODEL + } + # There is no openai compatible environment variable to set a default model + return "gpt-3.5-turbo" +} + +function Get-OpenAIBaseUrl { + if ($env:PLEASE_OPENAI_API_BASE) { + return $env:PLEASE_OPENAI_API_BASE + } + if ($env:OPENAI_API_BASE) { + return $env:OPENAI_API_BASE + } + return "https://api.openai.com" +} + +function Get-OpenAIApiVersion { + if ($env:PLEASE_OPENAI_API_VERSION) { + return $env:PLEASE_OPENAI_API_VERSION + } + if ($env:OPENAI_API_VERSION) { + return $env:OPENAI_API_VERSION + } + return "v1" +} + +function Get-OpenAIBaseUrlWithVersion { + return "$(Get-OpenAIBaseUrl)/$(Get-OpenAIApiVersion)" +} + <# .SYNOPSIS Translates a prompt into a PowerShell command using OpenAI GPT. @@ -53,7 +95,7 @@ function Please { } function Test-ApiKey { - if ($null -eq $env:OPENAI_API_KEY) { + if ($null -eq $(Get-OpenAIApiKey)) { Write-Output "`u{1F50E} Api key missing. See https://help.openai.com/en/articles/4936850-where-do-i-find-my-secret-api-key" $Key = Read-Host "Please provide the api key" @@ -68,7 +110,7 @@ function Get-PwshCommand([string]$Prompt) { $Role = "You translate the input given into PowerShell command. You may not use natural language, but only a PowerShell commands as answer. Do not use markdown. Do not quote the whole output. If you do not know the answer, answer only with 'I do not know'" $Payload = @{ - 'model' = "gpt-3.5-turbo" + 'model' = Get-OpenAIModel 'messages' = @( @{ 'role' = 'system'; 'content' = $Role }, @{ 'role' = 'user'; 'content' = $Prompt } @@ -115,7 +157,7 @@ function Get-CommandExplanation([string]$Command) { $Payload = @{ 'max_tokens' = 100 - 'model' = "gpt-3.5-turbo" + 'model' = Get-OpenAIModel 'messages' = @( @{ 'role' = 'user'; 'content' = $Prompt } ) @@ -125,11 +167,11 @@ function Get-CommandExplanation([string]$Command) { } function Invoke-OpenAIRequest($Payload) { - $Uri = "https://api.openai.com/v1/chat/completions" + $Uri = "$(Get-OpenAIBaseUrlWithVersion)/chat/completions" $Headers = @{ 'Content-Type' = 'application/json' - 'Authorization' = "Bearer $env:OPENAI_API_KEY" + 'Authorization' = "Bearer $(Get-OpenAIApiKey)" } try { diff --git a/Readme.md b/Readme.md index 6dbd7ff..68a6307 100644 --- a/Readme.md +++ b/Readme.md @@ -46,3 +46,17 @@ Install-Module -Name PleasePwsh Set the OpenAI API key as environment variable. - Open PowerShell profile: `Code $PROFILE` - Add a line with your API key: `$ENV:OPENAI_API_KEY = ` + + +## Configuration + +You can use the following OpenAI compatible environment variables: +* `OPENAI_API_KEY` - Your OpenAI API key +* `OPENAI_API_BASE` - The base URL for the OpenAI API +* `OPENAI_API_VERSION` - The version of the OpenAI API + +You can use the more specific environment variables if you do not want to change OpenAI settings globally: +* `PLEASE_OPENAI_API_KEY` - Your OpenAI API key +* `PLEASE_OPENAI_API_BASE` - The base URL for the OpenAI API +* `PLEASE_OPENAI_API_VERSION` - The version of the OpenAI API +* `PLEASE_OPENAI_CHAT_MODEL` - The chat model to use diff --git a/Tests/Configuration.Tests.ps1 b/Tests/Configuration.Tests.ps1 new file mode 100644 index 0000000..a32a02e --- /dev/null +++ b/Tests/Configuration.Tests.ps1 @@ -0,0 +1,77 @@ +BeforeAll { + Remove-Module -Name PleasePwsh -ErrorAction SilentlyContinue + Import-Module $PSScriptRoot/../PleasePwsh/PleasePwsh.psm1 + + $env:OPENAI_API_KEY = "not-set" + + InModuleScope PleasePwsh { + Mock Invoke-RestMethod { return @{ choices = @(@{ message = @{ content = "Hello, World!" } }) } } + Mock Show-Menu { } + Mock Invoke-Action { } + } +} + +AfterAll { + $env:OPENAI_API_KEY = $null + $env:OPENAI_API_BASE = $null + $env:OPENAI_API_VERSION = $null + $env:PLEASE_OPENAI_API_BASE = $null + $env:PLEASE_OPENAI_API_VERSION = $null + $env:PLEASE_OPENAI_CHAT_MODEL = $null +} + +Describe 'OpenAI Configuration' { + It 'Given no configuration, defaults are used' { + InModuleScope PleasePwsh { + Please "say hello" + + Assert-MockCalled Invoke-RestMethod -Times 1 -ParameterFilter { + return ($Body | ConvertFrom-Json).model -eq 'gpt-3.5-turbo' -and + $Uri -eq 'https://api.openai.com/v1/chat/completions' -and + $Method -eq 'Post' -and + $Headers["Content-Type"] -eq 'application/json' + } + } + } + + It 'Given configuration from openai compatible environment variables are used' { + InModuleScope PleasePwsh { + $env:OPENAI_API_BASE = "openai-api-base" + $env:OPENAI_API_VERSION = "openai-api-version" + + Please "say hello" + + Assert-MockCalled Invoke-RestMethod -Times 1 -ParameterFilter { + return ($Body | ConvertFrom-Json).model -eq 'gpt-3.5-turbo' -and + $Uri -eq 'openai-api-base/openai-api-version/chat/completions' -and + $Method -eq 'Post' -and + $Headers["Content-Type"] -eq 'application/json' + } + } + } + + It 'Given configuration from please specific environment variables are preferred' { + InModuleScope PleasePwsh { + $env:OPENAI_API_BASE = "openai-api-base" + $env:OPENAI_API_VERSION = "openai-api-version" + + $env:PLEASE_OPENAI_API_BASE = "please-api-base" + $env:PLEASE_OPENAI_API_VERSION = "please-api-version" + + $env:PLEASE_OPENAI_CHAT_MODEL = "please-chat-model" + + Mock Invoke-RestMethod { return @{ choices = @(@{ message = @{ content = "Hello, World!" } }) } } + Mock Show-Menu { } + Mock Invoke-Action { } + + Please "say hello" + + Assert-MockCalled Invoke-RestMethod -Times 1 -ParameterFilter { + return ($Body | ConvertFrom-Json).model -eq 'please-chat-model' -and + $Uri -eq 'please-api-base/please-api-version/chat/completions' -and + $Method -eq 'Post' -and + $Headers["Content-Type"] -eq 'application/json' + } + } + } +} \ No newline at end of file