Skip to content

Forms class taxonomy + classifier (epic #2806, Phase B)

tools/formspec/formspec/form_class.py gives the AI form rebuild (formspec.from_detected) class-specific design knowledge. Real-world forms cluster into a small number of recurring kinds β€” a W-4, a patient-intake packet, a purchase order β€” and each kind has a conventional shape people expect: the section order, where the signature goes, where the legal fine print sits. Phase B captures that knowledge as data plus a classifier; Phase B2 (later) wires it into the rebuild prompt.

The taxonomy

FormClass β€” 12 classes:

ClassCovers
tax_governmentW-4/W-9/1099-style official filings, benefits/DMV forms
employment_applicationjob applications
patient_intakenew-patient / visit intake packets
consent_waiverpermission slips, releases of liability, photo/medical consent
registration_enrollmentprogram/class/camp/event sign-up
order_formpurchase orders with line items and totals
survey_feedbacksatisfaction/evaluation questionnaires
membershipjoin/renew an association, club, gym
incident_reportaccident/injury/safety event records
timesheetpay-period hour grids with dual approval
invoicebills requesting payment
genericexplicit β€œnone of the above” fallback

Template fields (ClassTemplate, one per class in CLASS_TEMPLATES)

  • display_name / description β€” human name and what the class covers (the description also feeds the AI classification prompt).
  • canonical_sections β€” the conventional reading order as (section name, short guidance) pairs. The order is the knowledge (e.g. patient intake: demographics β†’ insurance β†’ medical history β†’ medications/allergies β†’ emergency contact β†’ consent/signature).
  • autocomplete_hints β€” case-insensitive label-regex β†’ HTML autofill token (WCAG 1.3.5). These are class-specific complements to the generic mapper in workers/api/src/services/form-label-jury.ts (inferAutocomplete), not a copy: only a tax form should map β€œfiling status”, only an intake form should map β€œsex”. Sensitive identifiers (SSN/EIN, member IDs) map to off β€” a valid token that satisfies 1.3.5 while preventing browser autofill. All tokens are validated in tests against VALID_AUTOCOMPLETE_TOKENS (the jury’s emit-set plus a few class-specific WHATWG tokens: sex, honorific-prefix, tel-extension).
  • signature_convention β€” none / single / dual plus a placement note (e.g. timesheet: employee certifies, then supervisor approves).
  • legal_text_placement β€” none / top / bottom plus a note (e.g. a waiver’s terms MUST precede the signature; a survey’s anonymity note goes in the introduction).
  • prompt_guidance β€” 2-4 sentences of best-practice instruction destined verbatim for the rebuild prompt.

Classifier

Two layers, deterministic-first:

  1. classify_form_heuristic(text, field_labels) -> (FormClass, confidence) β€” zero-cost weighted keyword scoring over the case-folded document text + field labels (word-bounded regexes, so OCR-ish text works; each pattern counts once so repetition can’t swamp the vote). Confidence scales linearly to 1.0 at a score of 6; ties are broken by enum order with damped confidence. No signal at all (including empty text) β†’ (generic, 0.0).
  2. classify_form(text, field_labels, ai=None) -> FormClassification β€” when ai (an injected prompt -> reply_text callable, following formspec.ai’s injection pattern so tests need no SDK/network) is given, the model is asked to pick one class + confidence + one-line rationale as strict JSON. The reply is validated against the enum; on any parse failure, unknown class, exception, or confidence < 0.5 it falls back to the heuristic. ai=None β†’ heuristic only. The result’s source field ('ai' | 'heuristic') always says which path answered; it never raises for a bad model reply.

FormClassification carries form_class, confidence (clamped 0-1), source, rationale, and a .template convenience property.

Prompt rendering

prompt_addendum(classification) -> str renders the class’s prompt_guidance, numbered canonical sections, and signature/legal-text conventions into a text block ready to append to the rebuild prompt. Sections are framed as guidance, not hard rules (β€œskip a section when no field belongs to it; never invent fields to fill one”), preserving from_detected’s guardrail philosophy: the model only ever controls presentation β€” canonical field identity is always re-imposed.

Phase B2 wiring (DONE β€” epic #2806)

  1. formspec/from_detected.py classifies before the rebuild β€” an explicit form_class override wins (source "override", confidence 1.0; unknown values are logged and ignored), otherwise classify_form runs AI-first through the same ai._call_messages seam with the heuristic as fallback β€” and appends prompt_addendum(classification) to _build_prompt’s prompt. Auto-classified GENERIC omits the addendum (no signal β†’ no boilerplate); an explicit GENERIC override is honoured with it.
  2. The report carries report["classification"] (class_id, confidence, source, ai_attempted, rationale, plus that call’s own input_tokens/output_tokens/cost_usd); report["cost_usd"] is the TOTAL across the classification and rebuild calls, and the form-place executor books it to cost_ledger whenever it is non-zero (a fallback rebuild can still carry real classification spend).
  3. The rebuild choice dialog (apps/forms RebuildChoiceDialog) has a β€œWhat kind of form is this?” select β€” auto-detect default plus the 12 classes β€” threaded through useFormDetect.confirm β†’ PUT /api/forms/detect/:jobId/confirm (formClass, allowlist-validated against FORM_CLASS_IDS) β†’ the form-place SQS message β†’ the sidecar’s /rebuild body (form_class). The id list is mirrored in three places (Python enum, route allowlist, dialog constant) β€” keep them in sync.
  4. Still open: apply the class’s autocomplete_hints when emitting widgets, as a class-aware complement to the generic form-label-jury pass.

Tests

tools/formspec/tests/test_form_class.py β€” every class reachable via the heuristic with a representative fixture, AI happy path + all fallback paths (malformed reply, unknown class, low/non-numeric confidence, raising callable), confidence bounds/clamping, prompt_addendum content and ordering, and CLASS_TEMPLATES completeness (all 12 present, all fields non-empty, all autocomplete tokens valid regexes drawn from the valid set).

Run: cd tools/formspec && python3 -m pytest