- Posted on
- • Artificial Intelligence
Building Artificial Intelligence CLI Tools with Python
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Building Artificial Intelligence CLI Tools with Python (for Linux Bash Users)
What if “using AI” felt like running grep or awk—quick, scriptable, and right in your terminal? In this guide, you’ll build practical AI-powered command‑line tools with Python that integrate cleanly into your existing Bash workflows. We’ll cover both hosted APIs (OpenAI) and local models (Ollama), show you how to package a real CLI, and share patterns that make your tools fast, composable, and safe to automate.
Why AI on the CLI?
It meets you where you work. Bash users already chain small tools into big wins; an AI CLI is just another sharp tool.
It’s script-friendly. Pipe data in, stream answers out, return sensible exit codes.
It’s portable. A small Python CLI can run on any distro you SSH into.
It’s private (if you choose). Local models via Ollama keep data on your machine.
You’ll leave with working examples and a structure you can copy for your own tools (summarizers, commit message generators, code reviewers, changelog writers, and more).
Prerequisites (apt, dnf, zypper)
We’ll need Python, pip/venv, and some helpers (git, curl). Choose your package manager:
Debian/Ubuntu (apt):
sudo apt update sudo apt install -y python3 python3-venv python3-pip git curl build-essential # Optional: pipx for isolated CLI installs sudo apt install -y pipx || python3 -m pip install --user pipx python3 -m pipx ensurepathFedora/RHEL/CentOS (dnf):
sudo dnf install -y python3 python3-pip python3-virtualenv git curl gcc gcc-c++ make # Optional: pipx sudo dnf install -y pipx || python3 -m pip install --user pipx python3 -m pipx ensurepathopenSUSE (zypper):
sudo zypper refresh sudo zypper install -y python3 python3-pip python3-virtualenv git curl gcc gcc-c++ make # Optional: pipx sudo zypper install -y python3-pipx || python3 -m pip install --user pipx python3 -m pipx ensurepath
We’ll also show how to use:
OpenAI’s Python SDK (hosted API)
Ollama (local LLMs)
Install Ollama (Linux):
curl -fsSL https://ollama.com/install.sh | sh
# Then pull a model, e.g.:
ollama pull llama3
Step 1 — Scaffold a Clean Python CLI
We’ll create a small package called ai-cli with a console script entry point ai.
Directory layout:
ai-cli/
pyproject.toml
ai_cli/
__init__.py
cli.py
Create pyproject.toml:
[build-system]
requires = ["hatchling>=1.21.1"]
build-backend = "hatchling.build"
[project]
name = "ai-cli"
version = "0.1.0"
description = "AI-powered command line tools for Linux users"
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
"requests>=2.31.0",
"openai>=1.0.0",
"python-dotenv>=1.0.0"
]
[project.scripts]
ai = "ai_cli.cli:main"
Create ai_cli/cli.py:
#!/usr/bin/env python3
import os, sys, argparse, textwrap, hashlib, json, time, subprocess, pathlib
from typing import Optional, Iterable
CACHE_DIR = pathlib.Path(os.getenv("XDG_CACHE_HOME", pathlib.Path.home() / ".cache")) / "ai-cli"
CACHE_DIR.mkdir(parents=True, exist_ok=True)
def _hash_key(s: str) -> str:
import hashlib
return hashlib.sha256(s.encode("utf-8")).hexdigest()
def _cache_get(key: str) -> Optional[str]:
p = CACHE_DIR / f"{key}.txt"
if p.exists():
return p.read_text(encoding="utf-8")
return None
def _cache_set(key: str, value: str) -> None:
p = CACHE_DIR / f"{key}.txt"
p.write_text(value, encoding="utf-8")
def _read_input(file: Optional[str]) -> str:
if file:
return pathlib.Path(file).read_text(encoding="utf-8")
if not sys.stdin.isatty():
return sys.stdin.read()
raise SystemExit("No input provided. Pass a file or pipe content via stdin.")
def _print_stream(chunks: Iterable[str]) -> str:
buf = []
for chunk in chunks:
buf.append(chunk)
print(chunk, end="", flush=True)
print()
return "".join(buf)
# Backends
def _openai_complete(prompt: str, model: Optional[str], stream: bool) -> Iterable[str] | str:
from openai import OpenAI
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
raise SystemExit("Missing OPENAI_API_KEY in environment.")
client = OpenAI(api_key=api_key)
model = model or os.getenv("OPENAI_MODEL", "gpt-4o-mini")
messages = [{"role": "user", "content": prompt}]
if stream:
resp = client.chat.completions.create(model=model, messages=messages, stream=True)
def gen():
for event in resp:
delta = event.choices[0].delta.content or ""
if delta:
yield delta
return gen()
else:
resp = client.chat.completions.create(model=model, messages=messages)
return resp.choices[0].message.content
def _ollama_complete(prompt: str, model: Optional[str], stream: bool) -> Iterable[str] | str:
import requests
url = os.getenv("OLLAMA_URL", "http://localhost:11434/api/generate")
model = model or os.getenv("OLLAMA_MODEL", "llama3")
payload = {"model": model, "prompt": prompt, "stream": stream}
if stream:
r = requests.post(url, json=payload, stream=True, timeout=600)
r.raise_for_status()
def gen():
for line in r.iter_lines():
if not line:
continue
try:
obj = json.loads(line.decode("utf-8"))
yield obj.get("response", "")
except Exception:
continue
return gen()
else:
r = requests.post(url, json=payload, timeout=600)
r.raise_for_status()
return r.json().get("response", "")
def complete(prompt: str, model: Optional[str] = None, stream: bool = False) -> str:
backend = os.getenv("AI_BACKEND", "ollama").lower()
key = _hash_key(f"{backend}|{model}|{prompt}")
if not os.getenv("AI_NO_CACHE"):
cached = _cache_get(key)
if cached:
return cached
if backend == "openai":
result = _print_stream(_openai_complete(prompt, model, True)) if stream else _openai_complete(prompt, model, False) # type: ignore
elif backend == "ollama":
result = _print_stream(_ollama_complete(prompt, model, True)) if stream else _ollama_complete(prompt, model, False) # type: ignore
else:
raise SystemExit(f"Unknown AI_BACKEND: {backend}")
if isinstance(result, str):
_cache_set(key, result)
return result
else:
out = result # type: ignore
_cache_set(key, out)
return out
def cmd_summarize(args):
text = _read_input(args.file)
sys_prompt = "You are a concise Linux/Bash technical summarizer. Output short bullet points."
prompt = f"{sys_prompt}\n\n=== INPUT START ===\n{text}\n=== INPUT END ==="
res = complete(prompt, model=args.model, stream=args.stream)
if isinstance(res, str):
print(res)
def cmd_ask(args):
prompt = args.prompt or _read_input(None)
res = complete(prompt, model=args.model, stream=args.stream)
if isinstance(res, str):
print(res)
def _git_diff(staged: bool) -> str:
cmd = ["git", "diff", "--staged"] if staged else ["git", "diff"]
try:
out = subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT)
return out.strip()
except subprocess.CalledProcessError as e:
raise SystemExit(e.output)
def cmd_commit(args):
diff = _git_diff(staged=True)
if not diff:
raise SystemExit("No staged changes. Stage files first (git add).")
prompt = textwrap.dedent(f"""
You are a senior developer. Write a clear, conventional commit message
(title: 72 chars max; body: wrapped at 72; include scope/types if obvious)
based on this git diff:
{diff}
""").strip()
res = complete(prompt, model=args.model, stream=args.stream)
if isinstance(res, str):
msg = res.strip()
if args.preview:
print(msg)
else:
# Open editor pre-filled or directly commit
subprocess.run(["git", "commit", "-m", msg])
print("Committed with AI-generated message.")
def cmd_env(_args):
print("AI_BACKEND:", os.getenv("AI_BACKEND", "ollama"))
print("OPENAI_MODEL:", os.getenv("OPENAI_MODEL", "gpt-4o-mini"))
print("OLLAMA_MODEL:", os.getenv("OLLAMA_MODEL", "llama3"))
print("Cache dir:", str(CACHE_DIR))
print("AI_NO_CACHE:", os.getenv("AI_NO_CACHE", ""))
def main():
p = argparse.ArgumentParser(prog="ai", description="AI CLI tools for Linux users")
sub = p.add_subparsers(dest="cmd", required=True)
sp = sub.add_parser("summarize", help="Summarize stdin or a file")
sp.add_argument("file", nargs="?", help="File to summarize (optional, else stdin)")
sp.add_argument("-m", "--model", help="Override model name")
sp.add_argument("-s", "--stream", action="store_true", help="Stream output")
sp.set_defaults(func=cmd_summarize)
ap = sub.add_parser("ask", help="Ask a question (arg or stdin)")
ap.add_argument("prompt", nargs="?", help="Prompt text (optional, else stdin)")
ap.add_argument("-m", "--model", help="Override model name")
ap.add_argument("-s", "--stream", action="store_true", help="Stream output")
ap.set_defaults(func=cmd_ask)
cp = sub.add_parser("commit", help="Generate a commit message from staged diff")
cp.add_argument("-m", "--model", help="Override model name")
cp.add_argument("-s", "--stream", action="store_true", help="Stream output")
cp.add_argument("--preview", action="store_true", help="Print message instead of committing")
cp.set_defaults(func=cmd_commit)
ep = sub.add_parser("env", help="Show effective configuration")
ep.set_defaults(func=cmd_env)
args = p.parse_args()
try:
args.func(args)
except KeyboardInterrupt:
sys.exit(130)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
Make it executable:
chmod +x ai_cli/cli.py
Install locally with pipx (recommended):
cd ai-cli
pipx install .
Or with pip in a venv:
python3 -m venv .venv
. .venv/bin/activate
pip install .
Step 2 — Choose a Model Backend (OpenAI or Ollama)
You can switch at runtime with environment variables.
Use OpenAI (hosted):
export AI_BACKEND=openai export OPENAI_API_KEY=sk-... export OPENAI_MODEL=gpt-4o-mini # or another available modelUse Ollama (local, private, free to run):
export AI_BACKEND=ollama export OLLAMA_MODEL=llama3 # or mistral, codellama, etc. # optional custom URL if remote: # export OLLAMA_URL=http://remote-host:11434/api/generate
Tip: Save these in your shell profile or use a .env file:
AI_BACKEND=ollama
OLLAMA_MODEL=llama3
Load with:
python -m pip install --user python-dotenv
# or just `source .env` in Bash
Optional: Install direnv to auto-load per-directory env:
apt:
sudo apt install -y direnvdnf:
sudo dnf install -y direnvzypper:
sudo zypper install -y direnvThen
echo 'eval "$(direnv hook bash)"' >> ~/.bashrcand add.envrc.
Step 3 — Real-World Examples You Can Use Today
Once installed, try:
1) Summarize logs or docs:
journalctl -u ssh.service --no-pager | tail -n 200 | ai summarize -s
Or:
ai summarize README.md
2) Generate a commit message:
git add -A
ai commit --preview
# If you like it:
ai commit
3) Ask ad-hoc questions (great in scripts):
echo "Explain Linux cgroups v2 in 5 bullets for a sysadmin." | ai ask -s
These commands are pipe-friendly, support streaming, and cache results to avoid re-billing or re-computing identical prompts. Disable cache per call:
AI_NO_CACHE=1 ai summarize myfile.txt
Step 4 — Package, Distribute, and Keep Secrets Safe
Secrets:
- Never hardcode keys. Use env vars (
OPENAI_API_KEY) or.envfiles ignored by git. - Consider per-project env via direnv.
- Never hardcode keys. Use env vars (
Packaging:
- We used
pyproject.tomlwith a console script, soaiis on your PATH afterpipx install .. - To share, publish to a git repo and install directly:
pipx install git+https://github.com/yourname/ai-cli- Or build a wheel:
python -m build pipx install dist/ai_cli-0.1.0-py3-none-any.whl- We used
Version pinning:
- In
pyproject.toml, pin major versions for stability (already done with>=minimums; you can lock exact versions if desired).
- In
Step 5 — Polish for Production (UX, Errors, Performance)
Streaming output:
- Already implemented with
-s/--streamto get tokens incrementally—feels snappy in the terminal.
- Already implemented with
Exit codes:
- Non-zero on error, 130 on Ctrl-C—so scripts can detect failures.
Caching:
- Simple SHA-256 cache in
~/.cache/ai-cli. Toggle withAI_NO_CACHE=1for one-offs.
- Simple SHA-256 cache in
Timeouts and retries:
- Requests to Ollama include a timeout; for APIs, consider exponential backoff if you’re batching tasks.
Safety defaults:
- Summarizer uses a clear system prompt for concise, bullet output—tune prompts for predictable formatting in pipelines.
Observability:
- Wrap calls with timestamps or write simple logs if you’re running many automated invocations.
Optional: Installing Dev Dependencies via Your Package Manager
If you plan to hack further:
Ubuntu/Debian (apt):
sudo apt install -y ripgrep jqFedora (dnf):
sudo dnf install -y ripgrep jqopenSUSE (zypper):
sudo zypper install -y ripgrep jq
These help when wiring CLIs into Bash pipelines.
Quick Reference Commands
Switch backends:
export AI_BACKEND=openai # or ollamaSet models:
export OPENAI_MODEL=gpt-4o-mini export OLLAMA_MODEL=llama3Run:
echo "Summarize this long text" | ai summarize -s ai ask "Write a one-liner bash command to find the 10 largest files" git add -A && ai commit --preview
Conclusion / Call to Action
You now have a practical, extensible pattern for AI in the terminal:
A simple Python CLI that reads stdin, streams results, and plays nicely with Bash.
Pluggable backends (OpenAI for hosted convenience; Ollama for local, private inference).
Real commands you can drop into daily workflows.
Next steps:
Add new subcommands:
ai review(PR feedback),ai explain(explain shell pipelines),ai changelog.Share your CLI with your team via a git repo and
pipx install.Benchmark local vs hosted models for your use cases and tune prompts for consistent, machine-readable outputs.
If this unlocked ideas, turn one into a subcommand today—your future self at 2 a.m. debugging prod will thank you.