Posted on
Artificial Intelligence

Artificial Intelligence Customer Support Workflows

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

Build AI-Powered Customer Support Workflows from Bash

Tickets piling up? SLAs slipping? You don’t need a full-blown microservice or an expensive platform to start using AI for customer support. With a few Unix tools and a reliable AI API, you can triage, summarize, and draft replies straight from Bash. This post shows you how to assemble a practical, auditable, and automatable workflow using curl, jq, and sqlite3.

Why this matters:

  • Faster, more consistent triage and routing

  • Summaries agents can trust and verify

  • Draft responses that save minutes per ticket

  • All in CLI pipelines you can schedule, log, and version-control


What you’ll build

  • A portable Bash toolkit to:
    • Classify and prioritize tickets (intent + severity)
    • Summarize conversations for instant context
    • Generate compliant reply drafts for agent review
    • Auto-respond to simple FAQs with safe-guards

All examples assume an OpenAI-compatible API (any provider that implements /v1/chat/completions). You control where data goes by choosing the endpoint.


Prerequisites (Linux)

You’ll need curl, jq, and sqlite3. Install with your distro’s package manager:

  • Debian/Ubuntu (apt):

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

    sudo dnf install -y curl jq sqlite
    
  • openSUSE/SLE (zypper):

    sudo zypper refresh
    sudo zypper install -y curl jq sqlite3
    

Set API configuration via environment variables (for any OpenAI-compatible service):

export OPENAI_API_KEY="your_api_key_here"
export BASE_URL="https://api.openai.com"   # or your compatible endpoint
export MODEL="gpt-4o-mini"                 # any available model slug

Optional: put these in a .env and source .env before running scripts.


Core shell: AI request helper

Create ai.sh to standardize calls and make parsing easy.

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

: "${OPENAI_API_KEY:?Set OPENAI_API_KEY}"
BASE_URL="${BASE_URL:-https://api.openai.com}"
MODEL="${MODEL:-gpt-4o-mini}"
TEMPERATURE="${TEMPERATURE:-0.2}"

# ai_json: send system+user prompts and expect JSON output
ai_json() {
  local system="$1"
  local user="$2"
  curl -sS "${BASE_URL}/v1/chat/completions" \
    -H "Authorization: Bearer ${OPENAI_API_KEY}" \
    -H "Content-Type: application/json" \
    -d "$(jq -n \
      --arg m "$MODEL" \
      --arg s "$system" \
      --arg u "$user" \
      --argjson t "$TEMPERATURE" \
      '{
        model: $m,
        temperature: $t,
        response_format: { type: "json_object" },
        messages: [
          {role:"system", content:$s},
          {role:"user", content:$u}
        ]
      }')" \
  | jq -r '.choices[0].message.content'
}

Make it executable:

chmod +x ai.sh

Step 1 — Create a tiny ticket store (SQLite)

We’ll keep structured metadata in SQLite so you can query, automate, and audit changes.

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

DB="${DB:-support.db}"

