Source: docs/assistant.md
Text assistant
Run the herd by text message. Ask "who's out?" or "move them to North Thursday 6am" from any phone; changes always wait for a YES. A plain summary arrives every morning.
The code is in src/herdtag/server/assistant.py. It loads automatically with the server.
How a message is handled
-
Built-in commands first, no AI involved:
Text Does STATUSorSUMMARYThe herd summary (same as the morning text) YES(or the 4-digit code)Runs the change waiting for this phone, if it's under 15 minutes old NO/CANCELCancels the waiting change UNDOReverses the last pause, resume, start or scheduled move from this phone, within an hour HELPWhat it can do -
Anything else goes to Claude with the last 10 messages from that phone and these tools:
Tool Kind herd_statusRead: counts by state, who's out, not heard from, low batteries, fences not confirmed recent_eventsRead: alerts from the last N hours grazing_statusRead: paddocks, rotations, next move, moves scheduled by text list_fencesRead: hand-drawn fences rotation_actionChange: pause, resume, move-now, close-gate, stop start_rotationChange schedule_actionChange: run pause / resume / move-now / stop at a future local time send_fenceChange: send an existing fence to some or all tags
Safety rules
- Changes never run from the model. A change tool only queues a pending action. The reply is fixed text, "Pause Spring? Reply YES to confirm or NO to cancel.", whatever the model wrote, so it can't claim something happened.
- Only the phone that asked can confirm, within 15 minutes. A newer request replaces an older one.
- Every executed change is logged as a
herdevent of kindassistant_action, with the last 4 digits of the phone. - Allowlist. Texts from numbers not on the list get no reply and are logged as
assistant_ignored. - Twilio signature check whenever
TWILIO_AUTH_TOKENis set. Unsigned or forged posts get403. - The web chat page (
/assistant) skips the allowlist. It's part of the dashboard, which has no login yet, so keep the server private (see Backend).
Setup
- Claude. Set
ANTHROPIC_API_KEY. The SDK also picks up other credential sources, such asant auth login. Without credentials the built-in commands still work. - Allowlist. Add phones with the full country code:
curl -X POST localhost:8000/api/assistant/phones -H 'content-type: application/json' \ -d '{"phone": "+15551234567", "name": "Dale"}'Or setHERDTAG_ALLOWED_PHONES=+15551234567,+15557654321. - Twilio number. Buy an SMS-capable number. Under Messaging → A message comes in, set Webhook, HTTP POST to
https://<your public host>/sms/twilio. Behind a tunnel or proxy, setHERDTAG_PUBLIC_URL=https://<your public host>so the signature check uses the URL Twilio signed. - Outbound texts (morning summary, scheduled-action results): set
TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN,TWILIO_FROM. Without them, texts are stored with statusdry_run; seeGET /api/assistant/outbox. - US numbers need A2P 10DLC registration with Twilio before carriers deliver texts reliably.
| Env var | Default | Meaning |
|---|---|---|
HERDTAG_ASSISTANT_MODEL |
claude-sonnet-5-5 |
Model for free-text messages |
HERDTAG_ASSISTANT_FALLBACKS |
1 |
Server-side refusal fallback (fallbacks: "default") on models that support it; 0 turns it off |
HERDTAG_SUMMARY_HOUR |
6 |
Local hour for the morning text |
HERDTAG_ALLOWED_PHONES |
Comma-separated E.164 numbers, added to the phones table | |
HERDTAG_PUBLIC_URL |
Public base URL, for the Twilio signature check | |
HERDTAG_NO_SCHEDULER |
Set to skip the background thread (tests) |
The background thread runs every 30 s. It runs scheduled actions that are due, sends the morning summary once a day to every allowed phone, and expires stale confirmations.
API
| Method | Path | Notes |
|---|---|---|
| POST | /sms/twilio |
Twilio inbound webhook (form-encoded From, Body); replies with TwiML |
| POST | /api/assistant/chat |
{phone?, text} → {phone, reply}; same logic as SMS, no allowlist |
| GET | /api/assistant/summary |
{text}: the morning summary right now |
| GET / POST / DELETE | /api/assistant/phones, /api/assistant/phones/{phone} |
Manage the allowlist |
| GET | /api/assistant/outbox |
Outbound texts and their status (sent, dry_run, error) |
| GET | /api/assistant/pending |
Recent confirmation requests and what happened to them |
| GET | /assistant |
Web chat page for trying it without a phone |
Cost
Each free-text message is one short Claude conversation: usually 2 calls (a lookup, then the answer) at low effort. With Claude Sonnet 5.5 ($2 / $10 per million input / output tokens), that's roughly a cent or two per message; check the usage page. STATUS, YES, NO, UNDO and the morning summary use no AI. Twilio charges per message segment; replies are capped at 480 characters.
Try it
uv run herdtag-server
uv run herdtag demo --rotation 0.05 # optional: a live herd to ask about
open http://127.0.0.1:8000/assistant
Not yet tested
- Real Twilio delivery and signatures from Twilio's servers. The signature algorithm is tested against our own implementation of Twilio's documented scheme.
- Real Claude API calls. Tests use a scripted fake client, so wording and tool choice from the real model haven't been checked.
- How the model handles relative dates ("Thursday") near midnight or across time zones. The server's local time is the farm's time.