refactor: tighten backend, flows, frontend, tests; push flows to Langflow
This commit is contained in:
parent
6688fb08c6
commit
06ab25fb47
31 changed files with 783 additions and 358 deletions
147
backend/components/kirche/_form_core.py
Normal file
147
backend/components/kirche/_form_core.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
"""Shared form runtime core (single source, codegen-inlined for LFX).
|
||||
|
||||
The LFX runtime executes embedded module source without our ``kirche``
|
||||
package, so flow JSON must stay self-contained. Author components against
|
||||
this module (``from kirche._form_core import ...`` or ``from ._form_core``
|
||||
); ``scripts/make_flow_fixtures.py:kirche_node()`` inlines this file's text
|
||||
into the exported ``code`` so the runtime keeps working with one truth in
|
||||
the repo.
|
||||
|
||||
Supports both the legacy ``[visible_if feld=Wert]`` marker in ``info``
|
||||
and the structured ``visible_if`` key (list of {field, value} or dict):
|
||||
structured wins when present, markers stay as deprecated fallback.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
_VISIBLE_RE = re.compile(r"\[visible_if\s+([^\]]+)\]")
|
||||
|
||||
#: Reserved input carrying the run envelope — never a form field name.
|
||||
PLUMBING_INPUTS = frozenset({"form_values", "code"})
|
||||
|
||||
|
||||
def _text(value) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
if hasattr(value, "text"):
|
||||
return value.text or ""
|
||||
return str(value)
|
||||
|
||||
|
||||
def _json_safe(value):
|
||||
"""Plain JSON value: 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)
|
||||
|
||||
|
||||
def _split_conditions(text: str) -> list:
|
||||
parts: list = []
|
||||
buf: list = []
|
||||
quoted = False
|
||||
for char in text:
|
||||
if char == '"':
|
||||
quoted = not quoted
|
||||
buf.append(char)
|
||||
elif char == "&" and not quoted:
|
||||
parts.append("".join(buf))
|
||||
buf = []
|
||||
else:
|
||||
buf.append(char)
|
||||
parts.append("".join(buf))
|
||||
return [part.strip() for part in parts if part.strip()]
|
||||
|
||||
|
||||
def _parse_condition(token: str):
|
||||
field, sep, value = token.partition("=")
|
||||
field, value = field.strip(), value.strip()
|
||||
if not sep or not field or not value:
|
||||
return None
|
||||
if len(value) >= 2 and value.startswith('"') and value.endswith('"'):
|
||||
value = value[1:-1]
|
||||
lowered = value.lower()
|
||||
if lowered == "true":
|
||||
return field, True
|
||||
if lowered == "false":
|
||||
return field, False
|
||||
try:
|
||||
return field, int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
return field, float(value)
|
||||
except ValueError:
|
||||
pass
|
||||
return field, value
|
||||
|
||||
|
||||
def _field_conditions(info: object, structured=None) -> list:
|
||||
"""[(trigger, value)] from structured key or info markers.
|
||||
|
||||
Structured ``visible_if`` (list of ``{field, value}`` or ``{field: value}``
|
||||
dict) wins; legacy ``[visible_if ...]`` markers in ``info`` stay as
|
||||
deprecated fallback.
|
||||
"""
|
||||
if structured:
|
||||
conditions: list = []
|
||||
items = structured.items() if isinstance(structured, dict) else structured
|
||||
for item in items or []:
|
||||
if isinstance(item, dict) and "field" in item:
|
||||
conditions.append((item["field"], item.get("value")))
|
||||
elif isinstance(item, (list, tuple)) and len(item) == 2:
|
||||
conditions.append((item[0], item[1]))
|
||||
if conditions:
|
||||
return conditions
|
||||
conditions = []
|
||||
|
||||
def _collect(match) -> str:
|
||||
for token in _split_conditions(match.group(1)):
|
||||
parsed = _parse_condition(token)
|
||||
if parsed is not None:
|
||||
conditions.append(parsed)
|
||||
return ""
|
||||
|
||||
_VISIBLE_RE.sub(_collect, info if isinstance(info, str) else "")
|
||||
return conditions
|
||||
|
||||
|
||||
def collect_form_data(field_names: list, field_values: dict, payload: dict, asked=None) -> dict:
|
||||
"""Shared collect(): effective values, visibility scope, submitted wins."""
|
||||
effective = dict(field_values)
|
||||
for key, val in payload.items():
|
||||
if isinstance(key, str) and key:
|
||||
effective[key] = _json_safe(val)
|
||||
scope = [n for n in (asked if asked is not None else field_names) if n in field_names]
|
||||
return effective, scope
|
||||
|
||||
|
||||
def parse_envelope(text_value) -> tuple[dict, list | None]:
|
||||
"""(payload, asked-fields) from the run envelope text."""
|
||||
try:
|
||||
envelope = json.loads(_text(text_value) or "{}")
|
||||
except (json.JSONDecodeError, ValueError, AttributeError):
|
||||
return {}, None
|
||||
if not isinstance(envelope, dict):
|
||||
return {}, None
|
||||
if isinstance(envelope.get("payload"), dict):
|
||||
fields = envelope.get("fields")
|
||||
asked = (
|
||||
[i for i in fields if i]
|
||||
if isinstance(fields, list) and all(isinstance(i, str) for i in fields)
|
||||
else None
|
||||
)
|
||||
return envelope["payload"], asked
|
||||
return envelope, None
|
||||
Loading…
Add table
Add a link
Reference in a new issue