sqlite3 "$DB" <<'SQL'
CREATE TABLE IF NOT EXISTS tickets (
  id INTEGER PRIMARY KEY,
  subject TEXT NOT NULL,
  body TEXT NOT NULL,
  intent TEXT,
  priority TEXT,
  sentiment TEXT,
  status TEXT DEFAULT 'new',
  last_action TEXT,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
SQL

Save as init_db.sh and run:

./init_db.sh

Insert a sample ticket:

sqlite3 support.db \
  "INSERT INTO tickets(subject, body) VALUES('Login issue', 'I can’t reset my password and the reset link never arrives. My email is alice@example.com');"

Step 2 — Redact PII before sending to AI

Minimize exposure by scrubbing common PII. This example replaces emails and phone numbers. Tweak as needed.

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

redact() {
  sed -E \
    -e 's/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/[REDACTED_EMAIL]/g' \
    -e 's/\+?[0-9][0-9 .-]{7,}[0-9]/[REDACTED_PHONE]/g'
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  cat | redact
fi

Save as redact.sh and make executable:

chmod +x redact.sh

Step 3 — Triage: intent + priority classification

Classify each ticket into an intent and priority so you can route or queue intelligently.

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

. ./ai.sh

DB="${DB:-support.db}"

SYSTEM="You are a support triage assistant. Classify customer tickets.

- intents: password_reset, billing, bug_report, how_to, cancellation, other

- priority: low, medium, high, urgent
Rules:

- Be conservative with 'urgent' unless service is down or security risk.

- Output ONLY JSON with keys: intent, priority."

triage_ticket() {
  local subject="$1"
  local body="$2"
  local redacted
  redacted="$(printf '%s\n\n%s\n' "$subject" "$body" | ./redact.sh)"
  local user="Subject: ${subject}
Body:
${redacted}"

  ai_json "$SYSTEM" "$user"
}

# Process the latest 'new' ticket
row="$(sqlite3 "$DB" -json "SELECT id, subject, body FROM tickets WHERE status='new' ORDER BY id ASC LIMIT 1;")"
[[ -z "$row" || "$row" = "[]" ]] && { echo "No new tickets."; exit 0; }

id="$(echo "$row" | jq -r '.[0].id')"
subject="$(echo "$row" | jq -r '.[0].subject')"
body="$(echo "$row" | jq -r '.[0].body')"

result="$(triage_ticket "$subject" "$body")"
intent="$(echo "$result" | jq -r '.intent')"
priority="$(echo "$result" | jq -r '.priority')"

sqlite3 "$DB" <<SQL
UPDATE tickets
SET intent = '$intent',
    priority = '$priority',
    status = 'triaged',
    last_action = 'triage',
    updated_at = CURRENT_TIMESTAMP
WHERE id = $id;
SQL

echo "Ticket #$id triaged: intent=$intent priority=$priority"

Save as triage.sh, then:

chmod +x triage.sh
./triage.sh

Step 4 — Summarize for agents (context + sentiment)

Give agents a fast, consistent brief they can trust.

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

. ./ai.sh

DB="${DB:-support.db}"

SYSTEM="You are a support summarizer. Produce:

- tldr: one-sentence summary

- sentiment: positive, neutral, negative

- key_points: 3 bullet points
Output JSON with keys: tldr, sentiment, key_points (array)."

row="$(sqlite3 "$DB" -json "SELECT id, subject, body FROM tickets WHERE status='triaged' ORDER BY updated_at DESC LIMIT 1;")"
[[ -z "$row" || "$row" = "[]" ]] && { echo "No triaged tickets."; exit 0; }

id="$(echo "$row" | jq -r '.[0].id')"
subject="$(echo "$row" | jq -r '.[0].subject')"
body="$(echo "$row" | jq -r '.[0].body')"

redacted="$(printf '%s\n\n%s\n' "$subject" "$body" | ./redact.sh)"
user="Subject: ${subject}
Body:
${redacted}
"

result="$(ai_json "$SYSTEM" "$user")"
tldr="$(echo "$result" | jq -r '.tldr')"
sentiment="$(echo "$result" | jq -r '.sentiment')"

sqlite3 "$DB" <<SQL
UPDATE tickets
SET sentiment = '$sentiment',
    last_action = 'summarize',
    updated_at = CURRENT_TIMESTAMP
WHERE id = $id;
SQL

echo "Summary for ticket #$id:"
echo "$result" | jq .

Save as summarize.sh and run:

chmod +x summarize.sh
./summarize.sh

Step 5 — Draft replies with a style guide (human-in-the-loop)

Generate a safe, compliant draft that an agent can review and edit before sending.

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

. ./ai.sh

DB="${DB:-support.db}"
EDITOR="${EDITOR:-vi}"

STYLE_GUIDE="Write in a friendly, concise tone.

- Never promise refunds or SLAs.

- If unsure, ask a clarifying question.

- Provide exact, numbered steps if procedural.

- Keep to 150–200 words unless complex.

- Do not include PII; use placeholders."

SYSTEM="You are a support agent drafting replies. Follow the style guide strictly. Output plain text."

row="$(sqlite3 "$DB" -json "SELECT id, subject, body, intent FROM tickets WHERE status='triaged' ORDER BY priority DESC, updated_at ASC LIMIT 1;")"
[[ -z "$row" || "$row" = "[]" ]] && { echo "No tickets ready for draft."; exit 0; }

id="$(echo "$row" | jq -r '.[0].id')"
subject="$(echo "$row" | jq -r '.[0].subject')"
body="$(echo "$row" | jq -r '.[0].body')"
intent="$(echo "$row" | jq -r '.[0].intent')"

redacted="$(printf '%s\n\n%s\n' "$subject" "$body" | ./redact.sh)"
user="Subject: ${subject}
Intent: ${intent}
Customer message (PII-redacted):
${redacted}

Draft a helpful reply. Include steps if relevant. End with an open question to confirm resolution."

draft="$(curl -sS "${BASE_URL}/v1/chat/completions" \
  -H "Authorization: Bearer ${OPENAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg m "${MODEL:-gpt-4o-mini}" --arg s "$SYSTEM" --arg g "$STYLE_GUIDE" --arg u "$user" '{
        model: $m, temperature: 0.4,
        messages: [
          {role:"system", content:$s},
          {role:"system", content:("Style Guide:\n" + $g)},
          {role:"user", content:$u}
        ]
      }')" \
  | jq -r '.choices[0].message.content')"

tmp="$(mktemp)"
printf "%s\n" "$draft" > "$tmp"
${EDITOR} "$tmp"

final="$(cat "$tmp")"
rm -f "$tmp"

printf "\n--- Final Draft ---\n%s\n" "$final"

sqlite3 "$DB" <<SQL
UPDATE tickets
SET status = 'drafted',
    last_action = 'draft_reply',
    updated_at = CURRENT_TIMESTAMP
WHERE id = $id;
SQL

echo "Draft saved for ticket #$id."

Save as draft_reply.sh, then:

chmod +x draft_reply.sh
./draft_reply.sh

You now have a repeatable human-in-the-loop drafting flow that respects a style guide and prevents risky promises.


Bonus — Safe auto-responses for common FAQs

If intent is password_reset, you might automate a polite, personalized response. Keep it simple and templated; only use AI to tailor tone if you’re comfortable.

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

DB="${DB:-support.db}"

row="$(sqlite3 "$DB" -json "SELECT id, subject FROM tickets WHERE status='triaged' AND intent='password_reset' ORDER BY updated_at ASC LIMIT 1;")"
[[ -z "$row" || "$row" = "[]" ]] && { echo "No password reset tickets."; exit 0; }

id="$(echo "$row" | jq -r '.[0].id')"
subject="$(echo "$row" | jq -r '.[0].subject')"

response="Hi there,

Thanks for reaching out about password access.

Please try these steps:
1) Visit the Password Reset page and submit your account email.
2) Check your spam/junk folder for the reset link.
3) If you don’t receive a link within 10 minutes, reply here and we’ll assist further.

