kirche-einfach/backend/components/kirche/README.md

8.9 KiB

Authoring web forms (CustomInputForm)

Flows that should render a web form start with a CustomInputForm node (this package — the kirche category in the Langflow palette). The API derives a JSON Schema + uiSchema from the node's template (app/flows/input_schema.py) and the webapp renders it with RJSF.

Adding a new form field

  1. Open the Langflow authoring UI (http://localhost:7860) and load the flow from LANGFLOW_LOAD_FLOWS_PATH (canonical files are preloaded).
  2. Edit the CustomInputForm node in its code editor: every entry in its inputs list is one web-form field. Add an lfx.io input, e.g.
    from lfx.io import DropdownInput
    
    DropdownInput(
        name="room",  # JSON-Schema property key (freely renamable)
        display_name="Room",  # form label (title)
        info="Where should it take place",  # help text (description)
        options=["Chapel", "Hall"],  # dropdown choices (enum)
        value="Chapel",  # prefill (default)
        required=True,  # marks the field required
    )
    
    Supported input types → widgets: StrInput/MessageTextInput → text, MultilineInput → textarea, SecretStrInput → password, IntInput → integer, FloatInput → number, BoolInput → checkbox, DropdownInput → select, MultiselectInput → multi-select, SliderInput → slider (needs range_spec), FileInput → text stub (real upload later).
  3. Nothing else to change in code: collect() is dynamic — it emits every input except the plumbing ones, so renames, additions, and removals just work. (The inspector panel can tweak display_name, info, value, and options of existing fields, but name and new fields need the code editor.)
  4. Wire the envelope: keep the ChatInput → form_values edge (hidden input, show=False). Submitted web-form values arrive there as {"payload": {...}} and are merged over the defaults — including keys the node code does not declare (forward-compatible with renames). Never make form fields required with an incoming edge — that's the envelope's job.
  5. Export the flow (JSON) into backend/flows/<name>.json and commit. The API watcher syncs + deploys it automatically; the form schema is regenerated per version.

Branched questionnaires (visible_if)

A field can appear only when other fields have certain values — nested to any depth. Declare it in the code editor with a marker in info:

(
    DropdownInput(
        name="gebaeude",
        display_name="Welches Gebäude soll verändert werden?",
        options=["Kirche", "Anderes Gebäude"],
        value="Kirche",
        required=True,
    ),
)
(
    MultiselectInput(
        name="bestandteil",
        display_name="Welcher Bestandteil der Kirche?",
        options=["Glocke", "Orgel", "Innenraum", "Fenster", "Fassade"],
        value=[],
        info="Mehrfachauswahl möglich [visible_if gebaeude=Kirche]",
        required=True,
    ),
)
(
    DropdownInput(
        name="glockenmassnahme",
        display_name="Welche Maßnahme an der Glocke?",
        options=["Reinigung", "Reparatur", "Neuguss"],
        value="Reinigung",
        info="[visible_if bestandteil=Glocke]",
    ),
)

Syntax: [visible_if <feld>=<wert>], & for AND ([visible_if a=X & b=Y]), double quotes for values with spaces ([visible_if gebaeude="Anderes Gebäude"]). Trigger matching by input type: Dropdown/Text const, Bool const (true/false), Multiselect contains, Int/Float exact number. Nesting is automatic: a field whose trigger is itself conditional renders one level deeper, without limit.

What the parser does with markers:

  • The marker is stripped from the shown help text; the field leaves the top-level schema and lives only in its allOf/if/then branch, so RJSF hides it until the condition holds. ui:order keeps template order, so new questions appear directly below their trigger.
  • required on a branched field applies only while visible.
  • Values are always kept: hidden fields still validate and are submitted (stale values can keep deeper branches satisfied until changed).
  • Safety is fail-open: unknown triggers (renamed?), unsupported trigger types, self-references, and trigger cycles keep the field always visible instead of hiding data. Renaming a trigger means updating its markers.

See backend/flows/bauvorhaben.json for a full 4-level example (Gebäude → Bestandteil → Maßnahme → Gussjahr, plus sibling branch and bool trigger).

User-built form components (no kirche base needed)

A plain CustomComponent whose display name ends with "Form" is also parsed into a web form — same template contract, no base class. To make submitted values reach it at runtime, add an envelope input and merge it in the output method (pattern):

from lfx.io import MessageTextInput  # add to imports
import json

# add to inputs:
MessageTextInput(name="form_values", display_name="Form values", value="", show=False)

# at the top of the output method:
try:
    raw = self.form_values.text if hasattr(self.form_values, "text") else self.form_values
    submitted = json.loads(raw or "{}")
except Exception:
    submitted = {}
if isinstance(submitted.get("payload"), dict):
    submitted = submitted["payload"]


def val(name, default):
    return submitted.get(name, default)

Then use val("rolle", self.rolle) etc., and wire a ChatInput node into form_values on the canvas. Branch markers ([visible_if ...] in info) work here too — the parser reads the template only.

Asked fields only (run envelope fields)

The API sends the asked field names (from the version's form schema) in the run envelope's fields list. collect() then emits defaults only for asked AND currently visible fields (branch conditions are evaluated against submitted values over code defaults, so hidden fields stay out exactly as the frontend never submits them): the template may declare fields unknown to this code (they arrive via envelope and merge normally), and unasked code defaults stay out of the output. Without the list — raw dicts, Langflow playground — all visible declared defaults apply as before.

Variant flows (own component per flow)

A flow whose fields differ from CustomInputForm gets its own standalone component (see bauvorhaven_form.py → "Bauvorhaben Form" in the palette): same class pattern, own inputs list. Standalone means it: no imports between form modules — the LFX runtime executes the embedded module source without our kirche package, so every form component must be self-contained (stdlib + lfx only). Copy the file as the starting point for future forms.

Rule: code-declared inputs must equal template fields (test_embedded_code_matches_template_fields enforces it; required Multiselect uses the [" "] sentinel). Generate node + flow with kirche_node(..., component_cls, code_override) and build_bauvorhaben() in backend/scripts/make_flow_fixtures.py rather than hand-editing JSON. Restart Langflow afterwards so the palette picks up the new component, then build/upload the flow.

Output type: Data (JSON), not Message

collect() returns Data(data={...}) — a real JSON object. Consequences:

  • A terminal ChatOutput renders it as a fenced ```json block; the run result text is that block, not raw JSON (unwrap before json.loads).
  • Components consuming form_data should read .data (or get_text()), not .text like with Message.

Conventions the pipeline relies on

  • required=True on a template field: lfx validate --level 4 demands a value or an edge, so the canonical-flow generator (backend/scripts/make_flow_fixtures.py) writes a single-space sentinel value for such fields ([" "] for Multiselect, which rejects a plain string at runtime). The form parser treats whitespace-only values as "no default" — don't ship real whitespace defaults.
  • Field key = name (stable, freely renamable but must stay unique and non-empty; collisions get a _2/_3 suffix in the form schema); label = display_name; help = info. Renames propagate to downstream components, which read the JSON keys as-is.
  • Unknown future input types degrade to a plain text field in the web form (parser fallback), so adding exotic lfx inputs never breaks runs.

Regenerating the bundled fixtures

After editing the component source (form_inputs.py), refresh the canonical + test-fixture exports:

docker compose exec api /opt/venv/bin/python scripts/make_flow_fixtures.py
docker compose exec -T api uv run pytest tests/test_lfx_validate.py

Notes: the generator writes UTF-8/LF explicitly (Windows-safe), and backend/flows/*.json may carry Langflow round-trip state (key order) the generator does not reproduce — if a regen rewrites canonical files beyond the component's own node, revert them and splice only the changed node fields (code, description, output types, edge handles) instead.