# Backend Task: Move AI Field-Mapping to Cloudflare Workers AI + Add Quotas

## Context

`app.formconnector.co.uk` currently proxies AI field-mapping requests from the
FormConnector WordPress plugin to an AI provider. This doc specifies the exact
request/response contract the plugin already expects (**do not change it**
without also updating the plugin client), and what needs to change on the
backend:

1. Swap the model provider for these two endpoints to **Cloudflare Workers AI**
   (small/cheap instruct model — this is a tiny structured-JSON classification
   task, not a task that needs a frontier model).
2. Add hard per-license usage quotas (daily/monthly) so cost stays bounded.
3. Keep the existing plan-gating (`402`) and rate-limit (`429`) behaviors.

The plugin side (WordPress) is already fully implemented and should **not**
need any changes if this contract is preserved. Plugin-side files for
reference: `inc/Integration/AI/BackendAIClient.php`,
`inc/Integration/AI/AIMapper.php`, `inc/Integration/AI/ResponseParser.php`,
`inc/Integration/AI/PromptBuilder.php`, `inc/API/Endpoints/AIEndpoints.php`.

---

## Endpoint 1: `POST /api/ai/suggest-mappings`

### Request

Headers:

```
x-api-key: <license key>       # required, validate against active/paid license
Content-Type: application/json
Accept: application/json
```

Body:

```json
{
	"form_fields": [
		{
			"id": "field_1",
			"label": "Your Email Address",
			"type": "email",
			"options": ["Option A", "Option B"] // optional, only for select/radio/checkbox fields, max 20 items
		}
	],
	"carehq_fields": {
		"first_name": "First Name",
		"last_name": "Last Name",
		"email": "Email Address",
		"phone": "Phone Number",
		"care_requirements": "Message / Enquiry Details"
	},
	"site_url": "https://example.com"
}
```

Notes:

