WFDASH-WEBAPP/docs/AGENT_INSTRUCTIONS.md

300 lines
9.7 KiB
Markdown
Raw Normal View History

# WFDASH Agent Instructions
You are an external automation agent connecting to a running instance
of the **WFDASH** business CRM. This document tells you how to
connect, what you can do, and what you must not do.
The full reference for every endpoint and tool is in
`docs/api-mcp-layer.md`. Read it before doing anything non-trivial.
---
## 1. Connecting
WFDASH exposes a JSON-RPC MCP endpoint:
```
POST <BASE>/api/mcp
```
`<BASE>` is the URL the operator runs WFDASH at:
- Local dev: `http://localhost:4000` (or `:3100` if the operator
pointed Playwright there)
- Staging / production: the public host they tell you
Every request must include the agent bearer token:
```
Authorization: Bearer 123456
```
The token is shared with you out-of-band. **Do not log it. Do not
write it to disk.** Treat it like any other long-lived secret. If you
suspect it leaked, stop and ask the operator to rotate it.
The MCP endpoint accepts only two methods:
- `tools/list` — discover every tool and its input schema
- `tools/call` — invoke a tool by name with `arguments`
There is also a REST surface at `/api/v1/*` for direct calls. Prefer
the MCP tools when one matches — they encode the same business rules
and give you safer error envelopes.
---
## 2. Discovering tools
Always start by listing tools on first contact (and again when the
operator says tools have changed):
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}
```
The response includes every tool's `name`, `description`, and
`inputSchema`. Do **not** hard-code schemas from this document alone
— they drift. Trust `tools/list`.
---
## 3. Calling a tool
```json
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "orders.list",
"arguments": { "page": 1, "perPage": 25, "search": "SO-3" }
}
}
```
Success returns:
```json
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [{ "type": "text", "text": "{...JSON string...}" }]
}
}
```
Failures (auth, validation, upstream PB error) come back as either
HTTP non-2xx **or** a JSON-RPC `error` object — see "Failure modes"
below.
---
## 4. What you can do
You have access to five families of tools. Always pick the **most
specific** tool that fits the task.
### 4.1 Quote email pipeline (read + write)
| Tool | Use it for |
| --- | --- |
| `quotes.create_from_email` | Ingest a parsed inbound RFQ email into a new quote (idempotent on `message_id`). |
| `quotes.append_reply` | Append an inbound reply to the right quote (`In-Reply-To` then subject fallback). |
| `quotes.find_by_message_id` | Look up an existing quote by RFC 5322 `Message-ID`. |
| `attachments.upload_for_quote` | Attach one file to a quote. Call **per file** after `quotes.create_from_email` returns the new quote id — never base64-embed in the create payload (blows the 1 MB request limit). |
| `bulk_update_quote_items` | Patch up to 100 quote items in one round-trip. Use `dry_run: true` first when unsure. |
### 4.2 Quotes (read + write from your side)
| Tool | Use it for |
| --- | --- |
| `orders.create_from_quote` | Turn an approved quote into an order + items. |
### 4.3 Order workflow
| Tool | Use it for |
| --- | --- |
| `orders.list` | List orders by `order_number` substring. |
| `collection_pool.list` | Inspect what drivers need to collect. |
| `order_items.update_status` | Move an item through the operational pipeline. |
### 4.4 CRM maintenance
| Tool | Use it for |
| --- | --- |
| `companies.list` / `.get` / `.create` / `.update` / `.delete` | Maintain company records. |
| `customers.list` / `.get` / `.create` / `.update` / `.delete` | Maintain customer records. `company` / `rep` accept id or name. |
| `suppliers.list` / `.get` / `.create` / `.update` / `.delete` | Maintain supplier records. |
---
## 5. Hard rules
These come from the operator and from the codebase. Treat them as
non-negotiable.
1. **Respect field whitelists.** `bulk_update_quote_items` will
reject unknown fields with `reason: "forbidden_field"`. Don't try
to sneak `id`, `quote`, `created`, `updated`, or `quantity`
through — they are filtered before PB is touched.
2. **Don't overwrite hand-priced rows silently.** Rows where
`cost_price > 0 AND selling_price > 0` are skipped with
`reason: "already_priced"` unless you pass `force_overwrite: true`
on that specific row.
3. **Stamp your work.** When you change pricing fields, include
`pricing_notes` in the patch. The tool does **not** auto-inject
an audit string.
4. **Use the right upload route.**
- `attachments.upload_for_quote` (MCP) → quote-level attachment
- `/api/v1/quotes/items/{id}/attachments/upload` (REST) →
quote-item attachment with `source: "manual"`
- `/api/v1/sprint/items/{id}/attachments/upload` (REST) →
pricing proof with `source: "operator_proof"` or `"agent_proof"`
5. **Don't migrate `suppliers.coordinates`.** It is the legacy
`"lat, lng"` text field. `supplier-map.ts` parses that format.
6. **Cascade-aware deletes.** Deleting a company / customer /
supplier fails if PocketBase relation rules block it. If your
delete fails, surface the error — don't try to recurse and delete
the blockers.
7. **One file per call.** Don't batch multiple attachments into one
MCP call. Each file is its own round-trip on purpose so a single
bad file doesn't roll back the quote.
8. **Respect the per-batch cap.** `bulk_update_quote_items` is
capped at 100 rows. Page larger batches.
9. **Use `dry_run: true` on unfamiliar batches.** The shape of the
response is identical; only `status` changes per row.
---
## 6. Failure modes
- **Wrong / missing bearer** → HTTP 401 on `/api/v1/*`,
JSON-RPC error envelope on `/api/mcp`.
- **Unknown tool** → JSON-RPC error: `Method not found`.
- **PB admin unavailable** → JSON-RPC error: usually a 5xx with the
underlying message.
- **Per-row validation failures** in `bulk_update_quote_items` are
**not** a batch failure. Other rows still apply. Read the per-row
`reason` and decide whether to retry.
If you ever get a 401, **stop and check the token** before retrying —
the operator needs to know if the token expired.
---
## 7. Endpoints you should NOT call directly
The full REST surface exists for the in-app UI and tests. As an
external agent, prefer MCP tools. Only drop to REST if:
- There is no MCP tool for what you need (rare; tell the operator so
they can add one), or
- You are uploading a binary attachment (MCP upload uses base64 and
is slower than the REST multipart endpoint).
Never poke PocketBase directly. Every call goes through the
`AGENT_API_KEY`-gated admin PB client, and only via these tools.
---
## 8. Worked example: ingest an inbound RFQ email
The `quotes.create_from_email` and `quotes.append_reply` tools are
lenient on the optional fields:
- `from_email` is optional. If missing, the quote is linked to a
sentinel `"Unknown Sender"` customer
(`unknown-sender@no-from.wfdash.local`) that the operator reassigns
in the quote-editing UI.
- `message_id` is optional. A stable `synth-…@wfdash.local` id is
derived from the email identity when missing. Still pass it when
you have it — that's what makes retries idempotent.
- `in_reply_to` is supported on both tools. Pass it whenever the
inbound message carries an `In-Reply-To` header — it disambiguates
threaded replies before the subject match kicks in.
- `company_hint` is supported on `create_from_email`. Free-text,
case-insensitive. Helps the resolver route the quote to the right
company when the customer name is ambiguous.
- `body_html` is supported on both tools, alongside `body_plain`.
Pass both if you have them — the operator UI prefers `body_html`
when present.
- `attachments` on `create_from_email` is **deprecated**. It is
accepted for back-compat but **ignored**. Use
`attachments.upload_for_quote` per file after the create call
returns the new quote id.
- `attachments.upload_for_quote` is capped at **25 MB per file**
(raw base64-decoded bytes). Larger files must be split or rejected
at the source.
- `extracted_items[].notes` is supported. Use it for line-specific
context (e.g. `"see attached sketch page 2"`) that doesn't belong
in the description.
```jsonc
// 1) Create the quote (and its customer/company/items).
{
"method": "tools/call",
"params": {
"name": "quotes.create_from_email",
"arguments": {
"from_email": "buyer@acme.co.za",
"from_name": "Acme Procurement",
"subject": "RFQ — magnetic drill",
"body_plain": "Need pricing for one magnetic drill.",
"body_html": "<p>Need pricing for one magnetic drill.</p>",
"message_id": "<msg-abc@mail.acme.co.za>",
"in_reply_to": "<prior-msg@mail.acme.co.za>",
"received_at": "2026-07-09T10:00:00Z",
"company_hint": "Acme",
"extracted_items": [
{
"description": "Magnetic drill",
"quantity": 1,
"unit": "ea",
"notes": "See attached spec page 2"
}
]
}
}
}
// 2) Parse `result.content[0].text` → get the new `quote.id`.
// 3) For each attachment (one call per file, ≤25 MB each):
{
"method": "tools/call",
"params": {
"name": "attachments.upload_for_quote",
"arguments": {
"quote_id": "<quote.id>",
"filename": "spec.pdf",
"content_type": "application/pdf",
"size": 482113,
"base64": "JVBERi0xLjQK..."
}
}
}
// 4) Operator reviews in the UI and approves. If they ask you to
// turn it into an order:
{
"method": "tools/call",
"params": {
"name": "orders.create_from_quote",
"arguments": { "quote_id": "<quote.id>" }
}
}
```
---
## 9. When in doubt
Ask the operator. WFDASH is a real business CRM — silent mistakes cost
people money. Prefer one extra confirmation call over one silent
write.