Rebuild Piggy's interface, and give the demo book a business to describe
Piggy answered in raw markdown, threw away every tool result it streamed, and fought the reader's scroll on every token. The three surfaces that made it worth having — what it read, how it reasoned, what it cost — were all on the wire and none of them reached the screen. The transcript is now composed of five parts under components/piggy: answers render through streamdown, the container sticks to the bottom without pinning the reader there, tool steps say what they read and link to the record, and each turn carries its model and token count. Three lifecycle bugs went with them: Stop left a permanent spinner, a truncated stream was indistinguishable from thinking, and a failed send destroyed the message it failed to send. Underneath, the inference path grew timeouts, jittered retries on 429 and 5xx, tolerance of the malformed frames a 30B model emits, and an agent_runs row per turn so chat spend is observable. The system prompt now states that a field ending in Cents is cents — without it nemotron renders costPerGpuHourCents: 189 as "$189 per GPU-hour", which is a 100x error on the most scrutinised number in the room. The demo book was arithmetically incoherent: every deal's value contradicted its own allocation revenue by up to 3.6x, nothing had ever closed, no customer had any paper, and the marketplace was empty. Deal value is now derived from the allocation, the book clears 5.3% across five blocks with one deliberately underwater, and the renewal, compliance and agent-provenance machinery finally has rows to act on. A --clear that deleted every obligation, SLA term and capacity request in the database regardless of origin is scoped to the demo's own ids. Around that: accounts have a detail page, ⌘K searches the book, Settings can mint the API keys it always claimed to, and deploy.sh actually ships the agent instead of silently skipping its compose profile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+122
-20
@@ -1,8 +1,8 @@
|
||||
# Deploying PIG
|
||||
|
||||
PIG is an API/web container, a private Piggy worker/chat container and Postgres,
|
||||
behind any reverse proxy that terminates TLS. Nothing here is specific to a
|
||||
particular host.
|
||||
PIG is an API/web container, Postgres, and — when you ask for it — a private
|
||||
Piggy worker/chat container, behind any reverse proxy that terminates TLS.
|
||||
Nothing here is specific to a particular host.
|
||||
|
||||
## 1. DNS
|
||||
|
||||
@@ -28,11 +28,11 @@ The values that must be set for a production start:
|
||||
| `PIG_PUBLIC_URL` | The single origin the app is served from |
|
||||
| `SUPABASE_URL` / `SUPABASE_ANON_KEY` | Authentication. The app refuses to start in production without a Supabase URL, because it would otherwise serve the whole CRM unauthenticated |
|
||||
| `PIG_ADMIN_EMAILS` | Who may administer. **Every address here must already have an account** — an unregistered address listed as an admin is a standing offer of admin rights to whoever claims it first |
|
||||
| `PIGGY_INFERENCE_API_KEY` | Model credential held only by the Piggy process |
|
||||
| `PIGGY_INTERNAL_TOKEN` | A generated 32+ character bearer token shared only by API and Piggy |
|
||||
| `PIG_SETTINGS_ENCRYPTION_KEY` | Base64-encoded 32 bytes (`openssl rand -base64 32`). Only needed for the Notion and Google OAuth secrets typed into the admin UI, which the API refuses to store without it |
|
||||
|
||||
Optional: `PRIME_API_KEY` (scope it to `Availability → Read` only),
|
||||
`PIGGY_ENABLED`, and the Slack and Buzz credentials.
|
||||
Optional: `PRIME_API_KEY` (scope it to `Availability → Read` only), the Slack
|
||||
and Buzz credentials, and the whole Piggy block — the agent is off unless you
|
||||
[turn it on](#turning-piggy-on).
|
||||
|
||||
Piggy listens on `piggy:8931` inside the Compose network. The port is exposed to
|
||||
other containers but never published to the host, and Caddy must not route to
|
||||
@@ -57,11 +57,10 @@ have a container restarting every few seconds and no way in. `run --rm
|
||||
dependencies twice. This is what commit d4d7095 changed and it is what
|
||||
`scripts/deploy.sh` does.
|
||||
|
||||
When Piggy is enabled, start its private profile as well:
|
||||
|
||||
```bash
|
||||
docker compose -p pig --profile piggy up -d --build
|
||||
```
|
||||
That starts the CRM alone: the agent is behind a Compose profile and stays
|
||||
down. To run it, see [Turning Piggy on](#turning-piggy-on) — set the switch in
|
||||
`.env` rather than starting the container by hand, because a hand-started Piggy
|
||||
is one no later deploy knows to upgrade.
|
||||
|
||||
Use `-p pig`. A compose project that shares a name with a neighbouring stack
|
||||
will adopt its volumes, which is a memorable way to lose a database.
|
||||
@@ -111,6 +110,100 @@ satisfies every check that only asks whether something responded.
|
||||
`scripts/deploy.sh` now asserts the body is non-empty and contains the
|
||||
application's mount point for this reason.
|
||||
|
||||
## Turning Piggy on
|
||||
|
||||
Piggy is the in-app agent: a queue worker and a private chat server, one image
|
||||
running a second command. It is **off by default** and nothing about it is
|
||||
configurable from the admin UI — every value below is read once, when the
|
||||
container boots.
|
||||
|
||||
Three keys in `.env`, and all three are needed:
|
||||
|
||||
```bash
|
||||
PIGGY_ENABLED=true
|
||||
PIGGY_INFERENCE_API_KEY=<an inference key from app.primeintellect.ai>
|
||||
PIGGY_INTERNAL_TOKEN=<openssl rand -hex 32>
|
||||
```
|
||||
|
||||
The fourth thing the API needs, `PIGGY_INTERNAL_URL`, is already set to
|
||||
`http://piggy:8931` by `docker-compose.yml`. Set it in `.env` only for a Piggy
|
||||
running outside Compose.
|
||||
|
||||
Then deploy as usual:
|
||||
|
||||
```bash
|
||||
bash scripts/deploy.sh
|
||||
```
|
||||
|
||||
**Do not start the container by hand.** The service carries
|
||||
`profiles: ['piggy']`, and compose skips a profile-gated service *silently*:
|
||||
without the profile, `pull`, `build` and `up` behave as though it were not in
|
||||
the file, with no warning and a zero exit.
|
||||
`deploy.sh` reads `PIGGY_ENABLED` from `.env` and adds `--profile piggy`
|
||||
to the pull, the build, the `up` **and the rollback**, so the agent moves with
|
||||
the app. A Piggy started once with `--profile piggy up -d` and then forgotten is
|
||||
not covered by any of that: it keeps running the image of the day it was
|
||||
started, against a schema several migrations newer, which is the failure the
|
||||
comment at the top of `docker-compose.yml` is about. `deploy.sh` therefore
|
||||
compares the piggy container's image ID with the release's and rolls back if
|
||||
they differ.
|
||||
|
||||
`scripts/autodeploy.sh` compares both containers' digests for the same reason.
|
||||
Without that, an old Piggy is invisible to the poller: the app matches the
|
||||
newest tag, the poller says "up to date" every five minutes, and the agent runs
|
||||
last month's code indefinitely.
|
||||
|
||||
### A missing API key crash-loops the worker
|
||||
|
||||
`PIGGY_INFERENCE_API_KEY` is required by `apps/piggy/src/config.ts`. Without it
|
||||
the process exits at boot with `Invalid Piggy configuration:
|
||||
PIGGY_INFERENCE_API_KEY is required.`, and `restart: unless-stopped` starts it
|
||||
again — so the symptom is a container restarting every few seconds, not an error
|
||||
anyone sees in the CRM. `PIGGY_INTERNAL_TOKEN` shorter than 32 characters fails
|
||||
the same way. `deploy.sh` catches both: it waits for the container to report
|
||||
healthy and exits 3 if it does not, deliberately **without** rolling back,
|
||||
because the previous image reads the same `.env` and would fail identically.
|
||||
|
||||
### Health
|
||||
|
||||
The piggy container has its own healthcheck, against the chat server's
|
||||
unauthenticated `GET /internal/health`:
|
||||
|
||||
```bash
|
||||
docker compose -p pig --profile piggy ps
|
||||
# NAME STATUS
|
||||
# pig-piggy-1 Up 2 minutes (healthy)
|
||||
|
||||
docker compose -p pig --profile piggy exec piggy \
|
||||
node -e "fetch('http://127.0.0.1:8931/internal/health').then(r=>r.text()).then(console.log)"
|
||||
# {"ok":true,"service":"piggy-chat","model":"nvidia/nemotron-3-nano-30b-a3b"}
|
||||
```
|
||||
|
||||
It needs its own because the image's `HEALTHCHECK` asks for
|
||||
`127.0.0.1:8920/api/health` — the API's port, which this container does not
|
||||
serve. Inherited unchanged, Piggy reported `unhealthy` for ever while working
|
||||
perfectly.
|
||||
|
||||
There is no published port and there must not be one. The listener is reachable
|
||||
only from inside the Compose network, the API authenticates the user before
|
||||
forwarding anything, and the bearer token goes in a header — never a query
|
||||
string, where a proxy or an access log would keep it.
|
||||
|
||||
### Tuning
|
||||
|
||||
Everything else has a working default and exists to be lowered:
|
||||
`PIGGY_MAX_TOKENS` (per queued task), `PIGGY_CHAT_MAX_TOKENS` (per interactive
|
||||
answer), `PIGGY_MAX_TURNS`, `PIGGY_POLL_INTERVAL_MS`, `PIGGY_LEASE_SECONDS`,
|
||||
`PIGGY_REASONING_EFFORT` and the two `PIGGY_PRICE_*_CENTS_PER_MTOK` values that
|
||||
make the cost recorded against each run exact. `.env.example` lists them
|
||||
commented out, and that is not decoration: an empty `PIGGY_MAX_TOKENS=` line is
|
||||
passed to the container as the empty string, which coerces to 0 and refuses to
|
||||
start. Leave a key commented to get its default; do not leave it blank.
|
||||
|
||||
Changing any of them means restarting the container — `bash scripts/deploy.sh`,
|
||||
or `docker compose -p pig --profile piggy up -d piggy` if the release is
|
||||
otherwise unchanged.
|
||||
|
||||
## Learn videos
|
||||
|
||||
PIG hosts its own Learn videos. There is no video service to configure, no
|
||||
@@ -219,8 +312,9 @@ bash scripts/deploy.sh
|
||||
```
|
||||
|
||||
It fetches `origin/main`, dumps the database, builds, migrates from a one-off
|
||||
container, starts the app, and refuses to call the deploy done until the health
|
||||
endpoint, the unauthenticated-401 gate and the public origin all agree.
|
||||
container, starts the app — and Piggy, when `PIGGY_ENABLED` is on — and refuses
|
||||
to call the deploy done until the health endpoint, the unauthenticated-401 gate,
|
||||
the piggy container's image and health, and the public origin all agree.
|
||||
|
||||
### By tag — the normal path
|
||||
|
||||
@@ -256,16 +350,19 @@ without it and compose interpolates the `pig:local` fallback from
|
||||
if it had not died, the migrate, the `up` and the rollback would all have run
|
||||
the stale local image while the log named the release tag. Every compose
|
||||
invocation in `deploy.sh` therefore goes through the `dc()` wrapper, which uses
|
||||
`sudo env PIG_IMAGE=… docker compose …`; `sudo -E` and bare `sudo VAR=val` are
|
||||
both refused by that same policy. Anything new that shells out to compose must
|
||||
use the wrapper.
|
||||
`sudo env PIG_IMAGE=… COMPOSE_PROFILES=… docker compose …`; `sudo -E` and bare
|
||||
`sudo VAR=val` are both refused by that same policy. `COMPOSE_PROFILES` travels
|
||||
the same way and for a sharper reason: dropped, compose does not error, it just
|
||||
leaves Piggy out of whatever you asked for. Anything new that shells out to
|
||||
compose must use the wrapper.
|
||||
|
||||
### Rollback
|
||||
|
||||
`scripts/deploy.sh` records the image the app container was running before it
|
||||
replaces it. If the health check, the unauthenticated-401 gate, or the
|
||||
public-origin marker check fails, it re-tags that image, restarts the app on it,
|
||||
reports whether the restored version is healthy, and exits non-zero. Previously
|
||||
replaces it. If the health check, the unauthenticated-401 gate, the piggy image
|
||||
assertion or the public-origin marker check fails, it re-tags that image,
|
||||
restarts the app — and Piggy with it, since they are one image running two
|
||||
commands — reports whether the restored version is healthy, and exits non-zero. Previously
|
||||
those exits left the broken release live, which was fine when a human was
|
||||
watching the terminal and an outage when the poller ran at 04:00.
|
||||
|
||||
@@ -291,6 +388,11 @@ Two things it deliberately does **not** do:
|
||||
- **It does not roll back when the public origin answers with an EMPTY body.**
|
||||
Something terminated TLS and replied, so the fault is the proxy — see `bind`
|
||||
below — and the previous image would fail the same check. Exit 3.
|
||||
- **It does not roll back when Piggy is enabled but does not come up.** Same
|
||||
reasoning: the previous image reads the same `.env`, so restoring it churns a
|
||||
healthy CRM without fixing the agent. Exit 3, and the log names
|
||||
`PIGGY_INFERENCE_API_KEY` because that is nearly always the cause. This is the
|
||||
one exit-3 case where the site itself is fine.
|
||||
|
||||
It *does* roll back when the origin answers with a **non-empty** body that lacks
|
||||
the marker. A proxy fault cannot serve a wrong-but-populated page for this
|
||||
|
||||
Reference in New Issue
Block a user