medOS ultra

AI Form Builder

Grounded form fill + form authoring on the shared assistant substrate.

6 min read diagramsUpdated 2026-06-05docs/architecture/ai-form-builder.md

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) + SeenMatches grounding,
  • the OpenAICompatibleLLM client (web/src/services/ai/shared/client.ts, VITE_LOCAL_LLM_URL),
  • the ASSISTANT_REGISTRY domain 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:

  • fillfillForm keeps a value only if its key was returned by getFormSchema; ungrounded keys are dropped into unmatched. fillForm before getFormSchema is rejected.
  • buildproposeFormDefinition keeps a field binding only if its operationId was returned by searchApiAction/getActionSchema; ungrounded bindings are dropped (field kept, unbound) and reported in warnings. 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 optional FieldBinding to FormField (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}.tsrunFormFill(), RegistryFormFillSource (over FormRegistryService).
  • web/src/services/ai/registry.tsformFillDomain registered.

P2 — API Action Catalog

  • infrastructure/medbase/migrations/20260604k_api_action_catalog.sqlapi_action_catalog (kind action|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.tssearchActions/getAction/searchEntities/getEntity/listServices/isPopulated.

P3 — form-build domain

  • web/src/services/ai/form-build/{types,tools,prompts,dispatcher,catalog-source,index}.tsrunFormBuild(), CatalogFormBuildSource, buildPayloadToUpsertInput().
  • web/src/services/ai/registry.tsformBuildDomain registered.

P4 — medbase adapter + runtime binding

  • web/src/services/forms/medbaseFormModelAdapter.tsFormSchema ↔ 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 existing callAPI (read→prefill, write→submit). Explicit/opt-in.

P5 — governance

  • web/src/services/ai/assistant-access.tsisDomainEnabled / availableDomains (flag + role gates).
  • web/src/services/ai/assistant-run-audit.ts + infrastructure/medbase/migrations/20260604l_assistant_run_log.sql.
  • @deprecated banners on AIFormAssistantProvider.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-types FormSchema (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

  1. No autonomous writes. Build is review-gated before upsert (status:'draft'); fill pre-fills, never submits.
  2. Grounded references only. No value/binding survives that wasn’t returned by a tool (SeenMatches).
  3. 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.
  4. Reuse the write path. write bindings use callAPI, never a new projection.
  5. Full audit. Every run → assistant_run_log.
  6. Gated. featureFlag + requiredRoles per domain; off by default.
  7. PHI-safe catalog. api_action_catalog stores API shape only.
  8. 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”)

  1. Migrations (SQL editor / psql): 20260604k_api_action_catalog.sql, 20260604l_assistant_run_log.sql.
  2. Edge fn: from infrastructure/medbase, supabase functions deploy ingest-api-catalog; set GATEWAY_BASE_URL (default = PH ALB); invoke once (POST {}) → expect ~2400 actions + ~2100 entities.
  3. Cron: at /super-admin/cron-jobs, set the real function URL + service-role key on api-action-catalog-refresh and enable it.
  4. Frontend env: VITE_LOCAL_LLM_URL / VITE_LOCAL_LLM_MODEL to a reachable OpenAI-compatible endpoint; enable flags assistant.formFill.enabled / assistant.formBuild.enabled per 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 calls runFormFill({ 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-configFormModelEditorDialog) that calls runFormBuild({ source: new CatalogFormBuildSource(), llm }), shows the proposed FormSchema for review, then FormRegistryService.upsertForm(buildPayloadToUpsertInput(payload)) and/or saveBuildToMedbase(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).

Ask Anything