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

6.2 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).

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. 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.