AI Form Builder
Grounded form fill + form authoring on the shared assistant substrate.
Status: P0–P5 shipped (logic + governance, verified). UI mount + manual deploys outstanding (see §8–9). Date: 2026-06-04.
This is the durable reference for the AI form builder: an assistant that (a) fills an existing form by voice/text and (b) builds a new form from a natural-language prompt, binding fields to real backend service actions across all 16 microservices and to the medbase form-model designer (the “medbase extension”). It supersedes the unrouted mock (AIFormAssistantProvider / VoiceToFormService).
It is built almost entirely by wiring existing infrastructure, not new engines:
- the shared agent loop
runAgentLoop(web/src/services/ai/shared/runner-engine.ts) +SeenMatchesgrounding, - the
OpenAICompatibleLLMclient (web/src/services/ai/shared/client.ts,VITE_LOCAL_LLM_URL), - the
ASSISTANT_REGISTRYdomain contract (web/src/services/ai/registry.ts), - the Universal Form Registry (
@ever-medos/form-registry-types+forms_registry+FormRegistryService), - the gateway’s aggregated OpenAPI (
GET /api/openapi/openapi.json, every action across all services).
1. Two capabilities (keep them distinct)
| Form FILL | Form BUILD | |
|---|---|---|
| Input | clinician narrative (voice/text) | natural-language authoring prompt |
| Output | FormFillPayload (key→value) |
FormBuildPayload (a FormSchema) |
| Grounding | values only for keys on the loaded form | field bindings only to operationIds the agent searched |
| Render | form-prefill (pre-fill the real renderer) |
confirm-gate (review → upsert as draft) |
| Domain id | form-fill |
form-build |
| Roles | any (flag-gated) | admin / super-admin (flag-gated) |
The user’s original ask — “an AI builder that links to the API nest functions across all services and the medbase extension” — is the BUILD path (§4 + §6).
2. Architecture
┌──────────────────────────────────────────────────────┐
│ SHARED AI SUBSTRATE (reused as-is) │
│ runAgentLoop + OpenAICompatibleLLM + SeenMatches │
│ + ASSISTANT_REGISTRY (AssistantDomain contract) │
└───────────────┬───────────────────────┬───────────────┘
┌─────────────────────┘ └─────────────────────┐
▼ form-build (P3) form-fill (P1) ▼
NL prompt → FormSchema voice/text → field values
GROUNDED tools: GROUNDED tools:
searchApiAction / getActionSchema ◄─┐ getFormSchema(formCode) ◄─ forms_registry
searchEntity / getEntitySchema ◄─┤ fillForm(values) [terminal]
proposeFormDefinition [terminal] │
│ confirm-gate │ │ form-prefill (never auto-submit)
▼ │ ▼
FormRegistryService.upsertForm │ existing renderer, pre-filled
(authorMode:'ai', status:'draft') │
│ └─ medbaseFormModelAdapter ──► workflow_form_models (medbase extension)
▼ │
forms_registry (Supabase) │
┌──────────────┴───────────────────────────┐
│ API ACTION CATALOG (P2) │
│ /api/openapi/openapi.json (16 services) │
│ │ ingest-api-catalog edge fn (cron) │
│ ▼ │
│ api_action_catalog (action + entity rows)│
└───────────────────────────────────────────┘
Audit (P5): every run → assistant_run_log (RunnerStep timeline + payload + grounded operationIds).
3. The grounding contract (the safety crux)
Mirrors voice-order’s “no hallucinated catalogId”, enforced in each domain’s dispatcher against the shared SeenMatches map:
- fill —
fillFormkeeps a value only if its key was returned bygetFormSchema; ungrounded keys are dropped intounmatched.fillFormbeforegetFormSchemais rejected. - build —
proposeFormDefinitionkeeps a fieldbindingonly if itsoperationIdwas returned bysearchApiAction/getActionSchema; ungrounded bindings are dropped (field kept, unbound) and reported inwarnings. A form can never persist a reference to an endpoint that does not exist.
This is what makes “links to the API across all services” both true (real operations) and safe (no fabricated endpoints).
4. Files (all shipped)
P0 — field contract
web/packages/form-registry-types/src/index.ts— added optionalFieldBindingtoFormField(operationId,service,direction: read|write,responsePath/requestParam,entityType/entityField).
P1 — form-fill domain
web/src/services/ai/form-fill/{types,tools,prompts,dispatcher,registry-source,index}.ts—runFormFill(),RegistryFormFillSource(overFormRegistryService).web/src/services/ai/registry.ts—formFillDomainregistered.
P2 — API Action Catalog
infrastructure/medbase/migrations/20260604k_api_action_catalog.sql—api_action_catalog(kindaction|entity), trigram search,search_api_actions()RPC, RLS, disabled cron row.infrastructure/medbase/functions/ingest-api-catalog/{index,normalize}.ts— fetch + normalize OpenAPI → upsert + stale-delete. PHI-safe (shape only).web/src/services/forms/apiActionCatalog.service.ts—searchActions/getAction/searchEntities/getEntity/listServices/isPopulated.
P3 — form-build domain
web/src/services/ai/form-build/{types,tools,prompts,dispatcher,catalog-source,index}.ts—runFormBuild(),CatalogFormBuildSource,buildPayloadToUpsertInput().web/src/services/ai/registry.ts—formBuildDomainregistered.
P4 — medbase adapter + runtime binding
web/src/services/forms/medbaseFormModelAdapter.ts—FormSchema ↔ WorkflowFormModel(extension_profile = medos.form.<code>, bindings →field_mappings),saveBuildToMedbase().web/src/services/forms/fieldBindingRuntime.ts— pure planner (collectBindings,planReadCalls,planWriteCalls, dot-path get/set).web/src/services/forms/fieldBindingExecutor.ts— resolves ops via catalog + calls backend via existingcallAPI(read→prefill, write→submit). Explicit/opt-in.
P5 — governance
web/src/services/ai/assistant-access.ts—isDomainEnabled/availableDomains(flag + role gates).web/src/services/ai/assistant-run-audit.ts+infrastructure/medbase/migrations/20260604l_assistant_run_log.sql.@deprecatedbanners onAIFormAssistantProvider.tsx+VoiceToFormService.ts(kept, not deleted).web/scripts/ai-form-builder-smoke.ts— end-to-end regression harness.
5. Canonical target & adapters
- Primary authoring target =
forms_registry/@ever-medos/form-registry-typesFormSchema(authorMode:'ai'). Cleanest LLM target;FormRegistryService.upsertForm. - medbase
WorkflowFormModel= adapter (P4) — satisfies “links to the medbase extension”. - Legacy Toolpad eform = wrap-by-reference via
forms_registry.toolpadAppId(no LLM authoring of Toolpad blobs).
6. Invariants
- No autonomous writes. Build is review-gated before upsert (
status:'draft'); fill pre-fills, never submits. - Grounded references only. No value/binding survives that wasn’t returned by a tool (
SeenMatches). - Spec is read-only doc; execution still needs JWT. Reading the catalog ≠ permission to call; bound calls go through the authenticated gateway via existing
callAPI. - Reuse the write path.
writebindings usecallAPI, never a new projection. - Full audit. Every run →
assistant_run_log. - Gated.
featureFlag+requiredRolesper domain; off by default. - PHI-safe catalog.
api_action_catalogstores API shape only. - Don’t fork the substrate. Domains plug into
runAgentLoop+ASSISTANT_REGISTRY.
7. Verification (deterministic, no live LLM)
| Harness | Asserts | Result |
|---|---|---|
| form-fill dispatcher | grounding, drop ungrounded, reject pre-schema, unknown form | 11/11 |
| form-build dispatcher | grounded binding kept, ungrounded dropped+warned, widget coercion, sectioning | 16/16 |
normalize vs live PH OpenAPI |
4535 rows (2421 actions + 2114 entities, 13 services), upsert-safe | 9/9 |
| P4 adapter + planner | model mapping, round-trip, read/write planning, dot-paths | 19/19 |
| assistant-access | flag+role gating; registry graph loads | 10/10 |
scripts/ai-form-builder-smoke.ts (full runAgentLoop) |
fill + build grounding end-to-end | 8/8 |
Run the durable harness: cd web && npx tsx scripts/ai-form-builder-smoke.ts.
8. Deploy (manual — see root CLAUDE.md “Deployment Status”)
- Migrations (SQL editor /
psql):20260604k_api_action_catalog.sql,20260604l_assistant_run_log.sql. - Edge fn: from
infrastructure/medbase,supabase functions deploy ingest-api-catalog; setGATEWAY_BASE_URL(default = PH ALB); invoke once (POST {}) → expect ~2400 actions + ~2100 entities. - Cron: at
/super-admin/cron-jobs, set the real function URL + service-role key onapi-action-catalog-refreshand enable it. - Frontend env:
VITE_LOCAL_LLM_URL/VITE_LOCAL_LLM_MODELto a reachable OpenAI-compatible endpoint; enable flagsassistant.formFill.enabled/assistant.formBuild.enabledper tenant.
9. Remaining integration (UI mount)
The domains, sources, governance and persistence are wired and verified. What’s left is mounting the assistant surface (drop-in, additive):
- Fill: mount a FAB on the e-form renderer (
web/packages/miniapps/e-form/FormPanel.tsx/ the dynamic-eform dialog) that callsrunFormFill({ source: new RegistryFormFillSource(), llm, formCode })and pre-fills via the existing form state. Reuse the VoiceOrder STT capture. - Build: a “Build with AI” entry in the routed designers (
/admin/eform,/admin/medbase-config→FormModelEditorDialog) that callsrunFormBuild({ source: new CatalogFormBuildSource(), llm }), shows the proposedFormSchemafor review, thenFormRegistryService.upsertForm(buildPayloadToUpsertInput(payload))and/orsaveBuildToMedbase(payload). - Audit: call
logAssistantRun(...)from both surfaces after each run. - Gate: list domains via
availableDomains({ roles, enabledFlags }).
Also pending: add a Key Files row for this doc to root CLAUDE.md (deferred — that file currently has unrelated uncommitted edits).