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
|
|
@ -11,6 +11,17 @@ keyed by ``name``. Field metadata maps as follows:
|
|||
* ``required`` → required array, ``advanced`` → ``ui:collapsed`` hint
|
||||
* ``_input_type`` → JSON type + widget (see ``app/flows/input_schema.py``)
|
||||
|
||||
``collect()`` is fully dynamic: it emits every input except the plumbing
|
||||
ones (``form_values``, ``code``), so fields can be renamed, added, or
|
||||
removed in the Langflow code editor without touching this method.
|
||||
``name`` is the stable JSON key (freely renamable, must stay unique and
|
||||
non-empty); downstream components read the keys as-is.
|
||||
|
||||
``collect()`` returns ``Data`` (the LFX JSON type), not ``Message``: the
|
||||
values travel as a real JSON object. A terminal ``ChatOutput`` renders it
|
||||
as a fenced ```json block; components consuming it should read ``.data``
|
||||
(or ``get_text()``), not ``.text``.
|
||||
|
||||
``FileInput`` is a stub for now: it carries a string reference only, real
|
||||
upload comes later.
|
||||
"""
|
||||
|
|
@ -36,7 +47,7 @@ from lfx.io import (
|
|||
SliderInput,
|
||||
StrInput,
|
||||
)
|
||||
from lfx.schema.message import Message
|
||||
from lfx.schema.data import Data
|
||||
|
||||
|
||||
def _text(value) -> str:
|
||||
|
|
@ -47,9 +58,30 @@ def _text(value) -> str:
|
|||
return str(value)
|
||||
|
||||
|
||||
def _json_safe(value):
|
||||
"""Plain JSON value for one field: unwrap LFX wrappers, keep scalars."""
|
||||
if value is None or isinstance(value, (bool, int, float, str)):
|
||||
return value
|
||||
if isinstance(value, list):
|
||||
return [_json_safe(item) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {str(key): _json_safe(item) for key, item in value.items()}
|
||||
text = getattr(value, "text", None)
|
||||
if isinstance(text, str):
|
||||
return text
|
||||
data = getattr(value, "data", None)
|
||||
if isinstance(data, dict):
|
||||
return _json_safe(data)
|
||||
return str(value)
|
||||
|
||||
|
||||
#: Inputs that are plumbing, not form fields (never part of the output).
|
||||
PLUMBING_INPUTS = frozenset({"form_values", "code"})
|
||||
|
||||
|
||||
class CustomInputForm(Component):
|
||||
display_name = "CustomInput Form"
|
||||
description = "Collects structured form values into a single JSON message."
|
||||
description = "Collects structured form values into a single JSON object."
|
||||
icon = "ClipboardList"
|
||||
|
||||
inputs: ClassVar[list] = [
|
||||
|
|
@ -134,29 +166,49 @@ class CustomInputForm(Component):
|
|||
Output(display_name="Form Data", name="form_data", method="collect"),
|
||||
]
|
||||
|
||||
def collect(self) -> Message:
|
||||
data = {
|
||||
"full_name": _text(self.full_name),
|
||||
"notes": _text(self.notes),
|
||||
"password": _text(self.password),
|
||||
"age": self.age,
|
||||
"amount": self.amount,
|
||||
"newsletter": self.newsletter,
|
||||
"department": _text(self.department),
|
||||
"topics": self.topics,
|
||||
"satisfaction": self.satisfaction,
|
||||
"attachment": _text(self.attachment),
|
||||
}
|
||||
def _field_names(self) -> list:
|
||||
"""Names of all form fields (every input except the plumbing ones)."""
|
||||
inputs = getattr(self, "_inputs", None)
|
||||
if isinstance(inputs, dict) and inputs:
|
||||
return [name for name in inputs if name not in PLUMBING_INPUTS]
|
||||
seen = []
|
||||
declared = getattr(self, "inputs", None) or getattr(type(self), "inputs", None) or []
|
||||
for inp in declared:
|
||||
name = getattr(inp, "name", None)
|
||||
if name and name not in PLUMBING_INPUTS and name not in seen:
|
||||
seen.append(name)
|
||||
return seen
|
||||
|
||||
def _field_value(self, name: str):
|
||||
"""Runtime value of one input.
|
||||
|
||||
Reads ``_inputs`` directly: a field renamed to e.g. ``name`` would
|
||||
otherwise resolve to the component attribute instead of its value.
|
||||
"""
|
||||
inputs = getattr(self, "_inputs", None)
|
||||
if isinstance(inputs, dict) and name in inputs:
|
||||
return getattr(inputs[name], "value", None)
|
||||
try:
|
||||
return getattr(self, name, None)
|
||||
except (AttributeError, ValueError):
|
||||
return None
|
||||
|
||||
def collect(self) -> Data:
|
||||
data = {}
|
||||
for name in self._field_names():
|
||||
data[name] = _json_safe(self._field_value(name))
|
||||
# Submitted values (via form_values) win over the field defaults.
|
||||
# Accepts the raw dict or the run envelope {"payload": {...}}.
|
||||
# Unknown keys are kept: the frontend may already know renamed
|
||||
# fields this code revision does not declare.
|
||||
try:
|
||||
override = json.loads(_text(self.form_values) or "{}")
|
||||
except json.JSONDecodeError:
|
||||
except (json.JSONDecodeError, ValueError, AttributeError):
|
||||
override = {}
|
||||
if isinstance(override, dict):
|
||||
if isinstance(override.get("payload"), dict):
|
||||
override = override["payload"]
|
||||
for key in data:
|
||||
if key in override:
|
||||
data[key] = override[key]
|
||||
return Message(text=json.dumps(data, ensure_ascii=False))
|
||||
for key, val in override.items():
|
||||
if isinstance(key, str) and key:
|
||||
data[key] = _json_safe(val)
|
||||
return Data(data=data)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue