docs(ask): agent actions
docs/modules.md §7.1: threads, memory, files, free queries/virtual datasets, the propose/confirm/apply/undo gateway, both PDF export paths (manual answer export and Ask-made reports via make_pdf_report), images, and change requests — with the actor/x-ask-user model and the three new PocketBase collections. docs/data-model.md §4.17: field-by-field ask_threads/ask_messages/ ask_log reference, cross-linked from modules.md via explicit anchors. docs/api-reference.md §12/§12b: /api/chat's new body shape and its thread/action/chart/image/pdf SSE events, plus every new agent-actions route (picker, threads, attachments, confirm-card actions, memories, change requests) and their auth-matrix entries; the pb-files proxy entry now notes its ask_messages admin-token path. README.md: the intro, the modules table (adds Change requests), and the API surface summary now describe an agent that acts, not just a read-only chat. Also marks Tasks 1–12 and 9b's implementation checkbox done in the plan doc (docs/superpowers/plans/2026-09-26-ask-agent-actions.md) — the remaining unchecked box is 9b's live check, skipped because ask_threads/ask_messages/ask_log aren't applied on the target PocketBase yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
88011eb5b8
commit
fecc885d3e
5 changed files with 244 additions and 15 deletions
17
README.md
17
README.md
|
|
@ -5,8 +5,11 @@ South African civil construction company, backed by **PocketBase**.
|
|||
Tracks, per construction site: contracts, employees & timesheets,
|
||||
vehicles (fuel + plant), fuel logs, plant hour meters, fixed costs,
|
||||
income claims, purchase orders + line items, and delivery notes with
|
||||
OCR-driven receipting. Includes an LLM chat ("Ask") with a read-only
|
||||
tool layer over the same data, and a read-only MCP server.
|
||||
OCR-driven receipting. Includes an LLM agent ("Ask") that reads the same
|
||||
data the dashboard uses, measures anything with free queries and its own
|
||||
charts, reads any file you attach, remembers lasting facts, changes data
|
||||
through confirm-first proposed actions with undo, and files its own PDF
|
||||
reports and app change requests — plus a read-only MCP server.
|
||||
|
||||
> **Default login PIN is `2468`.** Change `NEXT_PUBLIC_APP_PIN` in
|
||||
> production. There is no PIN bypass — the old `?demo=1` shortcut was
|
||||
|
|
@ -43,7 +46,7 @@ npm run mcp # uses the PocketBase creds in .env.local
|
|||
|
||||
## Modules
|
||||
|
||||
The sidebar (`src/components/layout/sidebar.tsx`) has four top-level
|
||||
The sidebar (`src/components/layout/sidebar.tsx`) has five top-level
|
||||
pages and three groups. Each is documented in
|
||||
[`docs/modules.md`](./docs/modules.md).
|
||||
|
||||
|
|
@ -64,7 +67,8 @@ pages and three groups. Each is documented in
|
|||
| `/dashboard/financial/purchase-orders[/[poId]]` | Purchase Orders | Every PO, fulfilment %, line editing, verify |
|
||||
| `/dashboard/financial/delivery-notes` (+ `/inbox`, `/work-queue`, `/document/[id]`) | Delivery Notes | Upload → OCR → match → post pipeline and its review queues |
|
||||
| `/dashboard/financial/suppliers[/[supplierId]]` | Suppliers | Ordered / delivered / outstanding per supplier |
|
||||
| `/dashboard/chat` | Ask | Read-only LLM chat with the same metrics the dashboard uses |
|
||||
| `/dashboard/chat` | Ask | Agent: free queries + charts, files, memory, proposed data changes with undo, PDF reports, images |
|
||||
| `/dashboard/change-requests` | Change requests | App changes ("can we add this page/column/button?") Ask has logged for the developer |
|
||||
| `/clock` | MEGA Clock | Installable phone PWA: worker badge scan in/out + vehicle hour-meter scan |
|
||||
|
||||
Old paths (`/dashboard/employees`, `/dashboard/fuel[/id]`,
|
||||
|
|
@ -131,7 +135,10 @@ historical, not authoritative.
|
|||
| `POST /api/vehicles/import-summary` | server-only | Sync the Buildsite Vehicle Fuel Summary CSV into `vehicles` |
|
||||
| `POST /api/vehicles/[id]/merge` | server-only | Merge a duplicate vehicle into another (dry run unless `confirm`) |
|
||||
| `GET /api/pb-files/[collection]/[recordId]/[...filename]` | server-only | Same-origin proxy for PocketBase files (PDF slips in iframes) |
|
||||
| `POST /api/chat` | `ENABLE_CHAT=true` (no user auth) | SSE chat stream |
|
||||
| `POST /api/chat` | `ENABLE_CHAT=true`, `x-ask-user` identifies (no real auth yet) | SSE chat stream — `thread`/`action`/`chart`/`image`/`pdf` events |
|
||||
| `GET /api/ask/users`, `GET/POST /api/chat/threads[/[id]]`, `POST /api/chat/attachments`, `GET/POST /api/ask/memories[/[id]]` | `ENABLE_CHAT=true` + `x-ask-user` | Ask's picker, threads, attachments, memory |
|
||||
| `GET /api/agent/actions/[id]`, `POST …/{apply,reject,undo}` | `ENABLE_CHAT=true` + `x-ask-user`; confirm routes are same-origin only | Propose → confirm → apply/undo, with a before/after snapshot |
|
||||
| `GET/PATCH /api/ask/change-requests[/[id]]` | server-only | App change requests Ask filed for the developer |
|
||||
| `GET /api/clock/employee-lookup`, `POST /api/clock/scan`, `POST /api/clock/vehicle-lookup`, `POST /api/clock/vehicle-scan` | server-only | MEGA Clock PWA: badge clock-in/out + vehicle hour-meter scan |
|
||||
| `POST /api/clock/events/[id]/correct` | same-origin + `x-app-pin` = `APP_PIN` | Record the real leaving time for a missed scan-out |
|
||||
|
||||
|
|
|
|||
|
|
@ -596,20 +596,41 @@ but valid "last scanned at site X" is more useful than nulling it.
|
|||
|
||||
**File:** `src/app/api/chat/route.ts`
|
||||
**Caching:** `dynamic = "force-dynamic"`
|
||||
**Auth:** none — gated by `ENABLE_CHAT=true`. **No user authentication.**
|
||||
**Auth:** none — gated by `ENABLE_CHAT=true`. Identification only, via
|
||||
`x-ask-user` (a `users` record id from the picker — see
|
||||
[modules.md §7.1](./modules.md#ask-agent-actions-modules)). **No real user
|
||||
authentication yet.**
|
||||
**Protocol:** Server-Sent Events
|
||||
|
||||
**Body — new:** `{ threadId?, message, display?, attachmentIds?, images? }`
|
||||
with `x-ask-user`. No `threadId` creates one, titled from `message`, owned
|
||||
by the actor; a thread owned by someone else 404s.
|
||||
**Body — legacy** (kept working for one release, unpersisted): `{ history }`
|
||||
with no `threadId`.
|
||||
|
||||
`Content-Type: text/event-stream; charset=utf-8`. Events emitted:
|
||||
- `meta` — `{ provider, model }` (first)
|
||||
- `thread` — `{ id, title }` — only when a new thread was just created
|
||||
- `delta` — `{ text }` (repeated, as the model streams)
|
||||
- `tool` — `{ name }` (when a tool is called)
|
||||
- `action` — the full `PlannedAction` preview from `propose_changes`
|
||||
(`{ actionId, summary, preview[{ collection, kind, id?, before?, after }], problems, status: "pending" }`).
|
||||
Sent **instead of** handing the plan to the model: the tool result the
|
||||
model sees is `{ actionId, status, problems }` only.
|
||||
- `chart` — a `ChartSpec` (`src/lib/reports/chart-spec.ts`:
|
||||
`{ id, kind, title, subtitle?, xKey, xLabel?, series[{ key, label, format }], stacked?, rows }`),
|
||||
once per chart a report tool returned. Sent **instead of** handing the
|
||||
chart to the model: the tool result the model sees has `charts` replaced
|
||||
by `chartsAvailable: { id, title }[]`, and the model places a chart by
|
||||
writing `[[chart:<id>]]` on its own line. `id` matches
|
||||
once per chart a report tool or `make_chart` returned. Sent **instead
|
||||
of** handing the chart to the model: the tool result the model sees has
|
||||
`charts` replaced by `chartsAvailable: { id, title }[]`, and the model
|
||||
places a chart by writing `[[chart:<id>]]` on its own line. `id` matches
|
||||
`/^[a-z0-9-]{1,64}$/`; specs with any other id are dropped.
|
||||
- `image` — `{ id, url }`, once per `generate_image` call. Same split as
|
||||
`chart`: the model keeps `imagesAvailable: [{ id }]` and places it with
|
||||
`[[image:<id>]]`.
|
||||
- `pdf` — `{ title, subtitle?, filename? }`, once when `make_pdf_report`
|
||||
fires (the model calls it last, after building the report itself). The
|
||||
browser renders a report cover and a Download PDF button for that turn;
|
||||
the model's own tool result is just `{ ok: true }`.
|
||||
- `done` — `{ text, iterations, stoppedOnLimit }` (final)
|
||||
- `error` — `{ message }` (on failure)
|
||||
|
||||
|
|
@ -623,6 +644,32 @@ tool layer (`src/lib/agent/`).
|
|||
|
||||
---
|
||||
|
||||
## 12b. Ask agent actions
|
||||
|
||||
Detailed field-by-field contracts live in each route's own doc comment
|
||||
(same convention as §12a); see
|
||||
[modules.md §7.1](./modules.md#ask-agent-actions-modules) for how these
|
||||
fit together and [data-model.md §4.17](./data-model.md#ask-agent-actions)
|
||||
for the collections. All gated the same way as §12 (`ENABLE_CHAT=true`);
|
||||
all identify the actor via `x-ask-user` except where noted.
|
||||
|
||||
| Method + path | Purpose |
|
||||
| --- | --- |
|
||||
| `GET /api/ask/users` | `{ users: { id, name }[] }` — `users` records with a non-empty `name`, for the "Who's using Ask?" picker. No `x-ask-user` needed. |
|
||||
| `GET` / `POST /api/chat/threads` | List the actor's threads (newest first) / create an empty one. |
|
||||
| `GET` / `PATCH` / `DELETE /api/chat/threads/[id]` | A thread's rows / rename+archive / delete. 404 if not owned by the actor. |
|
||||
| `POST /api/chat/attachments` | Multipart `file` (+ optional `threadId`). Parses and stores as an `ask_messages` row (`kind: "attachment"`); returns `{ threadId, attachmentId, kind, summary, images? }` (`images` only for a scanned PDF's rasterised pages). |
|
||||
| `GET /api/agent/actions/[id]` | An action's current state, reconstructed as `{ actionId, summary, preview, problems: [], status, error }` — used to re-render a card on thread reopen or refresh. |
|
||||
| `POST /api/agent/actions/[id]/apply` | Confirm: applies a pending action. Same-origin only (`Origin` must match). |
|
||||
| `POST /api/agent/actions/[id]/reject` | Cancel: marks a pending action rejected without applying anything. Same-origin only. |
|
||||
| `POST /api/agent/actions/[id]/undo` | Reverses an applied action once. Same-origin only. |
|
||||
| `GET` / `POST /api/ask/memories` | List the actor's active memories + active shared ones / add one manually (`{ text, site?, shared? }`). |
|
||||
| `DELETE /api/ask/memories/[id]` | Forget one (kept as `status: "undone"`, not deleted). 400 if someone else tries to forget a personal memory. |
|
||||
| `GET /api/ask/change-requests` | `{ requests: [...] }`, optional `?status=`. No `x-ask-user` needed — a plain dashboard route, not actor-scoped. |
|
||||
| `PATCH /api/ask/change-requests/[id]` | `{ status }` — `open`/`planned`/`done`/`declined`. No `x-ask-user` needed. |
|
||||
|
||||
---
|
||||
|
||||
## 12a. Routes added after the 2026-09-06 pass
|
||||
|
||||
These are summarised rather than documented field-by-field; each route
|
||||
|
|
@ -668,7 +715,7 @@ the queue item (see [`n8n/purchase-order-ingest-implementation.md`](./n8n/purcha
|
|||
| `PATCH /api/suppliers/[id]` | Rename, same normalisation + uniqueness. |
|
||||
| `POST /api/vehicles/[id]/merge` | Body `{ intoId, confirm? }`. Without `confirm: true` returns the plan (dry run). With it: moves the duplicate `[id]`'s fuel logs and plant-hours rows onto `intoId`, fills blank fields, deletes `[id]`. 409 on conflicts (different site/type, same plant-hours week). Batches of ≤ 50; safe to re-run if interrupted. |
|
||||
| `POST /api/vehicles/import-summary` | Sync the Buildsite "Vehicle Fuel Summary" CSV: which vehicles exist per site and whether each is fuel (km/L) or plant (L/h). |
|
||||
| `GET /api/pb-files/[collection]/[recordId]/[...filename]` | Same-origin proxy for PB files, so PDF slips render in an iframe without PB's sandbox CSP. |
|
||||
| `GET /api/pb-files/[collection]/[recordId]/[...filename]` | Same-origin proxy for PB files, so PDF slips render in an iframe without PB's sandbox CSP. Collections: `delivery_notes`/`purchase_orders` (public, forwarded as-is) and `ask_messages` (its rules are all null by design — this route mints a short-lived admin file token server-side rather than exposing that rule). |
|
||||
|
||||
### MEGA Clock additions
|
||||
|
||||
|
|
@ -722,7 +769,11 @@ searched for:
|
|||
| `GET /api/data-import-batches/[id]/status` | server-only | UI poll |
|
||||
| `POST /api/data-import-batches/[id]/retry` | server-only | |
|
||||
| `POST /api/records/[collection]/[id]/verify` | server-only | Allowlist: POs + DNs |
|
||||
| `POST /api/chat` | gated by `ENABLE_CHAT=true` | **No user auth** — the gate is the env flag, not identity |
|
||||
| `POST /api/chat` | gated by `ENABLE_CHAT=true` | `x-ask-user` **identifies**, doesn't authenticate — anyone with the PIN can pick any name |
|
||||
| `GET /api/ask/users` · `GET/POST /api/chat/threads` · `.../[id]` · `POST /api/chat/attachments` · `GET/POST /api/ask/memories` · `DELETE …/[id]` | gated by `ENABLE_CHAT=true` | `x-ask-user` identifies the actor; threads/memories 404/400 if not owned |
|
||||
| `GET /api/agent/actions/[id]` | gated by `ENABLE_CHAT=true` | `x-ask-user` only |
|
||||
| `POST /api/agent/actions/[id]/{apply,reject,undo}` | same-origin only + `x-ask-user` | Confirm-card buttons; in the test phase anyone may confirm any pending action |
|
||||
| `GET/PATCH /api/ask/change-requests` · `.../[id]` | server-only | Plain dashboard route, not actor-scoped |
|
||||
| `POST /api/clock/scan` | server-only | Behind the /clock PIN gate |
|
||||
| `POST /api/clock/vehicle-lookup` | server-only | Behind the /clock PIN gate |
|
||||
| `POST /api/clock/vehicle-scan` | server-only | Behind the /clock PIN gate |
|
||||
|
|
@ -740,7 +791,7 @@ searched for:
|
|||
| `POST /api/suppliers` · `PATCH /api/suppliers/[id]` | server-only | |
|
||||
| `POST /api/vehicles/import-summary` | server-only | Buildsite CSV sync |
|
||||
| `POST /api/vehicles/[id]/merge` | server-only | Dry run unless `confirm: true`; deletes the duplicate |
|
||||
| `GET /api/pb-files/[collection]/[recordId]/[...filename]` | server-only | Same-origin PB file proxy |
|
||||
| `GET /api/pb-files/[collection]/[recordId]/[...filename]` | server-only | Same-origin PB file proxy; mints an admin file token for `ask_messages` |
|
||||
|
||||
**Routes with no protection at all:** the chat route (when enabled) and
|
||||
any route that runs purely server-side. The browser-facing routes
|
||||
|
|
|
|||
|
|
@ -370,6 +370,62 @@ Joins DNs to PO line items. `cascadeDelete: true` on the parent DN.
|
|||
| `qty_received` | number | |
|
||||
| `condition_notes` | text | Free-text audit trail (e.g. `"OCR 92% · desc: Cement 50kg · unit: bag"`) |
|
||||
|
||||
<a id="ask-agent-actions"></a>
|
||||
|
||||
### 4.17 `ask_threads`, `ask_messages`, `ask_log` (Ask agent actions)
|
||||
|
||||
Additive, created by `scripts/pb-add-ask-collections.py` (`--apply` is a
|
||||
human step). All rules `null` — superuser only; the Next.js server is the
|
||||
only client. See [modules.md §7.1](./modules.md#ask-agent-actions-modules)
|
||||
for how these back threads, memory, actions and change requests.
|
||||
|
||||
**`ask_threads`** — one row per conversation.
|
||||
|
||||
| Field | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `title` | text | `autoTitle()` of the first message, or renamed by the user |
|
||||
| `owner` | relation → `users`, required | The picked actor (`x-ask-user`) |
|
||||
| `archived` | bool | |
|
||||
| `last_message_at` | date | Bumped on every `appendMessage`; a lost race here is harmless |
|
||||
|
||||
**`ask_messages`** — one row per message, attachment or generated image.
|
||||
**Append-only**: created once, never rewritten, so concurrent tabs/users
|
||||
can't overwrite each other.
|
||||
|
||||
| Field | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `thread` | relation → `ask_threads`, required, **cascade delete** | |
|
||||
| `owner` | relation → `users` | |
|
||||
| `kind` | select | `user` / `assistant` / `attachment` / `image` |
|
||||
| `text` | text (max 100 000) | The message; for an attachment, its summary; for an image, the prompt |
|
||||
| `display` | text | The slash command as typed, when applicable |
|
||||
| `charts` | json | `ChartSpec[]` the answer placed or left unplaced |
|
||||
| `action_ids`, `attachment_ids`, `tools` | json | Arrays of ids / tool names |
|
||||
| `file` | file (≤25 MB; xlsx/xls/csv/pdf/png/jpg/webp/txt) | Attachment or generated image |
|
||||
| `parsed` | json (≤5 MB) | Attachment preview (capped 2 000 rows / 50 000 chars — `read_attachment` re-parses the stored file beyond that), or `{ pdf: {...} }` for a report turn |
|
||||
| `parse_error` | text | |
|
||||
|
||||
**`ask_log`** — one row per thing Ask keeps track of, by `type`.
|
||||
|
||||
| Field | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `type` | select | `change` (proposed data edit, for Undo) / `memory` (lasting fact) / `request` (app change request) |
|
||||
| `owner` | relation → `users` | |
|
||||
| `shared` | bool | Memories only — visible to everyone when `true` |
|
||||
| `thread` | relation → `ask_threads` (optional, **no** cascade) | The log outlives a deleted thread |
|
||||
| `status` | select | `change`: `pending`/`applied`/`rejected`/`failed`/`undone`. `memory`: `applied` (active) / `undone` (forgotten). `request`: `open`/`planned`/`done`/`declined` |
|
||||
| `text` | text (max 2 000) | Change summary / memory text / request title |
|
||||
| `detail` | text | Request detail |
|
||||
| `site` | relation → `sites` (optional) | A memory about one site |
|
||||
| `ops` | json | The planned `AgentOp[]`, `$ref`s intact |
|
||||
| `before`, `after` | json | Snapshots for Undo. `before`: one entry per op. `after`: while `pending`, the proposed values per op; once `applied`/`failed`, `{ results, confirmed_by, undone_by? }` |
|
||||
| `error` | text | Set on `applied_action` → `failed` |
|
||||
| `applied_at`, `undone_at` | date | |
|
||||
|
||||
Indexes: `ask_threads(owner, archived, last_message_at)`,
|
||||
`ask_messages(thread, created)`, `ask_log(type, status)`,
|
||||
`ask_log(owner, type)`, `ask_log(thread)`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Indexes and constraints summary
|
||||
|
|
|
|||
115
docs/modules.md
115
docs/modules.md
|
|
@ -437,6 +437,121 @@ real authentication exists — the chat reads every site's financials and
|
|||
spends LLM credits for anyone who can reach it." See
|
||||
[multi-site-assessment §7](./multi-site-assessment.md#7-the-chatagent-layer).
|
||||
|
||||
<a id="ask-agent-actions-modules"></a>
|
||||
|
||||
### 7.1 Agent actions — threads, memory, files, free queries, proposed changes, PDF reports, images, change requests
|
||||
|
||||
Ask went from a report reader to an agent that reads any file, measures
|
||||
anything with its own charts, changes data through confirm-first actions
|
||||
with undo, and files its own PDF reports and app-change requests. Three
|
||||
PocketBase collections back all of it (`scripts/pb-add-ask-collections.py`,
|
||||
additive, `--apply` is a human step): `ask_threads`, `ask_messages`
|
||||
(append-only), `ask_log` (`type`: `change` | `memory` | `request`). Schema
|
||||
details: [data-model.md](./data-model.md#ask-agent-actions).
|
||||
|
||||
**Per-user, no logins yet.** `src/lib/agent/context.ts`'s `resolveActor`
|
||||
trusts an `x-ask-user` header — a `users` record id the "Who's using
|
||||
Ask?" picker (`GET /api/ask/users`, only records with a `name`) writes to
|
||||
`localStorage`. This is identification, not security, until real logins
|
||||
land — see "When logins land" in the plan doc. `AgentContext { actor,
|
||||
threadId }` is threaded through every tool and the gateway.
|
||||
|
||||
**Threads.** `src/lib/chat/threads.ts` loads/appends `ask_messages` rows
|
||||
(`autoTitle`, `loadThreadEntries`, `appendMessage` — creates only, never
|
||||
rewrites, so concurrent tabs/users can't clobber each other) under
|
||||
`/api/chat/threads` (list/create) and `/api/chat/threads/[id]`
|
||||
(get/rename/archive/delete). `/api/chat` accepts
|
||||
`{ threadId?, message, display?, attachmentIds?, images? }` with
|
||||
`x-ask-user`; no `threadId` creates one titled from the first message. The
|
||||
old `{ history }` body (no `threadId`) still works, unpersisted, for one
|
||||
release.
|
||||
|
||||
**Memory.** `src/lib/agent/memory.ts`: `remember`/`forget`/`listMemories`
|
||||
against `ask_log` (`type: "memory"`), deduped by normalised text. Personal
|
||||
memories are private to their owner; `shared: true` ones are visible to
|
||||
everyone. `formatMemoriesForPrompt` feeds the prompt's "What you remember"
|
||||
section. A Memory drawer in `chat-view.tsx` lists Mine / Shared with
|
||||
delete buttons and a manual add.
|
||||
|
||||
**Files.** `src/lib/agent/attachments.ts`'s `parseAttachment` reads
|
||||
xlsx/xls/csv (SheetJS/PapaParse), PDF (`pdftotext -layout`; under ~50
|
||||
chars/page marks it `scanned`), images and plain text.
|
||||
`POST /api/chat/attachments` stores the result as an `ask_messages` row
|
||||
(`kind: "attachment"`); a scanned PDF's first pages are rasterised and
|
||||
returned as vision images the same way a photographed slip is. The
|
||||
`read_attachment` tool re-parses on demand and pages a sheet 200 rows at a
|
||||
time. `send_to_po_pipeline` posts a stored attachment into the existing
|
||||
purchase-order pipeline (hash-dedupe, same as `/api/purchase-orders/upload`)
|
||||
once the user confirms it's a PO/DN. `src/lib/agent/people-match.ts`'s
|
||||
`matchPeople` (employee number → normalised full name → surname+initial
|
||||
"likely") backs the "load workers from a list" recipe: `match_workers` →
|
||||
`propose_changes`, never re-creating an exact match.
|
||||
|
||||
**Free queries and charts.** `src/lib/agent/query.ts`'s `runQuery` reads
|
||||
either a live PocketBase collection (fields validated against
|
||||
`schema-cache.ts`, filters bound with `pb.filter()`) or one of the seven
|
||||
virtual datasets in `src/lib/agent/datasets.ts` (`site_summary`,
|
||||
`fuel_log_status`, `plant_hours`, `payroll_hours`, `attendance`,
|
||||
`supplier_balance`, `po_fulfilment` — each built from the same
|
||||
`src/lib/metrics` functions the dashboard pages use, never re-derived).
|
||||
Grouping, Monday-start week/day/month bucketing, and
|
||||
sum/avg/min/max/count/count_distinct aggregation happen in JS on rows
|
||||
already fetched — the model never computes. Results are cached under a
|
||||
`resultId` (2h TTL); `make_chart` turns one into a `ChartSpec` by column
|
||||
name only, so the model designs its own chart without ever holding a
|
||||
number.
|
||||
|
||||
**Proposed changes (the gateway).** `src/lib/agent/gateway.ts` is the one
|
||||
seam every write goes through: `authorize()` (a stub today — rejects
|
||||
`NEVER_WRITABLE` collections and `dn_batches` lock/stage fields; the seam
|
||||
where logins will add real permission checks) → `planOps` (schema
|
||||
validation, `before` snapshots, relation-id existence, the
|
||||
sites/employees duplicate rule, capped at 500 ops) → `createPendingAction`
|
||||
(a pending `ask_log` row) → `applyAction`/`undoAction`. `propose_changes`
|
||||
is the tool; `/api/agent/actions/[id]/{apply,reject,undo}` are the
|
||||
same-origin-only confirm-card routes. The full plan streams to the
|
||||
browser as an `action` SSE event; the model itself only ever sees
|
||||
`{ actionId, status, problems }` — never the preview — the same split
|
||||
`extractCharts` already does for chart numbers. `action-card.tsx` renders
|
||||
it (created rows in green, updated fields old→new, deleted in red) with
|
||||
Confirm/Cancel, then Undo once applied.
|
||||
|
||||
**PDF export.** Two independent paths. (1) Any answer can be exported
|
||||
as-is: the toolbar's **PDF** button (`src/app/dashboard/chat/answer-pdf.ts`)
|
||||
clones the rendered answer at a fixed A4 width, renders it with
|
||||
`html-to-image`, and slices it into pages that never cut through a chart
|
||||
or table row (`slicePages`), embedding each with `pdf-lib`. (2) Ask can
|
||||
build a report on request ("give me a PDF on fuel at The Hills for
|
||||
September"): it writes the report itself (headings, narrative, KPI
|
||||
tables quoting tool display strings, charts via `make_chart`), then calls
|
||||
`make_pdf_report({ title, subtitle? })` **once, last** — a pure metadata
|
||||
signal (`src/lib/agent/pdf-report.ts`), split out of the tool result and
|
||||
streamed as a `pdf` SSE event the same way an action card is. The turn
|
||||
then renders a report cover (wordmark, title, date, "Prepared by Ask for
|
||||
{name}") and reuses path (1)'s `answerToPdf`/`slicePages` for a Download
|
||||
button that also fires once automatically
|
||||
(`src/app/dashboard/chat/pdf-auto-download.ts`'s `nextPdfTurnToDownload`,
|
||||
a pure "once per turn" decision). The report meta persists in the
|
||||
assistant row's existing `parsed` json field, so reopening the thread
|
||||
still shows the cover and button.
|
||||
|
||||
**Images.** `generate_image` (`src/lib/agent/images.ts`) calls MiniMax
|
||||
`image-01` and stores the result as its own `ask_messages` row
|
||||
(`kind: "image"`), streamed as an `image` SSE event and placed with
|
||||
`[[image:<id>]]` — the same split/placement mechanism as a chart, but an
|
||||
illustration only: never a chart, a number, or anything that looks like
|
||||
data. A `base_resp.status_code` in `{1028, 1030, 2061}` (the MiniMax Token
|
||||
Plan tier gate) surfaces as "Image generation isn't enabled on this
|
||||
MiniMax plan" rather than retrying.
|
||||
|
||||
**Change requests.** "Can we add this to the app?" can't be done by Ask
|
||||
itself — `file_change_request` logs an `ask_log` row (`type: "request"`,
|
||||
`status: "open"`) instead of `propose_changes`, which is for data.
|
||||
`/dashboard/change-requests` lists and re-statuses them
|
||||
(open/planned/done/declined); `list_change_requests` (read-only) is also
|
||||
served by the MCP server (`src/mcp/server.ts`) so Claude Code can pick
|
||||
them up.
|
||||
|
||||
---
|
||||
|
||||
## 8. Financial landing, Delivery Notes, Suppliers
|
||||
|
|
|
|||
|
|
@ -379,13 +379,13 @@ Added mid-plan: the user wants to ask for any custom report and get a finished P
|
|||
|
||||
- The Claude provider still points at `claude-opus-5`. Read the `claude-api` skill and update it to the current model (a configurable `ANTHROPIC_MODEL` env with a current default).
|
||||
- Add `docs/operations.md` guidance: `CHAT_PROVIDER=anthropic` + `ANTHROPIC_API_KEY` switches Ask to Claude, which is more reliable at long multi-step tool use (reading a sheet → matching → proposing 40 creates). Run the same 5 test prompts on both providers and compare.
|
||||
- [ ] Commit — `chore(ask): configurable Claude model`.
|
||||
- [x] Commit — `chore(ask): configurable Claude model`.
|
||||
|
||||
---
|
||||
|
||||
### Task 13: Docs
|
||||
|
||||
- [ ] `docs/modules.md` §7 (threads, memory, files, query/datasets, actions + undo, PDF, images, change requests), `docs/data-model.md` (`ask_threads`, `ask_messages`, `ask_log`), `docs/api-reference.md` (new routes + `thread`/`action`/`image` SSE events), `README.md` summary. Commit — `docs(ask): agent actions`.
|
||||
- [x] `docs/modules.md` §7 (threads, memory, files, query/datasets, actions + undo, PDF, images, change requests), `docs/data-model.md` (`ask_threads`, `ask_messages`, `ask_log`), `docs/api-reference.md` (new routes + `thread`/`action`/`image`/`pdf` SSE events), `README.md` summary. Commit — `docs(ask): agent actions`.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue