Posted on
Artificial Intelligence

Artificial Intelligence Bash Design Patterns

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

Artificial Intelligence Bash Design Patterns: Make Your Shell Scripts Think

If you’re comfortable piping text between grep, awk, and jq, imagine piping intelligence itself. AI endpoints are now just HTTP calls away. With a few robust Bash design patterns, you can turn brittle one-offs into reliable, testable, and scalable AI-powered command-line tools.

This article shows how to integrate LLMs into your shell scripts using a handful of proven patterns: template-and-version prompts, deterministic caching, robust retries with a circuit breaker, safe batch processing, and auditable logs. You’ll get copy‑pasteable snippets, real-world examples, and package-manager-specific install commands.

Why Bash + AI is a valid combo

  • Bash is the lingua franca for glue code: it’s already where you watch logs, transform text, and orchestrate jobs.

  • AI endpoints are HTTP-based: curl + jq gets you 90% there if you design for reliability.

  • Design patterns prevent the “it worked once on my laptop” problem: they increase reproducibility, control cost, and tame rate limits.

Prerequisites

We’ll use curl (HTTP), jq (JSON), and parallel (optional, for batch work). Install them with your package manager:

  • Debian/Ubuntu (apt):

    sudo apt update
    sudo apt install -y curl jq parallel
    
  • Fedora/RHEL/CentOS (dnf):

    sudo dnf install -y curl jq parallel
    
  • openSUSE (zypper):

    sudo zypper refresh
    sudo zypper install -y curl jq parallel
    

Set your API configuration (example uses an OpenAI-compatible endpoint; adjust as needed):

export OPENAI_API_KEY="sk-..."
export AI_API_BASE="https://api.openai.com"   # or your self-hosted/base URL
export AI_MODEL="gpt-4o-mini"                 # any model your provider supports

Tip: put those exports in ~/.bashrc (or .zshrc) for convenience.

Core patterns (copy-paste friendly)

Below is a compact library of functions you can drop into ai_patterns.sh. It implements:

1) Prompt templates with versioning
2) Deterministic file-based caching
3) Robust requests: timeouts, retries, backoff, and a circuit breaker
4) Safe batch processing (optional with parallel)
5) Auditable JSONL logs

#!/usr/bin/env bash
set -euo pipefail

# Config
AI_API_BASE="${AI_API_BASE:-https://api.openai.com}"
AI_MODEL="${AI_MODEL:-gpt-4o-mini}"
AI_TIMEOUT="${AI_TIMEOUT:-30}"           # seconds per request
AI_MAX_ATTEMPTS="${AI_MAX_ATTEMPTS:-5}"  # retries
AI_CACHE_DIR="${AI_CACHE_DIR:-.ai-cache}"
AI_LOG_JSONL="${AI_LOG_JSONL:-ai_calls.jsonl}"  # append-only audit log
CIRCUIT_FILE="${CIRCUIT_FILE:-.ai-circuit}"
CIRCUIT_OPEN_SECS="${CIRCUIT_OPEN_SECS:-60}"    # pause when tripped
CIRCUIT_FAIL_THRESHOLD="${CIRCUIT_FAIL_THRESHOLD:-5}"

mkdir -p "$AI_CACHE_DIR"

_now() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
_ts()  { date -u +%s; }

# --- 1) Prompt templates with versioning ---
# Keep a version string to invalidate caches when your prompt changes materially.
PROMPT_VERSION="v1-severity-labeler"

# Render a prompt template. You can add variables and versions here.
render_prompt() {
  local user_input="$1"
  cat <<EOF
You are a precise log line labeler. Output just one label: ERROR, WARN, or INFO.
Be conservative: only choose ERROR if it's clearly an error.
---INPUT---
$user_input
---END---
EOF
}

# --- 2) Deterministic file-based cache ---
_hash() {
  # Hash model + version + system + prompt for a stable key
  local key="$1"
  printf '%s' "$key" | sha256sum | awk '{print $1}'
}

cache_get() {
  local key="$1" f="$AI_CACHE_DIR/$key.txt"
  [[ -f "$f" ]] && cat "$f" && return 0
  return 1
}

cache_put() {
  local key="$1" value="$2" f="$AI_CACHE_DIR/$key.txt"
  printf '%s' "$value" > "$f"
}

# --- 3) Robust request with timeout, retries, and circuit breaker ---
_circuit_is_open() {
  [[ -f "$CIRCUIT_FILE" ]] || return 1
  local last_fail ts_now=$(_ts)
  last_fail=$(<"$CIRCUIT_FILE")
  (( ts_now - last_fail < CIRCUIT_OPEN_SECS )) && return 0 || return 1
}

_trip_circuit() {
  printf '%s' "$(_ts)" > "$CIRCUIT_FILE"
}

_clear_circuit() {
  [[ -f "$CIRCUIT_FILE" ]] && rm -f "$CIRCUIT_FILE"
}

