diff --git a/council.sh b/council.sh index 833e5ca..fbdc374 100755 --- a/council.sh +++ b/council.sh @@ -44,6 +44,301 @@ RETURN_ERROR=1 YES_COMMAND_HANDLED=1 NO_COMMAND_HANDLED=0 +################################################################################ +# Ollama Bash Lib Functions +################################################################################ + +OLLAMA_LIB_API="${OLLAMA_HOST:-http://localhost:11434}" # Ollama API URL, No slash at end +OLLAMA_LIB_DEBUG="${OLLAMA_LIB_DEBUG:-0}" # 0 = debug off, 1 = debug, 2 = verbose debug +OLLAMA_LIB_TIMEOUT="${OLLAMA_LIB_TIMEOUT:-300}" # Curl timeout in seconds +OLLAMA_LIB_TURBO_KEY="" # Turbo API Key +OLLAMA_LIB_STREAM=0 # Streaming mode: 0 = No streaming, 1 = Yes streaming +OLLAMA_LIB_THINKING="${OLLAMA_LIB_THINKING:-off}" # Thinking mode: off, on, hide +OLLAMA_LIB_TOOLS_DEFINITION=() # Array of tool definitions + +_redact() { + local msg="$1" + if [[ -n "${OLLAMA_LIB_TURBO_KEY}" ]]; then + msg=${msg//"${OLLAMA_LIB_TURBO_KEY}"/'[REDACTED]'} # never show the private api key + fi + printf '%s' "$msg" +} + +_lib_debug() { + if [ "$DEBUG_MODE" -eq 1 ]; then + local date_string + date_string="$(if ! date '+%H:%M:%S:%N' 2>/dev/null; then date '+%H:%M:%S'; fi)" + printf "[DEBUG] ${date_string}: %s\n" "$(_redact "$1")" >&2 + fi +} + +_lib_error() { + error "$(_redact "$1")" +} + +_exists() { + command -v "$1" >/dev/null 2>&1 + return $? +} + +_is_valid_json() { + if [[ -z "$1" ]]; then + _lib_debug '_is_valid_json: empty string' + return 1 + fi + if ! _exists 'jq'; then _lib_error '_is_valid_json: jq Not Found'; return 1; fi + printf '%s' "$1" | jq -e '.' >/dev/null 2>&1 + local return_code=$? + case $return_code in + 0) _lib_debug '_is_valid_json: success'; return 0 ;; + 1) _lib_debug '_is_valid_json: FAILURE jq: output false or null: return 1'; return 1 ;; + 2) _lib_debug '_is_valid_json: USAGE ERROR jq: incorrect command-line options: return 2'; return 2 ;; + 3) _lib_debug '_is_valid_json: COMPILE ERROR jq: filter syntax error: return 3'; return 3 ;; + 4) _lib_debug '_is_valid_json: NO OUTPUT jq: result empty: return 4'; return 4 ;; + 5) _lib_debug '_is_valid_json: HALT_ERROR jq: return 5'; return 5 ;; + *) _lib_debug "_is_valid_json: UNKNOWN jq error: return $return_code"; return "$return_code" ;; + esac +} + +_call_curl() { + _lib_debug "_call_curl: [${1:0:42}] [${2:0:42}] ${3:0:120}" + if ! _exists 'curl'; then _lib_error '_call_curl: curl Not Found'; return 1; fi + local method="$1" + if [[ -z "$method" || ( "$method" != "GET" && "$method" != "POST" ) ]]; then + _lib_error '_call_curl: Method Not Found. Usage: _call_curl "GET|POST" "/api/path" "{ optional json content }"' + return 1 + fi + local endpoint="$2" + if [[ -n "$endpoint" && ( "$endpoint" != /* || "$endpoint" == *" "* || "$endpoint" == *"\\"* ) ]]; then + _lib_error "_call_curl: Invalid API Path: [${endpoint:0:120}]" + return 1 + fi + local json_body="$3" + if [[ -n "$json_body" ]] && ! _is_valid_json "$json_body"; then + _lib_error "_call_curl: JSON body is invalid: [${json_body:0:120}]" + return 1 + fi + _lib_debug "_call_curl: OLLAMA_LIB_API: $OLLAMA_LIB_API" + local curl_args=(-s -N --max-time "$OLLAMA_LIB_TIMEOUT" -H 'Content-Type: application/json' -w '\n%{http_code}') + if [[ -n "${OLLAMA_LIB_TURBO_KEY}" ]]; then + _lib_debug '_call_curl: Turbo Mode' + curl_args+=( -H "Authorization: Bearer ${OLLAMA_LIB_TURBO_KEY}" ) + fi + curl_args+=( -X "$method" ) + curl_args+=( "${OLLAMA_LIB_API}${endpoint}" ) + local response + local curl_exit_code + if [[ -n "$json_body" ]]; then + _lib_debug "_call_curl: json_body: ${json_body:0:120}" + curl_args+=( -d "@-" ) + _lib_debug "_call_curl: piping json_body | curl ${curl_args[*]}" + response="$(printf '%s' "$json_body" | curl "${curl_args[@]}")" + curl_exit_code=$? + else + _lib_debug "_call_curl: args: ${curl_args[*]}" + response="$(curl "${curl_args[@]}")" + curl_exit_code=$? + fi + if (( curl_exit_code )); then + _lib_error "_call_curl: curl command failed with exit code $curl_exit_code" + return "$curl_exit_code" + fi + local http_code + http_code="$(printf '%s' "$response" | tail -n1)" + local body + body="$(printf '%s' "$response" | sed '$d')" + if (( http_code >= 400 )); then + _lib_error "_call_curl: HTTP error ${http_code}: ${body}" + return 1 + fi + printf '%s' "$body" + return 0 +} + +ollama_api_post() { + _lib_debug "ollama_api_post: [${1:0:42}] ${2:0:120}" + _call_curl "POST" "$1" "$2" + local error_curl=$? + if (( error_curl )); then + _lib_error "ollama_api_post: curl error: $error_curl" + return "$error_curl" + fi + _lib_debug 'ollama_api_post: success' + return 0 +} + +_is_valid_model() { + local model="${1:-}" + if [[ -z "$model" ]]; then + _lib_debug '_is_valid_model: Model name empty: getting random model' + model="$(ollama_model_random)" + if [[ -z "$model" ]]; then + _lib_debug '_is_valid_model: Model Not Found: ollama_model_random failed' + printf '' + return 1 + fi + fi + if [[ ! "$model" =~ ^[a-zA-Z0-9._:/-]+$ ]]; then + _lib_debug "_is_valid_model: INVALID: [${model:0:120}]" + printf '' + return 1 + fi + _lib_debug "_is_valid_model: VALID: [${model:0:120}]" + printf '%s' "$model" + return 0 +} + +ollama_model_random() { + if ! ollama_app_installed; then _lib_error 'ollama_model_random: ollama is not installed'; return 1; fi + local models + models=$(ollama list | awk 'NR>1 {print $1}' | grep -v '^$') + if [[ -z "$models" ]]; then + _lib_error 'ollama_model_random: get ollama list failed' + return 1 + fi + if _exists 'shuf'; then + printf '%s\n' "$models" | shuf -n1 + else + printf '%s\n' "$models" | awk 'BEGIN{srand()} {a[NR]=$0} END{if(NR) print a[int(rand()*NR)+1]}' + fi +} + +_ollama_payload_generate() { + local model="$1" + local prompt="$2" + local stream=true + (( OLLAMA_LIB_STREAM == 0 )) && stream=false + local thinking=false + [[ "$OLLAMA_LIB_THINKING" == 'on' || "$OLLAMA_LIB_THINKING" == 'hide' ]] && thinking=true + local payload + payload="$(jq -c -n \ + --arg model "$model" \ + --arg prompt "$prompt" \ + --argjson stream "$stream" \ + --argjson thinking "$thinking" \ + '{model: $model, prompt: $prompt, stream: $stream, thinking: $thinking}')" + if (( ${#OLLAMA_LIB_TOOLS_DEFINITION[@]} > 0 )); then + local tools_json + tools_json='['$(IFS=,; echo "${OLLAMA_LIB_TOOLS_DEFINITION[*]}")']' + payload="$(printf '%s' "$payload" | jq -c --argjson tools "$tools_json" '. + {tools: $tools}')" + fi + printf '%s' "$payload" +} + +ollama_generate_json() { + _lib_debug "ollama_generate_json: [${1:0:42}] [${2:0:42}]" + if ! _exists 'jq'; then _lib_error 'ollama_generate_json: Not Found: jq'; return 1; fi + local model + model="$(_is_valid_model "$1")" + if [[ -z "$model" ]]; then + _lib_error 'ollama_generate_json: Not Found: model. Usage: ollama_generate_json "model" "prompt"' + return 1 + fi + local prompt="$2" + if [[ -z "$prompt" ]]; then + _lib_error 'ollama_generate_json: Not Found: prompt. Usage: ollama_generate_json "model" "prompt"' + return 1 + fi + local json_payload + json_payload="$(_ollama_payload_generate "$model" "$prompt")" + _lib_debug "ollama_generate_json: json_payload: ${json_payload:0:120}" + if ! ollama_api_post '/api/generate' "$json_payload"; then + _lib_error 'ollama_generate_json: ollama_api_post failed' + return 1 + fi + _lib_debug 'ollama_generate_json: success' + return 0 +} + +ollama_generate() { + if ! _exists 'jq'; then _lib_error 'ollama_generate: jq Not Found'; return 1; fi + _lib_debug "ollama_generate: [${1:0:42}] [${2:0:42}]" + OLLAMA_LIB_STREAM=0 + local result + result="$(ollama_generate_json "$1" "$2")" + local error_ollama_generate_json=$? + _lib_debug "ollama_generate: result: $(echo "$result" | wc -c | tr -d ' ') bytes: ${result:0:120}" + if (( error_ollama_generate_json )); then + _lib_error "ollama_generate: error_ollama_generate_json: $error_ollama_generate_json" + return 1 + fi + if ! _is_valid_json "$result"; then + _lib_error 'ollama_generate: model response is not valid JSON' + return 1 + fi + if error_msg=$(printf '%s' "$result" | jq -r '.error // empty'); then + if [[ -n $error_msg ]]; then + _lib_error "ollama_generate: $error_msg" + return 1 + fi + fi + _lib_debug "ollama_generate: thinking: $OLLAMA_LIB_THINKING" + if [[ "$OLLAMA_LIB_THINKING" != 'hide' ]]; then + local thinking + thinking="$(printf '%s' "$result" | jq -r '.thinking // empty')" + if [[ -n "$thinking" ]]; then + _lib_debug 'ollama_generate: thinking FOUND' + printf '# \n# %s\n# \n\n' "$thinking" >&2 + fi + fi + local result_response + result_response="$(printf '%s' "$result" | jq -r '.response')" + if [[ -z "$result_response" ]]; then + _lib_error 'ollama_generate: jq failed to get .response' + return 1 + fi + printf '%s\n' "$result_response" + _lib_debug 'ollama_generate: success' + return 0 +} + +ollama_app_installed() { + _lib_debug 'ollama_app_installed' + _exists "ollama" +} + +ollama_list() { + if ! ollama_app_installed; then _lib_error 'ollama_list: ollama is not installed'; return 1; fi + local list + if ! list="$(ollama list)"; then + _lib_error 'ollama_list: list=|ollama list failed' + return 1 + fi + if ! echo "$list" | head -n+1; then + _lib_error 'ollama_list: echo|head failed' + return 1 + fi + if ! echo "$list" | tail -n+2 | sort; then + _lib_error 'ollama_list: ollama echo|tail|sort failed' + return 1 + fi + return 0 +} + +ollama_list_array() { + if ! ollama_app_installed; then _lib_error 'ollama_list_array: ollama is not installed'; return 1; fi + local models=() + while IFS= read -r line; do + models+=("$line") + done < <(ollama list | awk 'NR > 1 {print $1}' | sort) + echo "${models[@]}" + _lib_debug "ollama_list_array: ${#models[@]} models found: return 0" + return 0 +} + +ollama_ps() { + if ! ollama_app_installed; then _lib_error 'ollama_ps: ollama is not installed'; return 1; fi + if ! ollama ps; then + _lib_error 'ollama_ps: ollama ps failed' + return 1 + fi + return 0 +} + +################################################################################ +# End Ollama Bash Lib Functions +################################################################################ + banner() { echo " ▗▖ ▗▖ ▗▖ ▗▖ ▗▄▄▖ ▗▄▖ ▗▖ ▗▖▗▖ ▗▖ ▗▄▄▖▗▄▄▄▖▗▖ @@ -264,7 +559,7 @@ setModels() { if [ "$noModels" -eq 1 ]; then return fi - models=($(ollama list | awk '{if (NR > 1) print $1}' | sort)) # Get list of models, sorted alphabetically + models=($(ollama_list_array)) # Get list of models, sorted alphabetically if [ -z "$models" ]; then error "No models installed in Ollama. Please install models with 'ollama pull '" >&2 exit $RETURN_ERROR @@ -384,7 +679,7 @@ removeThinking() { ollamaRunWithTimeout() { local stderrFile=$(mktemp) ( - ollama run "$model" --hidethinking -- "${rules}${context}" 2> "$stderrFile" + ollama_generate "$model" "${rules}${context}" 2> "$stderrFile" ) & local pidOllama=$! ( @@ -533,12 +828,12 @@ handleAdminCommands() { ;; /olist) # Ollama list sendToTerminal "\nModels available in Ollama:\n" - ollama list | awk '{if (NR > 1) print $1}' | sort + ollama_list return $YES_COMMAND_HANDLED ;; /ps) # Ollama ps sendToTerminal "\n" - ollama ps + ollama_ps sendToTerminal "\n" return $YES_COMMAND_HANDLED ;; @@ -564,7 +859,7 @@ handleAdminCommands() { error "Model <$message> is already in the chat." return $YES_COMMAND_HANDLED fi - local ollamaModels=($(ollama list | awk '{if (NR > 1) print $1}')) + local ollamaModels=($(ollama_list_array)) if ! inArray "$message" "${ollamaModels[@]}"; then error "Model <$message> not found in Ollama." return $YES_COMMAND_HANDLED