feat: dynamic form fields, JSON output, branched questionnaires
This commit is contained in:
parent
0b03399c1a
commit
0d43b0e8fb
11 changed files with 2579 additions and 92 deletions
|
|
@ -8,35 +8,111 @@ derives a JSON Schema + uiSchema from the node's template
|
|||
## 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: every entry in its `inputs` list is
|
||||
one web-form field. Add an `lfx.io` input, e.g.
|
||||
```python
|
||||
from lfx.io import DropdownInput
|
||||
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
|
||||
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. Add the new field to `collect()` so it reaches the JSON output.
|
||||
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. Never make form
|
||||
fields required *with* an incoming edge — that's the envelope's job.
|
||||
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.
|
||||
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).
|
||||
|
||||
## 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
|
||||
|
||||
|
|
@ -45,7 +121,10 @@ derives a JSON Schema + uiSchema from the node's template
|
|||
(`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); label = `display_name`; help = `info`.
|
||||
- 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.
|
||||
|
||||
|
|
@ -56,3 +135,9 @@ 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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue