kirche-einfach/backend/tests/fixtures/flows/mitarbeiter_einstellen.json

991 lines
62 KiB
JSON

{
"data": {
"nodes": [
{
"data": {
"node": {
"template": {
"_type": "Component",
"files": {
"tool_mode": false,
"trace_as_metadata": true,
"file_path": "",
"fileTypes": [
"csv",
"json",
"pdf",
"txt",
"md",
"mdx",
"yaml",
"yml",
"xml",
"html",
"htm",
"docx",
"py",
"sh",
"sql",
"js",
"ts",
"tsx",
"jpg",
"jpeg",
"png",
"bmp",
"image"
],
"temp_file": true,
"list": true,
"list_add_label": "Add More",
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "files",
"value": "",
"display_name": "Files",
"advanced": true,
"api_editable": false,
"dynamic": false,
"info": "Files to be sent with the message.",
"title_case": false,
"track_in_telemetry": false,
"type": "file",
"_input_type": "FileInput"
},
"code": {
"type": "code",
"required": true,
"placeholder": "",
"list": false,
"show": true,
"multiline": true,
"value": "from lfx.base.data.utils import IMG_FILE_TYPES, TEXT_FILE_TYPES\nfrom lfx.base.io.chat import ChatComponent\nfrom lfx.inputs.inputs import BoolInput\nfrom lfx.io import (\n DropdownInput,\n FileInput,\n MessageTextInput,\n MultilineInput,\n Output,\n)\nfrom lfx.schema.image import get_file_paths\nfrom lfx.schema.message import Message\nfrom lfx.services.deps import get_settings_service\nfrom lfx.utils.constants import (\n MESSAGE_SENDER_AI,\n MESSAGE_SENDER_NAME_USER,\n MESSAGE_SENDER_USER,\n)\nfrom lfx.utils.file_path_security import (\n component_file_access_scopes,\n enforce_local_file_access,\n is_local_file_access_restricted,\n)\n\n\nclass ChatInput(ChatComponent):\n display_name = \"Chat Input\"\n description = \"Get chat inputs from the Playground.\"\n documentation: str = \"https://docs.langflow.org/chat-input-and-output\"\n icon = \"MessagesSquare\"\n name = \"ChatInput\"\n minimized = True\n\n inputs = [\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input Text\",\n value=\"\",\n info=\"Message to be passed as input.\",\n input_types=[],\n ),\n BoolInput(\n name=\"should_store_message\",\n display_name=\"Store Messages\",\n info=\"Store the message in the history.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER],\n value=MESSAGE_SENDER_USER,\n info=\"Type of sender.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Name of the sender.\",\n value=MESSAGE_SENDER_NAME_USER,\n advanced=True,\n ),\n MessageTextInput(\n name=\"session_id\",\n display_name=\"Session ID\",\n info=\"The session ID of the chat. If empty, the current session ID parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"context_id\",\n display_name=\"Context ID\",\n info=\"The context ID of the chat. Adds an extra layer to the local memory.\",\n value=\"\",\n advanced=True,\n ),\n FileInput(\n name=\"files\",\n display_name=\"Files\",\n file_types=TEXT_FILE_TYPES + IMG_FILE_TYPES,\n info=\"Files to be sent with the message.\",\n advanced=True,\n is_list=True,\n temp_file=True,\n ),\n ]\n outputs = [\n Output(display_name=\"Chat Message\", name=\"message\", method=\"message_response\"),\n ]\n\n async def message_response(self) -> Message:\n # Ensure files is a list and filter out empty/None values\n files = self.files if self.files else []\n if files and not isinstance(files, list):\n files = [files]\n # Filter out None/empty values\n files = [f for f in files if f is not None and f != \"\"]\n\n # Build endpoints inject caller-controlled file references directly into\n # ChatInput. Resolve and validate local references before the Message can\n # later read them while constructing model attachment content.\n if files:\n settings_service = get_settings_service()\n storage_type = getattr(getattr(settings_service, \"settings\", None), \"storage_type\", \"local\")\n # The storage factory falls back to local storage for unsupported\n # values, so only the explicitly configured S3 backend may bypass\n # filesystem containment.\n if str(storage_type).lower() != \"s3\" and is_local_file_access_restricted():\n scope_ids = list(component_file_access_scopes(self))\n # Public executions replace graph.flow_id with a per-visitor\n # virtual ID. Only the server-populated source provenance may\n # restore the already-validated public attachment namespace.\n source_flow_id = getattr(self.graph, \"source_flow_id\", None)\n if source_flow_id:\n scope_ids.append(source_flow_id)\n for file_path in get_file_paths(files):\n enforce_local_file_access(file_path, scope_ids=scope_ids)\n\n session_id = self.session_id or self.graph.session_id or \"\"\n message = await Message.create(\n text=self.input_value,\n sender=self.sender,\n sender_name=self.sender_name,\n session_id=session_id,\n context_id=self.context_id,\n files=files,\n )\n if session_id and isinstance(message, Message) and self.should_store_message:\n stored_message = await self.send_message(\n message,\n )\n self.message.value = stored_message\n message = stored_message\n\n self.status = message\n return message\n",
"fileTypes": [],
"file_path": "",
"password": false,
"name": "code",
"advanced": true,
"api_editable": false,
"dynamic": true,
"info": "",
"load_from_db": false,
"title_case": false
},
"context_id": {
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"load_from_db": false,
"list": false,
"list_add_label": "Add More",
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "context_id",
"value": "",
"display_name": "Context ID",
"advanced": true,
"api_editable": false,
"input_types": [
"Message"
],
"dynamic": false,
"info": "The context ID of the chat. Adds an extra layer to the local memory.",
"title_case": false,
"track_in_telemetry": false,
"type": "str",
"_input_type": "MessageTextInput"
},
"input_value": {
"tool_mode": false,
"trace_as_input": true,
"multiline": true,
"ai_enabled": false,
"trace_as_metadata": true,
"load_from_db": false,
"list": false,
"list_add_label": "Add More",
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "input_value",
"value": "",
"display_name": "Input Text",
"advanced": false,
"api_editable": false,
"input_types": [],
"dynamic": false,
"info": "Message to be passed as input.",
"title_case": false,
"track_in_telemetry": false,
"copy_field": false,
"password": false,
"type": "str",
"_input_type": "MultilineInput"
},
"sender": {
"tool_mode": false,
"trace_as_metadata": true,
"options": [
"Machine",
"User"
],
"options_metadata": [],
"combobox": false,
"dialog_inputs": {},
"toggle": false,
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "sender",
"value": "User",
"display_name": "Sender Type",
"advanced": true,
"api_editable": false,
"dynamic": false,
"info": "Type of sender.",
"title_case": false,
"track_in_telemetry": true,
"external_options": {},
"type": "str",
"_input_type": "DropdownInput"
},
"sender_name": {
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"load_from_db": false,
"list": false,
"list_add_label": "Add More",
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "sender_name",
"value": "User",
"display_name": "Sender Name",
"advanced": true,
"api_editable": false,
"input_types": [
"Message"
],
"dynamic": false,
"info": "Name of the sender.",
"title_case": false,
"track_in_telemetry": false,
"type": "str",
"_input_type": "MessageTextInput"
},
"session_id": {
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"load_from_db": false,
"list": false,
"list_add_label": "Add More",
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "session_id",
"value": "",
"display_name": "Session ID",
"advanced": true,
"api_editable": false,
"input_types": [
"Message"
],
"dynamic": false,
"info": "The session ID of the chat. If empty, the current session ID parameter will be used.",
"title_case": false,
"track_in_telemetry": false,
"type": "str",
"_input_type": "MessageTextInput"
},
"should_store_message": {
"tool_mode": false,
"trace_as_metadata": true,
"list": false,
"list_add_label": "Add More",
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "should_store_message",
"value": true,
"display_name": "Store Messages",
"advanced": true,
"api_editable": false,
"dynamic": false,
"info": "Store the message in the history.",
"title_case": false,
"track_in_telemetry": true,
"type": "bool",
"_input_type": "BoolInput"
}
},
"description": "Get chat inputs from the Playground.",
"icon": "MessagesSquare",
"base_classes": [
"Message"
],
"display_name": "Chat Input",
"documentation": "https://docs.langflow.org/chat-input-and-output",
"minimized": true,
"custom_fields": {},
"output_types": [],
"pinned": false,
"conditional_paths": [],
"frozen": false,
"outputs": [
{
"types": [
"Message"
],
"selected": "Message",
"name": "message",
"display_name": "Chat Message",
"method": "message_response",
"value": "__UNDEFINED__",
"cache": true,
"allows_loop": false,
"group_outputs": false,
"tool_mode": true
}
],
"field_order": [],
"beta": false,
"legacy": false,
"edited": false,
"metadata": {},
"tool_mode": false
},
"type": "ChatInput",
"id": "ChatInput-entry",
"showNode": true
},
"id": "ChatInput-entry",
"measured": {
"width": 320,
"height": 200
},
"position": {
"x": 0,
"y": 200
},
"type": "genericNode"
},
{
"data": {
"node": {
"template": {
"form_values": {
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"load_from_db": false,
"list": false,
"list_add_label": "Add More",
"override_skip": false,
"required": false,
"placeholder": "",
"show": false,
"name": "form_values",
"value": "",
"display_name": "Form values (JSON envelope)",
"advanced": false,
"api_editable": false,
"input_types": [
"Message"
],
"dynamic": false,
"info": "Submitted values as JSON; merged over the defaults",
"title_case": false,
"track_in_telemetry": false,
"type": "str",
"_input_type": "MessageTextInput"
},
"rolle": {
"tool_mode": false,
"trace_as_metadata": true,
"options": [
"Assistenz",
"Pfarrer:in",
"Vikar:in",
"Messner:in"
],
"options_metadata": [],
"combobox": false,
"dialog_inputs": {},
"toggle": false,
"override_skip": false,
"required": true,
"placeholder": "",
"show": true,
"name": "rolle",
"value": "Assistenz",
"display_name": "Rolle",
"advanced": false,
"api_editable": false,
"dynamic": false,
"info": "",
"title_case": false,
"track_in_telemetry": true,
"external_options": {},
"type": "str",
"_input_type": "DropdownInput"
},
"einstufung": {
"tool_mode": false,
"trace_as_metadata": true,
"options": [
"Stufe 1",
"Stufe 2",
"Stufe 3"
],
"options_metadata": [],
"combobox": false,
"dialog_inputs": {},
"toggle": false,
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "einstufung",
"value": "Stufe 1",
"display_name": "Einstufung",
"advanced": false,
"api_editable": false,
"dynamic": false,
"info": "Nur für Pfarrer:in [visible_if rolle=Pfarrer:in]",
"title_case": false,
"track_in_telemetry": true,
"external_options": {},
"type": "str",
"_input_type": "DropdownInput"
},
"mitarbeiter_name": {
"tool_mode": false,
"trace_as_metadata": true,
"load_from_db": false,
"list": false,
"list_add_label": "Add More",
"override_skip": false,
"required": true,
"placeholder": "",
"show": true,
"name": "mitarbeiter_name",
"value": " ",
"display_name": "Name",
"advanced": false,
"api_editable": false,
"dynamic": false,
"info": "Vor- und Nachname des Mitarbeiters",
"title_case": false,
"track_in_telemetry": false,
"type": "str",
"_input_type": "StrInput"
},
"geburtsdatum": {
"tool_mode": false,
"trace_as_metadata": true,
"load_from_db": false,
"list": false,
"list_add_label": "Add More",
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "geburtsdatum",
"value": "",
"display_name": "Geburtsdatum",
"advanced": false,
"api_editable": false,
"dynamic": false,
"info": "Datum eingeben (z. B. JJJJ-MM-TT)",
"title_case": false,
"track_in_telemetry": false,
"type": "str",
"_input_type": "StrInput"
},
"_type": "Component",
"code": {
"advanced": true,
"api_editable": false,
"dynamic": true,
"fileTypes": [],
"info": "",
"list": false,
"load_from_db": false,
"multiline": true,
"name": "code",
"password": false,
"placeholder": "",
"required": true,
"show": true,
"title_case": false,
"type": "code",
"value": "\"\"\"Mitarbeiter-einstellen form component for kirche-einfach flows.\n\nStandalone by design: this module shares NO code with the other form\nmodules (no imports from them). The LFX runtime executes the embedded\nmodule source without our ``kirche`` package, so every form component\nmust be self-contained (stdlib + ``lfx`` only). Do NOT refactor the\nduplicated machinery below into a shared import — it would break flow\nexecution. Copy this file (or ``bauvorhaben_form.py``) as the starting\npoint for future forms.\n\nA flow that starts with :class:`CustomInputMitarbeiter` declares the\nfields of its web form. The API reads the exported node template\n(``template`` minus ``code``) and derives ``{json_schema, ui_schema}``\nfrom the ``lfx.io`` input definitions below — one JSON-Schema property\nper field, keyed by ``name``:\n\n* ``display_name`` → ``title``, ``info`` → ``description``\n (``[visible_if ...]`` markers declare conditional branches),\n ``value`` → ``default``, ``options`` → ``enum``\n* ``required`` → required array (branch-required applies only while visible)\n\n``collect()`` is fully dynamic: it emits every input except the plumbing\none (``form_values``), so fields can be renamed, added, or removed in\nthe Langflow code editor without touching this method.\n\nWhen the run envelope names the asked fields (``fields`` list, sent by\nthe API from the version's form schema), defaults are emitted only for\nasked AND currently visible fields. Without the list (raw dict,\nLangflow playground), all visible declared defaults apply.\n\n``collect()`` returns ``Data`` (the LFX JSON type), not ``Message``.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport re\nfrom typing import ClassVar\n\nfrom lfx.custom import Component\nfrom lfx.io import (\n DropdownInput,\n MessageTextInput,\n Output,\n StrInput,\n)\nfrom lfx.schema.data import Data\n\n\ndef _text(value) -> str:\n if value is None:\n return \"\"\n if hasattr(value, \"text\"):\n return value.text or \"\"\n return str(value)\n\n\ndef _json_safe(value):\n \"\"\"Plain JSON value for one field: unwrap LFX wrappers, keep scalars.\"\"\"\n if value is None or isinstance(value, (bool, int, float, str)):\n return value\n if isinstance(value, list):\n return [_json_safe(item) for item in value]\n if isinstance(value, dict):\n return {str(key): _json_safe(item) for key, item in value.items()}\n text = getattr(value, \"text\", None)\n if isinstance(text, str):\n return text\n data = getattr(value, \"data\", None)\n if isinstance(data, dict):\n return _json_safe(data)\n return str(value)\n\n\ndef _split_conditions(text: str) -> list:\n parts: list = []\n buf: list = []\n quoted = False\n for char in text:\n if char == '\"':\n quoted = not quoted\n buf.append(char)\n elif char == \"&\" and not quoted:\n parts.append(\"\".join(buf))\n buf = []\n else:\n buf.append(char)\n parts.append(\"\".join(buf))\n return [part.strip() for part in parts if part.strip()]\n\n\ndef _parse_condition(token: str):\n field, sep, value = token.partition(\"=\")\n field, value = field.strip(), value.strip()\n if not sep or not field or not value:\n return None\n if len(value) >= 2 and value.startswith('\"') and value.endswith('\"'):\n value = value[1:-1]\n lowered = value.lower()\n if lowered == \"true\":\n return field, True\n if lowered == \"false\":\n return field, False\n try:\n return field, int(value)\n except ValueError:\n pass\n try:\n return field, float(value)\n except ValueError:\n pass\n return field, value\n\n\ndef _field_conditions(info: object) -> list:\n \"\"\"``[(trigger, value)]`` from a field's ``info`` markers.\"\"\"\n conditions: list = []\n\n def _collect(match) -> str:\n for token in _split_conditions(match.group(1)):\n parsed = _parse_condition(token)\n if parsed is not None:\n conditions.append(parsed)\n return \"\"\n\n _VISIBLE_RE.sub(_collect, info if isinstance(info, str) else \"\")\n return conditions\n\n\n#: Reserved input carrying the run envelope — a real runtime input, so it\n#: must stay out of the output. Never name a form field ``form_values``.\nPLUMBING_INPUTS = frozenset({\"form_values\"})\n\n#: ``[visible_if ...]`` marker (same syntax as the backend parser in\n#: ``app/flows/input_schema.py`` — keep the two in sync).\n_VISIBLE_RE = re.compile(r\"\\[visible_if\\s+([^\\]]+)\\]\")\n\n\nclass CustomInputMitarbeiter(Component):\n display_name = \"Mitarbeiter Einstellen Form\"\n description = \"Collects hiring form values into a single JSON object.\"\n icon = \"UserPlus\"\n\n inputs: ClassVar[list] = [\n # Plumbing, not a form field: the API posts submitted web-form\n # values as the run envelope (same {\"payload\": {...}} shape the\n # other kirche components use); they arrive here through a ChatInput\n # entry node and override the defaults below. Hidden in the GUI and\n # skipped by the form parser (show=False).\n MessageTextInput(\n name=\"form_values\",\n display_name=\"Form values (JSON envelope)\",\n info=\"Submitted values as JSON; merged over the defaults\",\n value=\"\",\n show=False,\n ),\n DropdownInput(\n name=\"rolle\",\n display_name=\"Rolle\",\n info=\"\",\n options=[\"Assistenz\", \"Pfarrer:in\", \"Vikar:in\", \"Messner:in\"],\n value=\"Assistenz\",\n required=True,\n ),\n DropdownInput(\n name=\"einstufung\",\n display_name=\"Einstufung\",\n info=\"Nur für Pfarrer:in [visible_if rolle=Pfarrer:in]\",\n options=[\"Stufe 1\", \"Stufe 2\", \"Stufe 3\"],\n value=\"Stufe 1\",\n ),\n StrInput(\n name=\"mitarbeiter_name\",\n display_name=\"Name\",\n info=\"Vor- und Nachname des Mitarbeiters\",\n value=\"\",\n required=True,\n ),\n StrInput(\n name=\"geburtsdatum\",\n display_name=\"Geburtsdatum\",\n info=\"Datum eingeben (z. B. JJJJ-MM-TT)\",\n value=\"\",\n ),\n ]\n outputs: ClassVar[list] = [\n Output(display_name=\"Form Data\", name=\"form_data\", method=\"collect\"),\n ]\n\n def _field_names(self) -> list:\n \"\"\"Names of all form fields (every input except the plumbing ones).\"\"\"\n inputs = getattr(self, \"_inputs\", None)\n if isinstance(inputs, dict) and inputs:\n return [name for name in inputs if name not in PLUMBING_INPUTS]\n seen = []\n declared = getattr(self, \"inputs\", None) or getattr(type(self), \"inputs\", None) or []\n for inp in declared:\n name = getattr(inp, \"name\", None)\n if name and name not in PLUMBING_INPUTS and name not in seen:\n seen.append(name)\n return seen\n\n def _field_value(self, name: str):\n \"\"\"Runtime value of one input.\n\n Reads ``_inputs`` directly: a field renamed to e.g. ``name`` would\n otherwise resolve to the component attribute instead of its value.\n \"\"\"\n inputs = getattr(self, \"_inputs\", None)\n if isinstance(inputs, dict) and name in inputs:\n return getattr(inputs[name], \"value\", None)\n try:\n return getattr(self, name, None)\n except (AttributeError, ValueError):\n return None\n\n def _is_visible(self, name: str, effective: dict) -> bool:\n \"\"\"Whether a field's branch conditions hold.\n\n Single pass over effective values (submitted first, code defaults\n after): no recursion, so trigger cycles cannot hang — a stale or\n cyclic marker simply matches or not. Unknown triggers never match.\n \"\"\"\n inputs = getattr(self, \"_inputs\", None)\n info = \"\"\n if isinstance(inputs, dict) and name in inputs:\n info = getattr(inputs[name], \"info\", \"\") or \"\"\n for trigger, expected in _field_conditions(info):\n if trigger == name or trigger not in effective:\n # Self-reference or unknown trigger: fail open (visible),\n # mirroring the backend parser.\n continue\n actual = effective[trigger]\n if isinstance(actual, list):\n if expected not in actual:\n return False\n elif actual != expected:\n return False\n return True\n\n def collect(self) -> Data:\n try:\n envelope = json.loads(_text(self.form_values) or \"{}\")\n except (json.JSONDecodeError, ValueError, AttributeError):\n envelope = {}\n payload: dict = {}\n asked: list | None = None\n if isinstance(envelope, dict):\n # Asked-field list counts only in true envelope shape\n # ({\"payload\": {...}, \"fields\": [...]}); a raw dict or a form\n # field literally named \"fields\" must not confuse it.\n if isinstance(envelope.get(\"payload\"), dict):\n payload = envelope[\"payload\"]\n fields = envelope.get(\"fields\")\n if isinstance(fields, list) and all(isinstance(item, str) for item in fields):\n asked = [item for item in fields if item]\n else:\n payload = envelope\n # Effective trigger values: submitted first, code defaults after.\n declared = self._field_names()\n effective = {name: _json_safe(self._field_value(name)) for name in declared}\n for key, val in payload.items():\n if isinstance(key, str) and key:\n effective[key] = _json_safe(val)\n # Defaults only for asked AND visible fields (hidden branch fields\n # stay out, like the frontend never submits them); without an asked\n # list, all visible declared fields (Langflow playground).\n scope = [name for name in (asked if asked is not None else declared) if name in declared]\n data = {name: effective[name] for name in scope if self._is_visible(name, effective)}\n # Submitted values win over the field defaults. Unknown keys are\n # kept: the frontend may already know renamed fields this code\n # revision does not declare.\n for key, val in payload.items():\n if isinstance(key, str) and key:\n data[key] = _json_safe(val)\n return Data(data=data)\n"
}
},
"description": "Collects hiring form values into a single JSON object.",
"icon": "UserPlus",
"base_classes": [],
"display_name": "Mitarbeiter Einstellen Form",
"documentation": "",
"minimized": false,
"custom_fields": {},
"output_types": [],
"pinned": false,
"conditional_paths": [],
"frozen": false,
"outputs": [
{
"types": [
"JSON"
],
"selected": "JSON",
"name": "form_data",
"display_name": "Form Data",
"method": "collect",
"value": "__UNDEFINED__",
"cache": true,
"allows_loop": false,
"group_outputs": false,
"tool_mode": true
}
],
"field_order": [],
"beta": false,
"legacy": false,
"edited": false,
"metadata": {},
"tool_mode": false
},
"type": "CustomInputMitarbeiter",
"id": "CustomInputMitarbeiter-mitarbeiter",
"showNode": true
},
"id": "CustomInputMitarbeiter-mitarbeiter",
"measured": {
"width": 320,
"height": 200
},
"position": {
"x": 400,
"y": 200
},
"type": "genericNode"
},
{
"data": {
"node": {
"template": {
"_type": "Component",
"input_data": {
"trace_as_metadata": true,
"list": false,
"list_add_label": "Add More",
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "input_data",
"value": "",
"display_name": "Employee Data",
"advanced": false,
"api_editable": false,
"input_types": [
"JSON",
"Data",
"Message",
"DataFrame",
"Table"
],
"dynamic": false,
"info": "Employee data as JSON/Data/Message/DataFrame. Must contain 'rolle' (and 'einstufung' for Pfarrer:in), plus 'mitarbeiter_name' and 'geburtsdatum'.",
"title_case": false,
"track_in_telemetry": false,
"type": "other",
"_input_type": "HandleInput"
},
"code": {
"type": "code",
"required": true,
"placeholder": "",
"list": false,
"show": true,
"multiline": true,
"value": "from lfx.custom import Component\nfrom lfx.io import HandleInput, Output\nfrom lfx.schema import Data, Message\n\ntry:\n from lfx.schema import DataFrame\nexcept Exception: # pragma: no cover\n DataFrame = None\n\nimport json\n\n\nclass GehaltsrechnerComponent(Component):\n display_name = \"Gehaltsrechner\"\n description = (\n \"Calculates the annual salary (Gehalt) for church employees based on their role \"\n \"(rolle) and level (einstufung). Use this when you need to determine the yearly \"\n \"salary for roles such as Assistenz, Vikar:in, Messner:in or Pfarrer:in.\"\n )\n icon = \"Calculator\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"Employee Data\",\n input_types=[\"JSON\", \"Data\", \"Message\", \"DataFrame\", \"Table\"],\n info=(\n \"Employee data as JSON/Data/Message/DataFrame. Must contain \"\n \"'rolle' (and 'einstufung' for Pfarrer:in), plus 'mitarbeiter_name' and 'geburtsdatum'.\"\n ),\n ),\n ]\n\n outputs = [\n Output(\n name=\"salary_text\",\n display_name=\"Salary Text\",\n method=\"calculate_salary\",\n ),\n ]\n\n # Salary mapping for simple roles\n BASE_SALARY = {\n \"Assistenz\": 42000,\n \"Vikar:in\": 48000,\n \"Messner:in\": 36000,\n }\n\n # Salary mapping for Pfarrer:in per Einstufung\n PFARRER_SALARY = {\n \"Stufe 1\": 55000,\n \"Stufe 2\": 65000,\n \"Stufe 3\": 75000,\n }\n\n def _extract_data(self, value) -> dict | None:\n '''Normalize the incoming value into a plain dict.'''\n if isinstance(value, dict):\n return value\n if isinstance(value, Data):\n return value.data if isinstance(value.data, dict) else None\n if isinstance(value, Message):\n value = value.text\n if DataFrame is not None and isinstance(value, DataFrame):\n try:\n records = value.to_dict(orient=\"records\")\n if records:\n return records[0]\n except Exception:\n return None\n return None\n if isinstance(value, str):\n text = value.strip()\n try:\n parsed = json.loads(text)\n if isinstance(parsed, dict):\n return parsed\n if isinstance(parsed, list) and parsed and isinstance(parsed[0], dict):\n return parsed[0]\n except (json.JSONDecodeError, TypeError, ValueError):\n return None\n return None\n\n def calculate_salary(self) -> Message:\n try:\n data = self._extract_data(self.input_data)\n if not isinstance(data, dict):\n return Message(\n text=\"Error: Could not parse employee data. Please provide a valid JSON object.\"\n )\n rolle = data.get(\"rolle\")\n einstufung = data.get(\"einstufung\")\n name = data.get(\"mitarbeiter_name\", \"unknown\")\n if not rolle:\n return Message(\n text=\"Error: Missing required field 'rolle' in employee data.\"\n )\n if rolle in self.BASE_SALARY:\n salary = self.BASE_SALARY[rolle]\n elif rolle == \"Pfarrer:in\":\n if einstufung not in self.PFARRER_SALARY:\n return Message(\n text=(\n \"Error: For role 'Pfarrer:in', a valid 'einstufung' \"\n \"(Stufe 1, Stufe 2 or Stufe 3) is required.\"\n )\n )\n salary = self.PFARRER_SALARY[einstufung]\n else:\n return Message(text=f\"Error: Unknown role '{rolle}'.\")\n einstufung_display = einstufung if einstufung else \"-\"\n return Message(\n text=(\n f\"Mitarbeiter:{name} | Rolle:{rolle} | \"\n f\"Einstufung:{einstufung_display} | Gehalt:{salary} EUR pro Jahr\"\n )\n )\n except Exception as exc: # never raise, always return a clear error string\n return Message(text=f\"Error: {exc}\")\n",
"fileTypes": [],
"file_path": "",
"password": false,
"name": "code",
"advanced": true,
"api_editable": false,
"dynamic": true,
"info": "",
"load_from_db": false,
"title_case": false
}
},
"description": "Calculates the annual salary (Gehalt) for church employees based on their role (rolle) and level (einstufung). Use this when you need to determine the yearly salary for roles such as Assistenz, Vikar:in, Messner:in or Pfarrer:in.",
"icon": "Calculator",
"base_classes": [
"Message"
],
"display_name": "Gehaltsrechner",
"documentation": "",
"minimized": false,
"custom_fields": {},
"output_types": [],
"pinned": false,
"conditional_paths": [],
"frozen": false,
"outputs": [
{
"types": [
"Message"
],
"selected": "Message",
"name": "salary_text",
"display_name": "Salary Text",
"method": "calculate_salary",
"value": "__UNDEFINED__",
"cache": true,
"allows_loop": false,
"group_outputs": false,
"tool_mode": true
}
],
"field_order": [
"input_data"
],
"beta": false,
"legacy": false,
"edited": false,
"metadata": {
"module": "custom_components.gehaltsrechner",
"code_hash": "31079e5045cc",
"dependencies": {
"total_dependencies": 1,
"dependencies": [
{
"name": "lfx",
"version": "1.12.0"
}
]
}
},
"tool_mode": false
},
"type": "CustomComponent",
"id": "Gehaltsrechner-gehalt",
"showNode": true
},
"id": "Gehaltsrechner-gehalt",
"measured": {
"width": 320,
"height": 200
},
"position": {
"x": 800,
"y": 200
},
"type": "genericNode"
},
{
"data": {
"node": {
"template": {
"_type": "Component",
"input_value": {
"trace_as_metadata": true,
"list": false,
"list_add_label": "Add More",
"override_skip": false,
"required": true,
"placeholder": "",
"show": true,
"name": "input_value",
"value": "",
"display_name": "Inputs",
"advanced": false,
"api_editable": false,
"input_types": [
"Data",
"JSON",
"DataFrame",
"Table",
"Message"
],
"dynamic": false,
"info": "Message to be passed as output.",
"title_case": false,
"track_in_telemetry": false,
"type": "other",
"_input_type": "HandleInput"
},
"clean_data": {
"tool_mode": false,
"trace_as_metadata": true,
"list": false,
"list_add_label": "Add More",
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "clean_data",
"value": true,
"display_name": "Basic Clean Data",
"advanced": true,
"api_editable": false,
"dynamic": false,
"info": "Whether to clean data before converting to string.",
"title_case": false,
"track_in_telemetry": true,
"type": "bool",
"_input_type": "BoolInput"
},
"code": {
"type": "code",
"required": true,
"placeholder": "",
"list": false,
"show": true,
"multiline": true,
"value": "from collections.abc import Generator\nfrom typing import Any\n\nimport orjson\nfrom fastapi.encoders import jsonable_encoder\n\nfrom lfx.base.io.chat import ChatComponent\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, HandleInput, MessageTextInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.schema.properties import Source\nfrom lfx.template.field.base import Output\nfrom lfx.utils.constants import (\n MESSAGE_SENDER_AI,\n MESSAGE_SENDER_NAME_AI,\n MESSAGE_SENDER_USER,\n)\n\n\nclass ChatOutput(ChatComponent):\n display_name = \"Chat Output\"\n description = \"Display a chat message in the Playground.\"\n documentation: str = \"https://docs.langflow.org/chat-input-and-output\"\n icon = \"MessagesSquare\"\n name = \"ChatOutput\"\n minimized = True\n\n inputs = [\n HandleInput(\n name=\"input_value\",\n display_name=\"Inputs\",\n info=\"Message to be passed as output.\",\n input_types=[\"Data\", \"JSON\", \"DataFrame\", \"Table\", \"Message\"],\n required=True,\n ),\n BoolInput(\n name=\"should_store_message\",\n display_name=\"Store Messages\",\n info=\"Store the message in the history.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER],\n value=MESSAGE_SENDER_AI,\n advanced=True,\n info=\"Type of sender.\",\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Name of the sender.\",\n value=MESSAGE_SENDER_NAME_AI,\n advanced=True,\n ),\n MessageTextInput(\n name=\"session_id\",\n display_name=\"Session ID\",\n info=\"The session ID of the chat. If empty, the current session ID parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"context_id\",\n display_name=\"Context ID\",\n info=\"The context ID of the chat. Adds an extra layer to the local memory.\",\n value=\"\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"data_template\",\n display_name=\"Data Template\",\n value=\"{text}\",\n advanced=True,\n info=\"Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.\",\n ),\n BoolInput(\n name=\"clean_data\",\n display_name=\"Basic Clean Data\",\n value=True,\n advanced=True,\n info=\"Whether to clean data before converting to string.\",\n ),\n ]\n outputs = [\n Output(\n display_name=\"Output Message\",\n name=\"message\",\n method=\"message_response\",\n ),\n ]\n\n def _build_source(self, id_: str | None, display_name: str | None, source: str | None) -> Source:\n source_dict = {}\n if id_:\n source_dict[\"id\"] = id_\n if display_name:\n source_dict[\"display_name\"] = display_name\n if source:\n # Handle case where source is a ChatOpenAI object\n if hasattr(source, \"model_name\"):\n source_dict[\"source\"] = source.model_name\n elif hasattr(source, \"model\"):\n source_dict[\"source\"] = str(source.model)\n else:\n source_dict[\"source\"] = str(source)\n return Source(**source_dict)\n\n async def message_response(self) -> Message:\n # First convert the input to string if needed\n text = self.convert_to_string()\n\n # Get source properties\n source, _, display_name, source_id = self.get_properties_from_source_component()\n\n # Create or use existing Message object\n if isinstance(self.input_value, Message) and not self.is_connected_to_chat_input():\n message = self.input_value\n # Update message properties\n message.text = text\n # Preserve existing session_id from the incoming message if it exists\n existing_session_id = message.session_id\n else:\n message = Message(text=text)\n existing_session_id = None\n\n # Set message properties\n message.sender = self.sender\n message.sender_name = self.sender_name\n # Preserve session_id from incoming message, or use component/graph session_id\n message.session_id = (\n self.session_id or existing_session_id or (self.graph.session_id if hasattr(self, \"graph\") else None) or \"\"\n )\n message.context_id = self.context_id\n message.flow_id = self.graph.flow_id if hasattr(self, \"graph\") else None\n message.properties.source = self._build_source(source_id, display_name, source)\n\n # Store message if needed\n if message.session_id and self.should_store_message:\n stored_message = await self.send_message(message)\n self.message.value = stored_message\n message = stored_message\n\n # Set accumulated token usage from all upstream LLM vertices.\n # This must happen AFTER send_message() because streaming captures\n # usage from chunks and would overwrite accumulated totals.\n if hasattr(self, \"_vertex\") and self._vertex is not None:\n accumulated_usage = self._vertex._accumulate_upstream_token_usage() # noqa: SLF001\n if accumulated_usage:\n message.properties.usage = accumulated_usage\n if self.should_store_message and message.get_id():\n message = await self._update_stored_message(message)\n await self._send_message_event(message, id_=message.get_id())\n\n self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n",
"fileTypes": [],
"file_path": "",
"password": false,
"name": "code",
"advanced": true,
"api_editable": false,
"dynamic": true,
"info": "",
"load_from_db": false,
"title_case": false
},
"context_id": {
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"load_from_db": false,
"list": false,
"list_add_label": "Add More",
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "context_id",
"value": "",
"display_name": "Context ID",
"advanced": true,
"api_editable": false,
"input_types": [
"Message"
],
"dynamic": false,
"info": "The context ID of the chat. Adds an extra layer to the local memory.",
"title_case": false,
"track_in_telemetry": false,
"type": "str",
"_input_type": "MessageTextInput"
},
"data_template": {
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"load_from_db": false,
"list": false,
"list_add_label": "Add More",
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "data_template",
"value": "{text}",
"display_name": "Data Template",
"advanced": true,
"api_editable": false,
"input_types": [
"Message"
],
"dynamic": false,
"info": "Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.",
"title_case": false,
"track_in_telemetry": false,
"type": "str",
"_input_type": "MessageTextInput"
},
"sender": {
"tool_mode": false,
"trace_as_metadata": true,
"options": [
"Machine",
"User"
],
"options_metadata": [],
"combobox": false,
"dialog_inputs": {},
"toggle": false,
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "sender",
"value": "Machine",
"display_name": "Sender Type",
"advanced": true,
"api_editable": false,
"dynamic": false,
"info": "Type of sender.",
"title_case": false,
"track_in_telemetry": true,
"external_options": {},
"type": "str",
"_input_type": "DropdownInput"
},
"sender_name": {
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"load_from_db": false,
"list": false,
"list_add_label": "Add More",
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "sender_name",
"value": "AI",
"display_name": "Sender Name",
"advanced": true,
"api_editable": false,
"input_types": [
"Message"
],
"dynamic": false,
"info": "Name of the sender.",
"title_case": false,
"track_in_telemetry": false,
"type": "str",
"_input_type": "MessageTextInput"
},
"session_id": {
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"load_from_db": false,
"list": false,
"list_add_label": "Add More",
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "session_id",
"value": "",
"display_name": "Session ID",
"advanced": true,
"api_editable": false,
"input_types": [
"Message"
],
"dynamic": false,
"info": "The session ID of the chat. If empty, the current session ID parameter will be used.",
"title_case": false,
"track_in_telemetry": false,
"type": "str",
"_input_type": "MessageTextInput"
},
"should_store_message": {
"tool_mode": false,
"trace_as_metadata": true,
"list": false,
"list_add_label": "Add More",
"override_skip": false,
"required": false,
"placeholder": "",
"show": true,
"name": "should_store_message",
"value": true,
"display_name": "Store Messages",
"advanced": true,
"api_editable": false,
"dynamic": false,
"info": "Store the message in the history.",
"title_case": false,
"track_in_telemetry": true,
"type": "bool",
"_input_type": "BoolInput"
}
},
"description": "Display a chat message in the Playground.",
"icon": "MessagesSquare",
"base_classes": [
"Message"
],
"display_name": "Chat Output",
"documentation": "https://docs.langflow.org/chat-input-and-output",
"minimized": true,
"custom_fields": {},
"output_types": [],
"pinned": false,
"conditional_paths": [],
"frozen": false,
"outputs": [
{
"types": [
"Message"
],
"selected": "Message",
"name": "message",
"display_name": "Output Message",
"method": "message_response",
"value": "__UNDEFINED__",
"cache": true,
"allows_loop": false,
"group_outputs": false,
"tool_mode": true
}
],
"field_order": [],
"beta": false,
"legacy": false,
"edited": false,
"metadata": {},
"tool_mode": false
},
"type": "ChatOutput",
"id": "ChatOutput-collected",
"showNode": true
},
"id": "ChatOutput-collected",
"measured": {
"width": 320,
"height": 200
},
"position": {
"x": 1200,
"y": 200
},
"type": "genericNode"
}
],
"edges": [
{
"animated": false,
"className": "",
"data": {
"sourceHandle": {
"dataType": "ChatInput",
"id": "ChatInput-entry",
"name": "message",
"output_types": [
"Message"
]
},
"targetHandle": {
"fieldName": "form_values",
"id": "CustomInputMitarbeiter-mitarbeiter",
"inputTypes": [
"Message"
],
"type": "str"
}
},
"id": "xy-edge__{œdataTypeœ: œChatInputœ, œidœ: œChatInput-entryœ, œnameœ: œmessageœ, œoutput_typesœ: [œMessageœ]}-{œfieldNameœ: œform_valuesœ, œidœ: œCustomInputMitarbeiter-mitarbeiterœ, œinputTypesœ: [œMessageœ], œtypeœ: œstrœ}",
"selected": false,
"source": "ChatInput-entry",
"sourceHandle": "{œdataTypeœ: œChatInputœ, œidœ: œChatInput-entryœ, œnameœ: œmessageœ, œoutput_typesœ: [œMessageœ]}",
"target": "CustomInputMitarbeiter-mitarbeiter",
"targetHandle": "{œfieldNameœ: œform_valuesœ, œidœ: œCustomInputMitarbeiter-mitarbeiterœ, œinputTypesœ: [œMessageœ], œtypeœ: œstrœ}"
},
{
"animated": false,
"className": "",
"data": {
"sourceHandle": {
"dataType": "CustomInputMitarbeiter",
"id": "CustomInputMitarbeiter-mitarbeiter",
"name": "form_data",
"output_types": [
"JSON"
]
},
"targetHandle": {
"fieldName": "input_data",
"id": "Gehaltsrechner-gehalt",
"inputTypes": [
"JSON",
"Data",
"Message",
"DataFrame",
"Table"
],
"type": "other"
}
},
"id": "xy-edge__{œdataTypeœ: œCustomInputMitarbeiterœ, œidœ: œCustomInputMitarbeiter-mitarbeiterœ, œnameœ: œform_dataœ, œoutput_typesœ: [œJSONœ]}-{œfieldNameœ: œinput_dataœ, œidœ: œGehaltsrechner-gehaltœ, œinputTypesœ: [œJSONœ, œDataœ, œMessageœ, œDataFrameœ, œTableœ], œtypeœ: œotherœ}",
"selected": false,
"source": "CustomInputMitarbeiter-mitarbeiter",
"sourceHandle": "{œdataTypeœ: œCustomInputMitarbeiterœ, œidœ: œCustomInputMitarbeiter-mitarbeiterœ, œnameœ: œform_dataœ, œoutput_typesœ: [œJSONœ]}",
"target": "Gehaltsrechner-gehalt",
"targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œGehaltsrechner-gehaltœ, œinputTypesœ: [œJSONœ, œDataœ, œMessageœ, œDataFrameœ, œTableœ], œtypeœ: œotherœ}"
},
{
"animated": false,
"className": "",
"data": {
"sourceHandle": {
"dataType": "CustomComponent",
"id": "Gehaltsrechner-gehalt",
"name": "salary_text",
"output_types": [
"Message"
]
},
"targetHandle": {
"fieldName": "input_value",
"id": "ChatOutput-collected",
"inputTypes": [
"Data",
"JSON",
"DataFrame",
"Table",
"Message"
],
"type": "other"
}
},
"id": "xy-edge__{œdataTypeœ: œCustomComponentœ, œidœ: œGehaltsrechner-gehaltœ, œnameœ: œsalary_textœ, œoutput_typesœ: [œMessageœ]}-{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-collectedœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}",
"selected": false,
"source": "Gehaltsrechner-gehalt",
"sourceHandle": "{œdataTypeœ: œCustomComponentœ, œidœ: œGehaltsrechner-gehaltœ, œnameœ: œsalary_textœ, œoutput_typesœ: [œMessageœ]}",
"target": "ChatOutput-collected",
"targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-collectedœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}"
}
],
"viewport": {
"x": 0,
"y": 0,
"zoom": 1
}
},
"description": "Mitarbeiter einstellen: Formular mit Gehaltsrechner.",
"endpoint_name": null,
"icon": null,
"icon_bg_color": null,
"id": "b2076300-4ba0-5907-9633-c92b97237c35",
"is_component": false,
"locked": false,
"mcp_enabled": false,
"name": "mitarbeiter_einstellen",
"tags": [],
"webhook": false
}