ai_call_raw() {
  # Arguments: $1 = system_text, $2 = user_text
  local system_text="$1" user_text="$2"
  local payload tmp out status attempt=1 backoff=1

  if _circuit_is_open; then
    echo "Circuit open; backing off. Try again later." >&2
    return 75  # EX_TEMPFAIL
  fi

  tmp="$(mktemp)"
  payload="$(jq -n --arg model "$AI_MODEL" --arg sys "$system_text" --arg usr "$user_text" '{
      model: $model,
      messages: [
        {role:"system", content:$sys},
        {role:"user",   content:$usr}
      ],
      temperature: 0.2
    }')"

  while (( attempt <= AI_MAX_ATTEMPTS )); do
    # Send request; capture HTTP code separately
    status=$(
      curl -sS -m "$AI_TIMEOUT" -w '%{http_code}' -o "$tmp" \
        -H "Authorization: Bearer ${OPENAI_API_KEY:-}" \
        -H "Content-Type: application/json" \
        -X POST "$AI_API_BASE/v1/chat/completions" \
        -d "$payload"
    ) || status="000"

    if [[ "$status" == "200" ]]; then
      _clear_circuit
      out="$(jq -r '.choices[0].message.content // empty' < "$tmp")"
      rm -f "$tmp"
      printf '%s' "$out"
      return 0
    fi

    # Retry on transient issues: 429, 500-599, or network (000)
    if [[ "$status" == "429" || "$status" =~ ^5 || "$status" == "000" ]]; then
      sleep "$backoff"
      backoff=$(( backoff * 2 ))
      attempt=$(( attempt + 1 ))
      continue
    fi

    # Non-retryable: log and fail
    cat "$tmp" >&2
    rm -f "$tmp"
    _trip_circuit
    return 1
  done

  # Max attempts exceeded
  cat "$tmp" >&2 || true
  rm -f "$tmp"
  _trip_circuit
  return 1
}

# --- 4) High-level ask with caching and logging ---
ai_ask() {
  # $1 = user text, $2? = optional system override
  local user_text="$1"
  local system_text="${2:-"You are a helpful assistant for shell and logs."}"

  local prompt_full cache_key result
  prompt_full="$(render_prompt "$user_text")"
  cache_key="$(_hash "$AI_MODEL|$PROMPT_VERSION|$system_text|$prompt_full")"

  if result="$(cache_get "$cache_key")"; then
    printf '%s' "$result"
    return 0
  fi

  result="$(ai_call_raw "$system_text" "$prompt_full")" || return $?

  # Save to cache
  cache_put "$cache_key" "$result"

  # --- 5) Append audit log as JSONL ---
  jq -n --arg t "$(_now)" \
        --arg model "$AI_MODEL" \
        --arg ver "$PROMPT_VERSION" \
        --arg sys "$system_text" \
        --arg usr "$prompt_full" \
        --arg res "$result" \
        '{ts:$t, model:$model, prompt_version:$ver, system:$sys, user:$usr, response:$res}' \
    >> "$AI_LOG_JSONL"

  printf '%s' "$result"
}

# CLI helper: read from args or STDIN
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  if [[ $# -gt 0 ]]; then
    ai_ask "$*"
  else
    # Read entire STDIN as one prompt
    ai_ask "$(cat)"
  fi
fi

Save as ai_patterns.sh and make it executable:

chmod +x ai_patterns.sh

How to use it (real-world examples)

1) Single-shot classification (log severity)

echo "Disk quota exceeded for user alice" | ./ai_patterns.sh
# -> e.g., "ERROR"

2) Quick Q&A (reuse the same helper)

./ai_patterns.sh "In Bash, what's the difference between \$* and \$@?"

3) Batch labeling with parallel (rate-friendly)

  • Ensure parallel is installed (see install commands above).

  • Example: label the first 100 lines of a log file with 2 concurrent workers.

head -n 100 /var/log/syslog \
  | parallel -j2 --line-buffer './ai_patterns.sh "{}"'

Notes:

  • Caching prevents re-billing for repeated inputs.

  • The built-in retries and circuit breaker help when APIs throttle you.

4) Audit and analyze outcomes with jq

  • Inspect the last 3 calls:
tail -n 3 ai_calls.jsonl | jq .
  • Count labels produced
jq -r '.response' ai_calls.jsonl | sort | uniq -c

Pattern recap and why they matter

  • Prompt templates + versioning

    • Value: Makes experiments reproducible and cacheable. Bump PROMPT_VERSION when changing instructions to avoid stale cache hits.
  • Deterministic caching

    • Value: Cuts cost and latency dramatically for repeated inputs. Hash model+prompt+version for stable keys.
  • Retries + backoff + circuit breaker

    • Value: Survive transient 429/5xx errors without spamming the API. Circuit breaker prevents “thundering herd” after sustained failures.
  • Safe batch processing

    • Value: Parallelization speeds up offline jobs while avoiding rate limit spikes. Combine -j with your cache and backoff.
  • Auditable JSONL logs

    • Value: Keep a trail of prompts and outputs to debug regressions, measure quality, and comply with governance requirements.

Installation summary (all cited tools)

  • Debian/Ubuntu:

    sudo apt update
    sudo apt install -y curl jq parallel
    
  • Fedora/RHEL/CentOS:

    sudo dnf install -y curl jq parallel
    
  • openSUSE:

    sudo zypper refresh
    sudo zypper install -y curl jq parallel
    

Conclusion and next steps

You now have a small, production-grade toolkit for AI in Bash:

  • Templates you can iterate on without breaking reproducibility.

  • A cache that keeps costs down.

  • Resilient networking that respects timeouts and rate limits.

  • Batch workflows that scale.

  • A paper trail you can analyze later.

Your move:

  • Drop ai_patterns.sh into your repo, set your OPENAI_API_KEY, and try the examples.

  • Extend the template for your use case (summarization, extraction to JSON, test-case generation).

  • Wire it into cron, CI, or data pipelines—and keep shipping.

If you want a follow-up, ask for a streaming variant, function calling pattern, or a SQLite cache alternative.