feat: asked-field envelope, standalone form components, branched forms
This commit is contained in:
parent
0d43b0e8fb
commit
f27be044b4
26 changed files with 3651 additions and 104 deletions
101
docs/new_workflow_module.md
Normal file
101
docs/new_workflow_module.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# New workflow module: form flow end to end
|
||||
|
||||
How to add a form-based workflow (web form → run → result), learned from
|
||||
building intake, Bauvorhaben, and Mitarbeiter-Einstellen. Three layers:
|
||||
component (Langflow palette), flow (canvas), backend parsing (automatic).
|
||||
|
||||
## 1. Form component (`backend/components/kirche/<name>_form.py`)
|
||||
|
||||
Copy `mitarbeiter_form.py` (small) or `bauvorhaben_form.py` (branching).
|
||||
Rename the class with a **`CustomInput*` prefix** (e.g.
|
||||
`CustomInputMitarbeiter`) — the backend parser matches that prefix; a
|
||||
plain `CustomComponent` name additionally needs a display name ending
|
||||
in `"Form"`.
|
||||
|
||||
Rules, all load-bearing:
|
||||
|
||||
- **Self-contained**: stdlib + `lfx` only. NEVER import from sibling
|
||||
form modules — LFX executes the embedded module source without our
|
||||
package, and Langflow round-trips the code. Duplicated helpers are
|
||||
structural, not laziness.
|
||||
- **Inputs**: one `lfx.io` entry per question (`name` = stable JSON key,
|
||||
`display_name` = label, `info` = help, `options`/`value`/`required`
|
||||
as needed) plus the hidden `form_values` plumbing input (reserved
|
||||
name — never use it for a field).
|
||||
- **Branching**: `[visible_if feld=Wert]` markers in `info`
|
||||
(`&` for AND, quotes for spaces). Any depth; trigger types:
|
||||
Dropdown/Text `const`, Bool, Multiselect `contains`, Int/Float exact.
|
||||
Stale markers, cycles, and unknown triggers fail open (always visible).
|
||||
- **Required sentinels**: required fields with empty values carry
|
||||
`" "` (`[" "]` for Multiselect — a plain string crashes LFX at build,
|
||||
`[]` fails validation). The parser treats them as "no default".
|
||||
- **Outputs**: exactly `Output(name="form_data", method="collect")`
|
||||
returning `Data`. Do not touch `collect()` — it is fully dynamic
|
||||
(asked + visible defaults, submitted values win).
|
||||
- Export in `backend/components/kirche/__init__.py`.
|
||||
|
||||
Then: `docker compose restart langflow` (components load at startup),
|
||||
check the palette via `GET /api/v1/all` (or sidebar search, hard-refresh
|
||||
the browser tab if the entry is missing — the API is source of truth).
|
||||
|
||||
## 2. Flow JSON (`backend/flows/<name>.json`)
|
||||
|
||||
Never hand-edit: add `build_<name>()` in
|
||||
`backend/scripts/make_flow_fixtures.py` and run it (from `backend/`).
|
||||
|
||||
- Form node via `kirche_node(node_id, component_cls=..., code_override=...)`.
|
||||
- Logic nodes via `custom_node(CODE, ...)` (keep return annotations like
|
||||
`-> Message`: edge handles derive output types from them).
|
||||
- Always wire `ChatInput → form_values` (run envelope), then
|
||||
`form_data → … → ChatOutput`.
|
||||
- Reorder template keys to question order (lfx emits alphabetical; RJSF
|
||||
renders insertion order, and `ui:order` follows it).
|
||||
- The script writes fixtures + canonical files; `git checkout` the churn
|
||||
on unrelated flows, keep canonical files CRLF like the repo.
|
||||
- Custom logic code (e.g. salary math) lives as a `*_CODE` constant in
|
||||
the script — one owner for template and code.
|
||||
|
||||
## 3. Backend parsing (automatic, know the contract)
|
||||
|
||||
`app/flows/input_schema.py` turns the node template into
|
||||
`{json_schema, ui_schema}` per version: `name` → key (verbatim; empty
|
||||
skipped, collisions suffixed), `display_name`/`info`/`value`/`options`
|
||||
→ title/description/default/enum, `show: false` skipped, unknown types
|
||||
degrade to text. Markers become `allOf`/`if`/`then` branches;
|
||||
branch-`required` applies only while visible.
|
||||
|
||||
Runs carry an envelope (`payload`, `fields` = asked names from the
|
||||
cached schema, run context). `collect()` emits defaults only for asked
|
||||
AND visible fields; submitted values always win; hidden values are kept
|
||||
but never inventoried. Raw dicts and the Langflow playground use all
|
||||
visible declared defaults.
|
||||
|
||||
## 4. Langflow round-trip (where flows go to die, if careless)
|
||||
|
||||
- Node **type names must be distinct per component** — Langflow
|
||||
normalizes same-type nodes to the installed class on import and
|
||||
silently discards variant templates.
|
||||
- Upload via `POST /api/v1/flows/upload/`, then fetch the flow back and
|
||||
assert node type, template fields, and code are intact.
|
||||
- Restart re-seeds `backend/flows/` into Langflow; the sync button pulls
|
||||
Langflow → files **matched by flow name, overwriting silently**.
|
||||
Commit before syncing, review the `pulled` report, never rename or
|
||||
repurpose flows casually.
|
||||
- Experiments that must stay out: list their stem in
|
||||
`backend/flows/.flowsignore` (skipped by sync, deploy, validation,
|
||||
frontend, and future pulls).
|
||||
|
||||
## 5. Verify before reporting done
|
||||
|
||||
- `uv run pytest` (full suite incl. real-LFX deploy+run e2e) green.
|
||||
- `uv run ruff check .` clean, `uv run ruff format --check .` clean.
|
||||
- `uv run lfx validate --level 4 tests/fixtures/flows/` all pass.
|
||||
- Live: `GET /api/flows/{id}/input-schema` shows `fallback: null`
|
||||
with expected properties (cached schemas refresh only on new
|
||||
versions — unchanged files keep their cached schema).
|
||||
- Browser click-through in Workflows, including a branched path and an
|
||||
empty submit (branch-required errors).
|
||||
|
||||
New behavior ships with tests; schema changes ship as Alembic revisions
|
||||
(never edit applied migrations); commit on `feat/<short>`, never
|
||||
directly on `main`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue