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

206 lines
8.9 KiB
Markdown

# 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.
```python
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`:
```python
(
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):
```python
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.