- `form_fields` is a small array — typically 3–20 entries per form.
- `carehq_fields` is currently a fixed 5-key map (see above), but treat it as
  dynamic (don't hardcode the 5 keys — always use whatever is sent).
- No PII/submission data is ever sent — only field metadata (labels/types/option
  labels). Do not log full request bodies beyond what's needed for debugging.

### The prompt the model should follow (current implementation, keep the same contract)

System prompt (dynamic field list built from `carehq_fields`):

```
You are a field-mapping assistant for a CRM integration.
Your job is to match web form fields to CRM (CareHQ) contact fields.

Available CareHQ target fields:
  - "first_name" (First Name)
  - "last_name" (Last Name)
  - "email" (Email Address)
  - "phone" (Phone Number)
  - "care_requirements" (Message / Enquiry Details)

Rules:
1. Return ONLY valid JSON — no markdown, no explanation, no extra text.
2. The JSON must be an object where each key is the form field "id" and the value is an object with:
   - "target": the CareHQ field key it maps to, or "" if no match
   - "confidence": a number 0.0–1.0
   - "reason": a short explanation (max 15 words)
3. Every form field must appear exactly once in the output.
4. Each CareHQ target field should be used at most once (pick the best match).
5. Set confidence < 0.3 and target "" for fields that don't match anything.
6. Consider field labels, types, and option values when deciding matches.
```

User prompt:

```
Map these form fields to CareHQ fields. Return ONLY a JSON object.

Form fields:
<pretty-printed JSON array of form_fields>
```

Expected model output (strict JSON, keyed by form field id):

```json
{
	"field_1": {
		"target": "email",
		"confidence": 0.95,
		"reason": "Label and type both indicate an email field"
	}
}
```

### Response — Success (HTTP 200)

```json
{
	"success": true,
	"data": {
		"suggestions": {
			"field_1": { "target": "email", "confidence": 0.95, "reason": "..." }
		},
		"model": "name-of-model-used"
	}
}
```

- `data.suggestions` — required. The plugin re-validates/clamps this itself
  (`ResponseParser::parse_array`), so it's OK if the model is slightly messy —
  but do your own JSON-repair/strip-markdown-fences pass before returning,
  since the plugin only does light validation, not full JSON extraction from
  raw model text (it accepts a raw string too, but prefer sending it already
  parsed via `data.suggestions`).
- `data.model` — optional but nice to have (surfaced in the Mappings UI).
- The plugin computes its own `latency_ms` client-side — you don't need to
  return one, but it's harmless if you do (`data.latency_ms`, ignored).
- Also acceptable (fallback, older shape the plugin still supports):
  top-level `suggestions` / `model` instead of nested under `data`.

### Response — Errors

| HTTP Code | Meaning                                           | Body                                                                 |
| --------- | ------------------------------------------------- | -------------------------------------------------------------------- |
| 400       | Bad request (missing/invalid fields)              | `{ "message": "..." }`                                               |
| 402       | License valid but AI not included in current plan | `{ "message": "AI mapping is not available on your current plan." }` |
| 429       | Per-license quota/rate limit exceeded             | `{ "message": "AI request limit reached. Please try again later." }` |
| 401/403   | Invalid/missing license key                       | `{ "message": "..." }`                                               |
| 5xx       | Upstream/model error                              | `{ "message": "..." }`                                               |

The plugin treats any non-2xx as a generic error and surfaces `message` to the
admin UI, except it has special copy for 402 and 429 — **keep using those two
status codes** for those specific cases.

---

## Endpoint 2: `POST /api/ai/test-connection`

### Request

```
x-api-key: <license key>
Content-Type: application/json
Accept: application/json
```

```json
{ "site_url": "https://example.com" }
```

### Response

Success (HTTP 2xx **and** `success: true` in body — both are required):

```json
{ "success": true, "message": "AI service connected successfully." }
```

Failure (any other case):

```json
{ "success": false, "message": "Reason it failed" }
```

Use this endpoint to do a _cheap_ connectivity + auth check — e.g. validate
the license key and confirm the Workers AI binding responds, without
necessarily running a full inference call.

---

## Task 1: Swap provider to Cloudflare Workers AI

- Replace whatever model call currently backs `/api/ai/suggest-mappings` with
  a Cloudflare Workers AI binding (`env.AI.run(...)`).
- Recommended model class: a small instruct model good at structured JSON
  output (e.g. `@cf/meta/llama-3.1-8b-instruct` or an equivalent small/cheap
  model available in your Workers AI catalog). This task is a closed-set
  classification of a handful of labels against ≤10 target keys — it does not
  need a large/expensive model.
- Keep `max_tokens` low — the output JSON is small (one small object per form
  field).
- Set `response_format`/JSON-mode if the chosen model supports it, to reduce
  the need for markdown-fence stripping. Otherwise, strip ` ```json ` /
  ` ``` ` fences and parse the first `{ ... }` block before returning
  `data.suggestions` (the plugin does this too, but do it server-side first so
  bad output doesn't need a plugin release to fix).
- Populate `data.model` with the actual Workers AI model id used, so it shows
  up correctly in the Mappings UI ("Suggestions from <model>, Nms").

## Task 2: Add per-license quotas

- Add a counter (KV or D1 — whichever this backend already uses for
  per-license state) keyed by license key, incremented on each successful
  `/api/ai/suggest-mappings` call.
- Suggested default limits (tune as needed): **50 requests/day** and/or
  **500/month** per license. Return **HTTP 429** with the existing message
  shape once exceeded — no plugin changes required.
- Continue gating the whole feature behind an active **paid** plan — return
  **HTTP 402** for free-tier/inactive licenses, as already implemented.
- Since the plugin already caches identical suggestion requests client-side
  for 24h (same form fields + same target fields → no repeat call), you don't
  need to duplicate that dedup logic server-side, but it doesn't hurt to add
  a short (e.g. 1h) cache keyed by a hash of `form_fields` + `carehq_fields` +
  license, to protect against a form being re-mapped by multiple admins.

## Out of scope / no plugin changes needed

As long as the request/response contract above is preserved exactly
(especially the 402/429 status codes and the `data.suggestions` /
`data.model` shape), nothing in the WordPress plugin needs to change.
