From a08955a32b85ebfeab01fb6b86d8804680e9a9e7 Mon Sep 17 00:00:00 2001 From: Mathias Burger Date: Tue, 7 May 2024 15:20:28 +0200 Subject: [PATCH 1/3] 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 --- CONTRIBUTING.md | 9 ++++++ README.md | 13 ++++++++ please.sh | 77 +++++++++++++++++++++++++---------------------- test/join_by.bats | 49 ++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 36 deletions(-) create mode 100644 test/join_by.bats diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 94e9c97..9f86562 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,3 +42,12 @@ Resolves #7 ``` Furthermore, commits must be signed off according to the [DCO](DCO). + +### Testing + +Install [bats](https://bats-core.readthedocs.io/en/stable/installation.html). + +Run tests: +``` +bats --formatter pretty test +``` \ No newline at end of file diff --git a/README.md b/README.md index b4fd116..b136dba 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,19 @@ To store your API key using macOS keychain, run security add-generic-password -a "${USER}" -s OPENAI_API_KEY -w "${apiKey}" ``` +## 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 + ## Troubleshooting If you receive the following error message: diff --git a/please.sh b/please.sh index a1137a4..d4c3639 100755 --- a/please.sh +++ b/please.sh @@ -2,7 +2,7 @@ set -uo pipefail -model='gpt-4' +model=${PLEASE_OPENAI_CHAT_MODEL:-'gpt-4-turbo'} options=("[I] Invoke" "[C] Copy to clipboard" "[Q] Ask a question" "[A] Abort" ) number_of_options=${#options[@]} keyName="OPENAI_API_KEY" @@ -22,7 +22,10 @@ exclamation="\xE2\x9D\x97" questionMark="\x1B[31m?\x1B[0m" checkMark="\x1B[31m\xE2\x9C\x93\x1B[0m" -openai_invocation_url=${OPENAI_URL:-"https://api.openai.com/v1"} +openai_api_base=${PLEASE_OPENAI_API_BASE:-${OPENAI_API_BASE:-${OPENAI_URL:-"https://api.openai.com"}}} +openai_api_version=${PLEASE_OPENAI_API_VERSION:-${OPENAI_API_VERSION:-"v1"}} +openai_invocation_url=${openai_api_base}/${openai_api_version} + fail_msg="echo 'I do not know. Please rephrase your question.'" declare -a qaMessages=() @@ -98,10 +101,10 @@ function store_api_key() { else if [[ "$OSTYPE" == "darwin"* ]]; then security add-generic-password -a "${USER}" -s "${keyName}" -w "${apiKey}" -U - OPENAI_API_KEY=$(security find-generic-password -a "${USER}" -s "${keyName}" -w) + PLEASE_OPENAI_API_KEY=$(security find-generic-password -a "${USER}" -s "${keyName}" -w) else echo -e "${apiKey}" | secret-tool store --label="${keyName}" username "${USER}" key_name "${keyName}" - OPENAI_API_KEY=$(secret-tool lookup username "${USER}" key_name "${keyName}") + PLEASE_OPENAI_API_KEY=$(secret-tool lookup username "${USER}" key_name "${keyName}") fi echo "API key stored successfully and set as a global variable." break @@ -130,7 +133,7 @@ display_help() { echo " The remaining arguments are used as input to be turned into a CLI command." echo echo "OpenAI API Key:" - echo " The API key needs to be set as OPENAI_API_KEY environment variable or keychain entry. " + echo " The API key needs to be set as PLEASE_OPENAI_API_KEY or OPENAI_API_KEY environment variable or keychain entry. " } @@ -141,8 +144,8 @@ debug() { } check_key() { - if [ -z "${OPENAI_API_KEY+x}" ]; then - debug "OPENAI_API_KEY environment variable not set, trying to find it in keychain" + if [ -z "${PLEASE_OPENAI_API_KEY:-${OPENAI_API_KEY:-}}" ]; then + debug "PLEASE_OPENAI_API_KEY or OPENAI_API_KEY environment variable not set, trying to find it in keychain" get_key_from_keychain fi } @@ -156,7 +159,7 @@ get_key_from_keychain() { ;; Linux*) if ! command -v secret-tool &> /dev/null; then - debug "OPENAI_API_KEY not set and secret-tool not installed. Install it with 'sudo apt install libsecret-tools'." + debug "PLEASE_OPENAI_API_KEY or OPENAI_API_KEY not set and secret-tool not installed. Install it with 'sudo apt install libsecret-tools'." exitStatus=1 else key=$(secret-tool lookup username "${USER}" key_name "${keyName}") @@ -164,13 +167,13 @@ get_key_from_keychain() { fi ;; *) - debug "OPENAI_API_KEY not set and no supported keychain available." + debug "PLEASE_OPENAI_API_KEY or OPENAI_API_KEY not set and no supported keychain available." exitStatus=1 ;; esac if [ "${exitStatus}" -ne 0 ]; then - echo "OPENAI_API_KEY not set and unable to find it in keychain. See the README on how to persist your key." + echo "PLEASE_OPENAI_API_KEY or OPENAI_API_KEY not set and unable to find it in keychain. See the README on how to persist your key." echo "You can get an API at https://beta.openai.com/" echo "Please enter your OpenAI API key now to use it this time only, or rerun 'please -a' to store it in the keychain." echo "Your API Key:" @@ -181,7 +184,7 @@ get_key_from_keychain() { echo "No API key provided. Exiting." exit 1 fi - OPENAI_API_KEY="${key}" + PLEASE_OPENAI_API_KEY="${key}" } get_command() { @@ -221,7 +224,7 @@ perform_openai_request() { -s -w "\n%{http_code}" \ -H "Content-Type: application/json" \ -H "Accept-Encoding: identity" \ - -H "Authorization: Bearer ${OPENAI_API_KEY}" \ + -H "Authorization: Bearer ${PLEASE_OPENAI_API_KEY:-$OPENAI_API_KEY}" \ -d "${payload}" \ --silent) debug "Response:\n${result[*]}" @@ -433,12 +436,7 @@ answer_question_about_command() { prompt="${question}" escapedPrompt=$(printf %s "${prompt}" | jq -srR '@json') qaMessages+=("{ \"role\": \"user\", \"content\": ${escapedPrompt} }") - - if [ -z "${qaMessages+x}" ]; then - messagesJson="[]" - else - messagesJson='['$(join_by , "${qaMessages[@]}")']' - fi + messagesJson='['$(join_by , "${qaMessages[@]}")']' payload=$(jq --null-input --compact-output --argjson messagesJson "${messagesJson}" '{ max_tokens: 200, @@ -460,26 +458,33 @@ function join_by { fi } -if [ $# -eq 0 ]; then - input=("-h") -else - input=("$@") -fi +function main() { + if [ $# -eq 0 ]; then + input=("-h") + else + input=("$@") + fi + + check_args "${input[@]}" + check_key -check_args "${input[@]}" -check_key + get_command + if [ "${explain}" -eq 1 ]; then + explain_command + fi -get_command -if [ "${explain}" -eq 1 ]; then - explain_command -fi + print_option -print_option + if test "${command}" = "${fail_msg}"; then + exit 1 + fi -if test "${command}" = "${fail_msg}"; then - exit 1 -fi + init_questions + choose_action + act_on_action +} -init_questions -choose_action -act_on_action +# Only call main if the script is not being sourced +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi \ No newline at end of file diff --git a/test/join_by.bats b/test/join_by.bats new file mode 100644 index 0000000..0beb224 --- /dev/null +++ b/test/join_by.bats @@ -0,0 +1,49 @@ +#!/usr/bin/env bats + +load $BATS_TEST_DIRNAME/../please.sh + +@test "join elements with a comma" { + result=$(join_by ',' 'a' 'b' 'c') + [ "$result" == "a,b,c" ] +} + +@test "join elements with a semicolon" { + result=$(join_by ';' '1' '2' '3') + [ "$result" == "1;2;3" ] +} + +@test "join with a space as delimiter" { + result=$(join_by ' ' 'x' 'y' 'z') + [ "$result" == "x y z" ] +} + +@test "join with an empty delimiter" { + result=$(join_by '' '1' '2' '3') + [ "$result" == "123" ] +} + +@test "no delimiter provided should return the first element" { + result=$(join_by '' 'single') + [ "$result" == "single" ] +} + +@test "join using array with no elements" { + qaMessages=() + + result=$(join_by , "${qaMessages[@]}") + [ "$result" == "" ] +} + +@test "join using array with one element" { + qaMessages=("a") + + result=$(join_by , "${qaMessages[@]}") + [ "$result" == "a" ] +} + +@test "join using array with two elements" { + qaMessages=("a" "b") + + result=$(join_by , "${qaMessages[@]}") + [ "$result" == "a,b" ] +} \ No newline at end of file From 6677a13b594e6c39b3e81a5a2e9405de578de6d7 Mon Sep 17 00:00:00 2001 From: Mathias Burger Date: Wed, 8 May 2024 11:27:46 +0200 Subject: [PATCH 2/3] Add BATS shell tests to github workflows Signed-off-by: Mathias Burger --- .github/workflows/test.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 063d8cb..20103ba 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,3 +14,12 @@ jobs: - uses: actions/checkout@v3 - name: Run ShellCheck uses: ludeeus/action-shellcheck@master + bats-tests: + name: BATS Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install BATS + run: sudo apt-get update && sudo apt-get install -y bats + - name: Run BATS Tests + run: bats --formatter tap test From 0641366e9073e1d34e16312dee04db9e53cb447c Mon Sep 17 00:00:00 2001 From: Mathias Burger Date: Tue, 14 May 2024 15:33:29 +0200 Subject: [PATCH 3/3] Test main() with help and version flags Signed-off-by: Mathias Burger --- test/main.bats | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 test/main.bats diff --git a/test/main.bats b/test/main.bats new file mode 100644 index 0000000..a200d5f --- /dev/null +++ b/test/main.bats @@ -0,0 +1,13 @@ +#!/usr/bin/env bats + +PLEASE_EXE=$BATS_TEST_DIRNAME/../please.sh + +@test "smoke test please --help" { + result=$($PLEASE_EXE '--help') + [ "${result:0:6}" == "Please" ] +} + +@test "smoke test please --version" { + result=$($PLEASE_EXE '--version') + [ "${result:0:8}" == "Please v" ] +} \ No newline at end of file