feat: asked-field envelope, standalone form components, branched forms

This commit is contained in:
Jonas Miederer 2026-09-28 14:07:09 +02:00
parent 0d43b0e8fb
commit f27be044b4
26 changed files with 3651 additions and 104 deletions

View file

@ -1,8 +1,10 @@
"""Derive a web-form schema from a Langflow export.
Looks for ``CustomInput*`` nodes (``data.type`` or the inner
``display_name`` starting with ``CustomInput`` — see
``backend/components/kirche/form_inputs.py``) and maps their ``template``
Looks for form nodes — ``CustomInput*`` (``data.type`` or the inner
``display_name`` starting with ``CustomInput``, see
``backend/components/kirche/form_inputs.py``) plus user-built
``CustomComponent`` nodes whose display name ends with ``"Form"``
(e.g. ``Mitarbeiter Einstellen Form``) — and maps their ``template``
(minus ``code``) to ``{json_schema, ui_schema}`` for RJSF:
* ``name`` → property key (freely renamable in the Langflow code editor;
@ -39,7 +41,7 @@ _VISIBLE_RE = re.compile(r"\[visible_if\s+([^\]]+)\]")
def _form_templates(langflow_json: dict) -> list[dict]:
"""Inner node dicts (``data.node``) of all CustomInput* nodes."""
"""Inner node dicts (``data.node``) of all form nodes."""
if not isinstance(langflow_json, dict):
return []
data = langflow_json.get("data") or {}
@ -54,6 +56,10 @@ def _form_templates(langflow_json: dict) -> list[dict]:
display = str(inner.get("display_name") or "")
if node_type.startswith("CustomInput") or display.startswith("CustomInput"):
found.append(inner)
elif node_type == "CustomComponent" and display.endswith("Form"):
# User-built form components (e.g. "Mitarbeiter Einstellen
# Form"): same template contract, no kirche base needed.
found.append(inner)
return found
@ -328,3 +334,37 @@ def extract_form_schema(langflow_json: dict) -> dict:
elif advanced:
ui_schema["ui:order"] = normal + advanced + ["*"]
return {"json_schema": json_schema, "ui_schema": ui_schema}
def schema_field_names(cached: dict) -> list[str]:
"""All asked field names of a cached form schema (root + branches).
Used for the run envelope's ``fields`` list so the runtime emits
defaults only for asked fields. Order: root properties first, then
branch discoveries.
"""
if not isinstance(cached, dict):
return []
json_schema = cached.get("json_schema") or {}
names: list[str] = []
def _walk(node: object) -> None:
if not isinstance(node, dict):
return
for subschema in (node.get("then"), node.get("else")):
if isinstance(subschema, dict):
for key in subschema.get("properties") or {}:
if key not in names:
names.append(key)
_walk(subschema)
for key in ("allOf", "oneOf", "anyOf"):
items = node.get(key) or []
if isinstance(items, list):
for item in items:
_walk(item)
for key in json_schema.get("properties") or {}:
if key not in names:
names.append(key)
_walk(json_schema)
return names