Best regards,
Support Team"

printf "\n--- Auto-response for ticket #%s (%s) ---\n%s\n" "$id" "$subject" "$response"

sqlite3 "$DB" <<SQL
UPDATE tickets
SET status = 'awaiting_customer',
    last_action = 'auto_respond_password_reset',
    updated_at = CURRENT_TIMESTAMP
WHERE id = $id;
SQL

Save as auto_respond.sh and run:

chmod +x auto_respond.sh
./auto_respond.sh

You can later swap the static template with an AI-personalized version that still adheres to your style guide and guardrails.


Why this approach works

  • Bash-native: Compose, log, and schedule with cron or systemd; integrate with existing scripts.

  • Observable: Every step writes to SQLite; you can audit who did what, when.

  • Privacy-aware: Redaction before API calls. You can add more deterministic filters.

  • Portable: curl, jq, sqlite3 are available across major distros.

  • Vendor-flexible: Works with any OpenAI-compatible endpoint by changing BASE_URL and model.


Real-world tips

  • Rate-limiting: Add sleep or a token-bucket when looping over many tickets.

  • Backoff on errors: Wrap curl with retry logic (e.g., --retry 5 --retry-delay 2).

  • System prompts as config: Store style guides and rules in version-controlled files.

  • Guardrails: For auto-actions, prefer templates + minimal AI; always log final content.

  • Access control: Keep your API key in a restricted .env and never commit it.


Next steps (CTA)

  • Wire these scripts into your helpdesk API to ingest new tickets and post replies.

  • Add a systemd timer to run triage.sh and summarize.sh every few minutes.

  • Expand intents and templates; implement escalation rules by priority.

  • Add tests with shellcheck and sample fixtures to keep prompts stable as you iterate.

If you want a minimal, auditable, and automation-friendly way to bring AI into customer support, start with Bash. The examples above are enough to pilot real improvements this week.