AI Agents#

The agents package (pkgs/agents/) provides a suite of CLI tools for personal data archeology — making sense of decades of accumulated files (.txt, .rtf, .eml) using local Ollama inference.

All agents are available as flake apps and as installable binaries.

Running#

# Via flake app (no install needed)
nix run .#triage -- ~/Documents > results.jsonl
nix run .#summarize -- somefile.txt

# Or install via home-manager / nix profile
nix profile install .#agents

Environment variables#

Variable Default Description
OLLAMA_HOST http://localhost:11434 Ollama base URL
OLLAMA_MODEL qwen2.5:3b LLM model to use

Agents#

triage — batch analysis#

The primary tool for working through large collections. One LLM call per file, outputs JSONL (one JSON object per line).

triage ~/Dropbox/Personal > results.jsonl

Each line contains:

{
  "file":      "/path/to/file.txt",
  "class":     "financial",
  "score":     4,
  "reason":    "W-2 tax document from 2003",
  "summary":   "Annual wage statement from Acme Corp...",
  "people":    ["John Doe"],
  "dates":     ["2003-01-31"],
  "orgs":      ["Acme Corp"],
  "amounts":   ["$52,400"],
  "sensitive": false
}

Score scale: 1=junk/spam · 2=low · 3=moderate · 4=important · 5=critical

Useful jq filters on the output:

# Files worth keeping
jq -r 'select(.score >= 4) | .file' results.jsonl

# Files that may contain PII
jq -r 'select(.sensitive) | .file' results.jsonl

# All financial documents
jq -r 'select(.class == "financial") | .file' results.jsonl

# TSV for spreadsheet import
jq -r '[.file, .class, (.score|tostring), .reason] | @tsv' results.jsonl > triage.tsv

# Deletion candidates
jq -r 'select(.score <= 2) | .file' results.jsonl

summarize — human-readable summary#

3–5 sentence summary of one or more files. Accepts files and/or directories (scans for .txt/.rtf/.eml at depth 1).

summarize letter.txt
summarize ~/Dropbox/Personal/Correspondence > summaries.txt

classify — single category word#

Outputs one of: legal · financial · medical · employment · correspondence · technical · junk

classify document.txt        # → "financial"

Useful for routing in shell pipelines:

case $(classify "$file") in
  legal)       process-legal    "$file" ;;
  financial)   process-financial "$file" ;;
  *)           summarize        "$file" ;;
esac

extract — structured entity extraction#

Outputs JSON with people, organizations, dates, places, amounts, and a sensitive flag.

extract contract.txt
{
  "people":        ["Jane Smith", "Robert Jones"],
  "organizations": ["First National Bank"],
  "dates":         ["2001-03-15"],
  "places":        ["Indianapolis, IN"],
  "amounts":       ["$150,000"],
  "sensitive":     true
}

importance — archival score#

Outputs a 1–5 score and a one-line reason. Useful for deciding what to keep before a bulk triage run.

importance old-receipt.txt
{"score": 2, "reason": "Retail receipt for minor purchase, no legal significance"}

Supported file formats#

Extension Handling
.txt, .TXT cat
.rtf, .RTF unrtf --text
.eml Python email module (extracts From/Date/Subject + text body)

All content is truncated to ~3000 characters before sending to the LLM.

Adding a new agent#

  1. Add a writeShellScriptBin block in pkgs/agents/default.nix
  2. Source "$AGENTS_LIB" at the top — this gives you read_file, collect_files, and ollama_chat
  3. Add the binary name to the for bin in … loop in extraSetup
  4. Add it to allScripts
  5. Add a flake app entry in modules/flake/packages/default.nix

Minimal template:

my-agent = pkgs.writeShellScriptBin "my-agent" ''
  set -euo pipefail
  source "$AGENTS_LIB"
  SYSTEM='Your persona / instructions here. Output format constraints go here.'
  [[ $# -lt 1 ]] && { echo "usage: my-agent <file>" >&2; exit 1; }
  content=$(read_file "$1" | head -c 3000)
  ollama_chat "$SYSTEM" "$content"
'';

Prompt design at 3b scale#

qwen2.5:3b is capable but small. Reliability comes from constraint:

  • Force output format — single word, JSON schema, or CSV. Never open-ended prose.
  • One task per call — don’t ask for classification AND extraction in the same prompt unless you’re OK with occasional failures (as triage does for speed).
  • Short context — 3000 chars (~750 words) is enough for metadata extraction. Fighting the context limit produces worse results than working within it.
  • Explicit schema — include the JSON schema in the system prompt, not the user message. Models follow system-prompt constraints more reliably.