diff --git a/CHANGELOG.md b/CHANGELOG.md index bdf48f5..c320d28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Startup-Sync bleiben dateibasiert. Die Verwaltung zeigt eine deutsche Schritt-Anleitung direkt beim Sync-Button. ### Fixed +- Flow-Sync aus Langflow: neue Flows blieben unsichtbar, weil LFX-Uploads + mit `Permission denied` scheiterten (named Volume root-owned, LFX läuft + rootless). `lfx-entrypoint.sh` chownt `/flows` per gosu vor dem Start; + zusätzlich Pull-Dedup (wiederverwendet gleichnamige/identische Dateien + statt Bindestrich-Duplikate wie `simple-rule.json`, die jede Sync-Meldung + mit Namenskonflikten füllten). - LFX runtime user gets a home directory (`useradd -m`, `HOME` set): without one every flow run failed with "Settings service not found". ### Security diff --git a/Dockerfile.lfx b/Dockerfile.lfx index c533199..34ace67 100644 --- a/Dockerfile.lfx +++ b/Dockerfile.lfx @@ -16,11 +16,18 @@ RUN pip install "lfx~=${LFX_VERSION}" requests # Non-root runtime: uploads persist into the mounted volume. The user needs # a home directory: lfx resolves settings/cache paths from $HOME, and # without one every flow run fails ("Settings service not found"). +# gosu drops privileges in the entrypoint after chowning /flows: the named +# volume arrives root-owned and the server user could otherwise never write +# uploads (every deploy would fail with "Permission denied"). ENV HOME=/home/lfx -RUN useradd -r -m -s /bin/false lfx && mkdir -p /flows && chown lfx /flows +RUN apt-get update \ + && apt-get install -y --no-install-recommends gosu \ + && rm -rf /var/lib/apt/lists/* \ + && useradd -r -m -s /bin/false lfx && mkdir -p /flows && chown lfx /flows +COPY lfx-entrypoint.sh /usr/local/bin/lfx-entrypoint.sh VOLUME /flows EXPOSE 6004 -USER lfx +ENTRYPOINT ["sh", "/usr/local/bin/lfx-entrypoint.sh"] # Serve the /flows dir (uploaded flows persist there via --flow-dir). # No positional path: with one, lfx>=1.12.1 exits when the dir holds no diff --git a/backend/app/flows/deploy.py b/backend/app/flows/deploy.py index ee990ef..ef53c3c 100644 --- a/backend/app/flows/deploy.py +++ b/backend/app/flows/deploy.py @@ -49,7 +49,12 @@ async def pull_all_from_langflow( try: exported = await langflow.export_flow(flow_id) doc = normalize_exported_flow(exported, flow_id) - path = export.write_flow_file(flows_dir, doc, name=doc["name"]) + reuse, identical = export.find_reuse_path(flows_dir, doc) + if identical and reuse is not None: + pulled.append(reuse.stem) + continue + target = reuse or export.flow_path(flows_dir, export.slugify(doc["name"])) + path = export.write_flow_bytes(target, doc) pulled.append(path.stem) except (LangflowError, OSError, ValueError) as exc: errors[flow_id] = str(exc) diff --git a/backend/app/flows/export.py b/backend/app/flows/export.py index 0fac6c0..2ad6a49 100644 --- a/backend/app/flows/export.py +++ b/backend/app/flows/export.py @@ -36,17 +36,45 @@ def flow_path(flows_dir: str | Path, stem: str) -> Path: return Path(flows_dir) / f"{stem}.json" -def write_flow_file(flows_dir: str | Path, doc: dict, *, name: str | None = None) -> Path: - """Write a flow document to backend/flows atomically (tmp + rename). +def _read_doc(path: Path) -> dict | None: + try: + doc = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError): + return None + return doc if isinstance(doc, dict) else None - The filename derives from `name` (or the document's own name). Returns - the written path. + +def find_reuse_path(flows_dir: str | Path, doc: dict) -> tuple[Path | None, bool]: + """Existing file to reuse for `doc`: (path, identical). + + Matches identical content first (any filename), then the same flow name. + Returns (None, False) when no candidate exists (caller writes a slug file). + This keeps pulls idempotent: a flow seeded from `simple_rule.json` is + updated in place instead of duplicated as `simple-rule.json`. """ flows_dir = Path(flows_dir) - flows_dir.mkdir(parents=True, exist_ok=True) - stem = slugify(name or doc.get("name") or "flow") - target = flow_path(flows_dir, stem) - fd, tmp_name = tempfile.mkstemp(dir=str(flows_dir), prefix=f".{stem}-", suffix=".tmp") + if not flows_dir.is_dir(): + return None, False + wanted_hash = content_hash(doc) + wanted_name = doc.get("name") + named: Path | None = None + for path in iter_flow_files(flows_dir): + existing = _read_doc(path) + if existing is None: + continue + if content_hash(existing) == wanted_hash: + return path, True + if named is None and wanted_name and existing.get("name") == wanted_name: + named = path + return (named, False) if named is not None else (None, False) + + +def write_flow_bytes(target: Path, doc: dict) -> Path: + """Write a flow document to an explicit path atomically (tmp + rename).""" + target.parent.mkdir(parents=True, exist_ok=True) + fd, tmp_name = tempfile.mkstemp( + dir=str(target.parent), prefix=f".{target.stem}-", suffix=".tmp" + ) try: with os.fdopen(fd, "wb") as tmp: tmp.write(canonical_bytes(doc)) @@ -60,6 +88,18 @@ def write_flow_file(flows_dir: str | Path, doc: dict, *, name: str | None = None return target +def write_flow_file(flows_dir: str | Path, doc: dict, *, name: str | None = None) -> Path: + """Write a flow document to backend/flows atomically (tmp + rename). + + The filename derives from `name` (or the document's own name). Returns + the written path. + """ + flows_dir = Path(flows_dir) + flows_dir.mkdir(parents=True, exist_ok=True) + stem = slugify(name or doc.get("name") or "flow") + return write_flow_bytes(flow_path(flows_dir, stem), doc) + + def iter_flow_files(flows_dir: str | Path) -> list[Path]: """Sorted list of candidate flow files (top level only).""" return sorted(Path(flows_dir).glob(FLOWS_GLOB)) diff --git a/backend/flows/approval_handoff.json b/backend/flows/approval_handoff.json index fbc7091..387ace3 100644 --- a/backend/flows/approval_handoff.json +++ b/backend/flows/approval_handoff.json @@ -1,1681 +1,5 @@ { "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": 300 - }, - "type": "genericNode" - }, - { - "data": { - "node": { - "template": { - "_type": "Component", - "code": { - "type": "code", - "required": true, - "placeholder": "", - "list": false, - "show": true, - "multiline": true, - "value": "from lfx.custom import Component\nfrom lfx.io import MessageTextInput, Output\nfrom lfx.schema.message import Message\nimport json\n\n\nclass EnvelopeComponent(Component):\n display_name = \"Envelope\"\n description = \"Zerlegt die Eingabe-Huelle in Payload und Laufzeit-Kontext.\"\n\n inputs = [\n MessageTextInput(name=\"raw\", display_name=\"Raw Input\"),\n ]\n outputs = [\n Output(display_name=\"Payload\", name=\"payload\", method=\"get_payload\"),\n Output(display_name=\"Run ID\", name=\"run_id\", method=\"get_run_id\"),\n Output(\n display_name=\"Callback URL\",\n name=\"callback_url\",\n method=\"get_callback_url\",\n ),\n Output(\n display_name=\"Continuation\",\n name=\"continuation\",\n method=\"get_continuation\",\n ),\n Output(\n display_name=\"Callback Token\",\n name=\"callback_token\",\n method=\"get_callback_token\",\n ),\n ]\n\n def _text(self, 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 def _env(self) -> dict:\n if not hasattr(self, \"_env_cache\"):\n try:\n self._env_cache = json.loads(self._text(self.raw) or \"{}\")\n except Exception:\n self._env_cache = {}\n if not isinstance(self._env_cache, dict):\n self._env_cache = {}\n return self._env_cache\n\n def get_payload(self) -> Message:\n return Message(text=json.dumps(self._env().get(\"payload\", {})))\n\n def get_run_id(self) -> Message:\n return Message(text=str(self._env().get(\"run_id\", \"\")))\n\n def get_callback_url(self) -> Message:\n return Message(text=str(self._env().get(\"callback_url\", \"\")))\n\n def get_continuation(self) -> Message:\n value = self._env().get(\"continuation_flow_id\")\n return Message(text=str(value) if value else \"\")\n\n def get_callback_token(self) -> Message:\n return Message(text=str(self._env().get(\"callback_token\", \"\")))\n", - "fileTypes": [], - "file_path": "", - "password": false, - "name": "code", - "advanced": true, - "api_editable": false, - "dynamic": true, - "info": "", - "load_from_db": false, - "title_case": false - }, - "raw": { - "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": "raw", - "value": "", - "display_name": "Raw Input", - "advanced": false, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageTextInput" - } - }, - "description": "Zerlegt die Eingabe-Huelle in Payload und Laufzeit-Kontext.", - "base_classes": [ - "Message" - ], - "display_name": "Envelope", - "documentation": "", - "minimized": false, - "custom_fields": {}, - "output_types": [], - "pinned": false, - "conditional_paths": [], - "frozen": false, - "outputs": [ - { - "types": [ - "Message" - ], - "selected": "Message", - "name": "payload", - "display_name": "Payload", - "method": "get_payload", - "value": "__UNDEFINED__", - "cache": true, - "allows_loop": false, - "group_outputs": false, - "tool_mode": true - }, - { - "types": [ - "Message" - ], - "selected": "Message", - "name": "run_id", - "display_name": "Run ID", - "method": "get_run_id", - "value": "__UNDEFINED__", - "cache": true, - "allows_loop": false, - "group_outputs": false, - "tool_mode": true - }, - { - "types": [ - "Message" - ], - "selected": "Message", - "name": "callback_url", - "display_name": "Callback URL", - "method": "get_callback_url", - "value": "__UNDEFINED__", - "cache": true, - "allows_loop": false, - "group_outputs": false, - "tool_mode": true - }, - { - "types": [ - "Message" - ], - "selected": "Message", - "name": "continuation", - "display_name": "Continuation", - "method": "get_continuation", - "value": "__UNDEFINED__", - "cache": true, - "allows_loop": false, - "group_outputs": false, - "tool_mode": true - }, - { - "types": [ - "Message" - ], - "selected": "Message", - "name": "callback_token", - "display_name": "Callback Token", - "method": "get_callback_token", - "value": "__UNDEFINED__", - "cache": true, - "allows_loop": false, - "group_outputs": false, - "tool_mode": true - } - ], - "field_order": [ - "raw" - ], - "beta": false, - "legacy": false, - "edited": false, - "metadata": { - "module": "custom_components.envelope", - "code_hash": "02e69613e210", - "dependencies": { - "total_dependencies": 1, - "dependencies": [ - { - "name": "lfx", - "version": "1.12.0" - } - ] - } - }, - "tool_mode": false - }, - "type": "CustomComponent", - "id": "CustomComponent-envelope", - "showNode": true - }, - "id": "CustomComponent-envelope", - "measured": { - "width": 320, - "height": 200 - }, - "position": { - "x": 400, - "y": 300 - }, - "type": "genericNode" - }, - { - "data": { - "node": { - "template": { - "_type": "Component", - "code": { - "type": "code", - "required": true, - "placeholder": "", - "list": false, - "show": true, - "multiline": true, - "value": "from lfx.custom import Component\nfrom lfx.io import MessageTextInput, Output\nfrom lfx.schema.message import Message\nimport json\n\n\nclass RuleCheckComponent(Component):\n display_name = \"Rule Check\"\n description = \"Bewertet Betrag gegen Schwelle aus JSON-Eingabe.\"\n\n inputs = [\n MessageTextInput(name=\"payload\", display_name=\"Payload\"),\n ]\n outputs = [\n Output(display_name=\"Verdict\", name=\"verdict\", method=\"check\"),\n ]\n\n def _text(self, 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 def check(self) -> Message:\n try:\n data = json.loads(self._text(self.payload) or \"{}\")\n except Exception:\n data = {}\n if not isinstance(data, dict):\n data = {}\n # Die API ruft Flows immer mit Huelle auf\n # {\"payload\": {...}, \"run_id\": ..., ...}; rohe Payloads gehen auch.\n if isinstance(data.get(\"payload\"), dict):\n data = data[\"payload\"]\n try:\n betrag = float(data.get(\"betrag\", 0) or 0)\n except (TypeError, ValueError):\n betrag = 0.0\n try:\n schwelle = float(data.get(\"schwelle\", 200000) or 200000)\n except (TypeError, ValueError):\n schwelle = 200000.0\n return Message(text=\"PRUEFUNG\" if betrag > schwelle else \"OK\")\n", - "fileTypes": [], - "file_path": "", - "password": false, - "name": "code", - "advanced": true, - "api_editable": false, - "dynamic": true, - "info": "", - "load_from_db": false, - "title_case": false - }, - "payload": { - "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": "payload", - "value": "", - "display_name": "Payload", - "advanced": false, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageTextInput" - } - }, - "description": "Bewertet Betrag gegen Schwelle aus JSON-Eingabe.", - "base_classes": [ - "Message" - ], - "display_name": "Rule Check", - "documentation": "", - "minimized": false, - "custom_fields": {}, - "output_types": [], - "pinned": false, - "conditional_paths": [], - "frozen": false, - "outputs": [ - { - "types": [ - "Message" - ], - "selected": "Message", - "name": "verdict", - "display_name": "Verdict", - "method": "check", - "value": "__UNDEFINED__", - "cache": true, - "allows_loop": false, - "group_outputs": false, - "tool_mode": true - } - ], - "field_order": [ - "payload" - ], - "beta": false, - "legacy": false, - "edited": false, - "metadata": { - "module": "custom_components.rule_check", - "code_hash": "0b9f8a009fcf", - "dependencies": { - "total_dependencies": 1, - "dependencies": [ - { - "name": "lfx", - "version": "1.12.0" - } - ] - } - }, - "tool_mode": false - }, - "type": "CustomComponent", - "id": "CustomComponent-rule", - "showNode": true - }, - "id": "CustomComponent-rule", - "measured": { - "width": 320, - "height": 200 - }, - "position": { - "x": 800, - "y": 300 - }, - "type": "genericNode" - }, - { - "data": { - "node": { - "template": { - "_type": "Component", - "case_sensitive": { - "tool_mode": false, - "trace_as_metadata": true, - "list": false, - "list_add_label": "Add More", - "override_skip": false, - "required": false, - "placeholder": "", - "show": true, - "name": "case_sensitive", - "value": true, - "display_name": "Case Sensitive", - "advanced": true, - "api_editable": false, - "dynamic": false, - "info": "If true, the comparison will be case sensitive.", - "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": "import re\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageInput, MessageTextInput, Output\nfrom lfx.schema.message import Message\n\n\nclass ConditionalRouterComponent(Component):\n display_name = \"If-Else\"\n description = \"Routes an input message to a corresponding output based on text comparison.\"\n documentation: str = \"https://docs.langflow.org/if-else\"\n icon = \"split\"\n name = \"ConditionalRouter\"\n\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self.__iteration_updated = False\n\n inputs = [\n MessageTextInput(\n name=\"input_text\",\n display_name=\"Text Input\",\n info=\"The primary text input for the operation.\",\n required=True,\n ),\n DropdownInput(\n name=\"operator\",\n display_name=\"Operator\",\n options=[\n \"equals\",\n \"not equals\",\n \"contains\",\n \"starts with\",\n \"ends with\",\n \"regex\",\n \"less than\",\n \"less than or equal\",\n \"greater than\",\n \"greater than or equal\",\n ],\n info=\"The operator to apply for comparing the texts.\",\n value=\"equals\",\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"match_text\",\n display_name=\"Match Text\",\n info=\"The text input to compare against.\",\n required=True,\n ),\n BoolInput(\n name=\"case_sensitive\",\n display_name=\"Case Sensitive\",\n info=\"If true, the comparison will be case sensitive.\",\n value=True,\n advanced=True,\n ),\n MessageInput(\n name=\"true_case_message\",\n display_name=\"Case True\",\n info=\"The message to pass if the condition is True.\",\n advanced=True,\n ),\n MessageInput(\n name=\"false_case_message\",\n display_name=\"Case False\",\n info=\"The message to pass if the condition is False.\",\n advanced=True,\n ),\n IntInput(\n name=\"max_iterations\",\n display_name=\"Max Iterations\",\n info=\"The maximum number of iterations for the conditional router.\",\n value=10,\n advanced=True,\n ),\n DropdownInput(\n name=\"default_route\",\n display_name=\"Default Route\",\n options=[\"true_result\", \"false_result\"],\n info=\"The default route to take when max iterations are reached.\",\n value=\"false_result\",\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"True\", name=\"true_result\", method=\"true_response\", group_outputs=True),\n Output(display_name=\"False\", name=\"false_result\", method=\"false_response\", group_outputs=True),\n ]\n\n def _pre_run_setup(self):\n self.__iteration_updated = False\n\n def evaluate_condition(self, input_text: str, match_text: str, operator: str, *, case_sensitive: bool) -> bool:\n if not case_sensitive and operator != \"regex\":\n input_text = input_text.lower()\n match_text = match_text.lower()\n\n if operator == \"equals\":\n return input_text == match_text\n if operator == \"not equals\":\n return input_text != match_text\n if operator == \"contains\":\n return match_text in input_text\n if operator == \"starts with\":\n return input_text.startswith(match_text)\n if operator == \"ends with\":\n return input_text.endswith(match_text)\n if operator == \"regex\":\n try:\n return bool(re.match(match_text, input_text))\n except re.error:\n return False # Return False if the regex is invalid\n if operator in [\"less than\", \"less than or equal\", \"greater than\", \"greater than or equal\"]:\n try:\n input_num = float(input_text)\n match_num = float(match_text)\n if operator == \"less than\":\n return input_num < match_num\n if operator == \"less than or equal\":\n return input_num <= match_num\n if operator == \"greater than\":\n return input_num > match_num\n if operator == \"greater than or equal\":\n return input_num >= match_num\n except ValueError:\n return False # Invalid number format for comparison\n return False\n\n def iterate_and_stop_once(self, route_to_stop: str):\n \"\"\"Handles cycle iteration counting and branch exclusion.\n\n Uses two complementary mechanisms:\n 1. stop() - ACTIVE/INACTIVE state for cycle management (gets reset each iteration)\n 2. exclude_branch_conditionally() - Persistent exclusion for conditional routing\n\n When max_iterations is reached, breaks the cycle by allowing the default_route to execute.\n \"\"\"\n if not self.__iteration_updated:\n self.update_ctx({f\"{self._id}_iteration\": self.ctx.get(f\"{self._id}_iteration\", 0) + 1})\n self.__iteration_updated = True\n current_iteration = self.ctx.get(f\"{self._id}_iteration\", 0)\n\n # Check if max iterations reached and we're trying to stop the default route\n if current_iteration >= self.max_iterations and route_to_stop == self.default_route:\n # Clear ALL conditional exclusions to allow default route to execute\n if self._id in self.graph.conditional_exclusion_sources:\n previous_exclusions = self.graph.conditional_exclusion_sources[self._id]\n self.graph.conditionally_excluded_vertices -= previous_exclusions\n del self.graph.conditional_exclusion_sources[self._id]\n\n # Switch which route to stop - stop the NON-default route to break the cycle\n route_to_stop = \"true_result\" if route_to_stop == \"false_result\" else \"false_result\"\n\n # Call stop to break the cycle\n self.stop(route_to_stop)\n # Don't apply conditional exclusion when breaking cycle\n return\n\n # Normal case: Use BOTH mechanisms\n # 1. stop() for cycle management (marks INACTIVE, updates run manager, gets reset)\n self.stop(route_to_stop)\n\n # 2. Conditional exclusion for persistent routing (doesn't get reset except by this router)\n self.graph.exclude_branch_conditionally(self._id, output_name=route_to_stop)\n\n def _resolve_case_message(self, case_message: Message | str | None) -> Message:\n \"\"\"Return the override case message, falling back to the input text when it is blank.\n\n ``Case True`` and ``Case False`` are optional overrides. When left blank the\n ``MessageInput`` resolves to an empty ``Message``, so the original ``Text Input``\n is routed through instead of an empty message.\n\n A ``Message`` is only treated as blank when it carries no payload at all. An\n override with empty text but meaningful content (files or content blocks) is\n preserved as-is so that payload is not dropped.\n\n Blankness is judged by payload only (``text``, ``files``, ``content_blocks``).\n A ``Message`` carrying just metadata (``sender``, ``properties``, ``error`` ...)\n and no payload is intentionally treated as blank: every empty ``Message`` also\n defaults ``timestamp`` / ``category`` / ``properties``, so keying on metadata\n would defeat the blank-field fallback. With no payload there is nothing to\n route, so the ``Text Input`` is used.\n\n ``case_message`` is typed defensively as ``Message | str | None``. ``MessageInput``\n wraps user input into a ``Message`` on the validated graph path, but a raw ``str``\n can still arrive via direct/programmatic assignment (``.set(...)`` stores the raw\n value), so the ``str`` branch is retained rather than dropping such an override.\n \"\"\"\n if isinstance(case_message, Message):\n if case_message.text or case_message.files or case_message.content_blocks:\n return case_message\n return Message(text=self.input_text)\n if isinstance(case_message, str) and case_message:\n return Message(text=case_message)\n return Message(text=self.input_text)\n\n def true_response(self) -> Message:\n result = self.evaluate_condition(\n self.input_text, self.match_text, self.operator, case_sensitive=self.case_sensitive\n )\n\n # Check if we should force output due to max_iterations on default route\n current_iteration = self.ctx.get(f\"{self._id}_iteration\", 0)\n force_output = current_iteration >= self.max_iterations and self.default_route == \"true_result\"\n\n if result or force_output:\n true_message = self._resolve_case_message(self.true_case_message)\n self.status = true_message\n if not force_output: # Only stop the other branch if not forcing due to max iterations\n self.iterate_and_stop_once(\"false_result\")\n return true_message\n self.iterate_and_stop_once(\"true_result\")\n return Message(text=\"\")\n\n def false_response(self) -> Message:\n result = self.evaluate_condition(\n self.input_text, self.match_text, self.operator, case_sensitive=self.case_sensitive\n )\n\n if not result:\n false_message = self._resolve_case_message(self.false_case_message)\n self.status = false_message\n self.iterate_and_stop_once(\"true_result\")\n return false_message\n\n self.iterate_and_stop_once(\"false_result\")\n return Message(text=\"\")\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:\n if field_name == \"operator\":\n if field_value == \"regex\":\n build_config.pop(\"case_sensitive\", None)\n elif \"case_sensitive\" not in build_config:\n case_sensitive_input = next(\n (input_field for input_field in self.inputs if input_field.name == \"case_sensitive\"), None\n )\n if case_sensitive_input:\n build_config[\"case_sensitive\"] = case_sensitive_input.to_dict()\n return build_config\n", - "fileTypes": [], - "file_path": "", - "password": false, - "name": "code", - "advanced": true, - "api_editable": false, - "dynamic": true, - "info": "", - "load_from_db": false, - "title_case": false - }, - "default_route": { - "tool_mode": false, - "trace_as_metadata": true, - "options": [ - "true_result", - "false_result" - ], - "options_metadata": [], - "combobox": false, - "dialog_inputs": {}, - "toggle": false, - "override_skip": false, - "required": false, - "placeholder": "", - "show": true, - "name": "default_route", - "value": "false_result", - "display_name": "Default Route", - "advanced": true, - "api_editable": false, - "dynamic": false, - "info": "The default route to take when max iterations are reached.", - "title_case": false, - "track_in_telemetry": true, - "external_options": {}, - "type": "str", - "_input_type": "DropdownInput" - }, - "false_case_message": { - "trace_as_input": true, - "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": "false_case_message", - "value": "Standardfall: automatisch freigegeben", - "display_name": "Case False", - "advanced": true, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "The message to pass if the condition is False.", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageInput" - }, - "input_text": { - "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": true, - "placeholder": "", - "show": true, - "name": "input_text", - "value": "", - "display_name": "Text Input", - "advanced": false, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "The primary text input for the operation.", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageTextInput" - }, - "match_text": { - "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": true, - "placeholder": "", - "show": true, - "name": "match_text", - "value": "PRUEFUNG", - "display_name": "Match Text", - "advanced": false, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "The text input to compare against.", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageTextInput" - }, - "max_iterations": { - "tool_mode": false, - "trace_as_metadata": true, - "list": false, - "list_add_label": "Add More", - "override_skip": false, - "required": false, - "placeholder": "", - "show": true, - "name": "max_iterations", - "value": 10, - "display_name": "Max Iterations", - "advanced": true, - "api_editable": false, - "dynamic": false, - "info": "The maximum number of iterations for the conditional router.", - "title_case": false, - "track_in_telemetry": true, - "type": "int", - "_input_type": "IntInput" - }, - "operator": { - "tool_mode": false, - "trace_as_metadata": true, - "options": [ - "equals", - "not equals", - "contains", - "starts with", - "ends with", - "regex", - "less than", - "less than or equal", - "greater than", - "greater than or equal" - ], - "options_metadata": [], - "combobox": false, - "dialog_inputs": {}, - "toggle": false, - "override_skip": false, - "required": false, - "placeholder": "", - "show": true, - "name": "operator", - "value": "equals", - "display_name": "Operator", - "advanced": false, - "api_editable": false, - "dynamic": false, - "info": "The operator to apply for comparing the texts.", - "real_time_refresh": true, - "title_case": false, - "track_in_telemetry": true, - "external_options": {}, - "type": "str", - "_input_type": "DropdownInput" - }, - "true_case_message": { - "trace_as_input": true, - "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": "true_case_message", - "value": "Sonderfall: menschliche Pruefung erforderlich", - "display_name": "Case True", - "advanced": true, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "The message to pass if the condition is True.", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageInput" - } - }, - "description": "Routes an input message to a corresponding output based on text comparison.", - "icon": "split", - "base_classes": [ - "Message" - ], - "display_name": "If-Else", - "documentation": "https://docs.langflow.org/if-else", - "minimized": false, - "custom_fields": {}, - "output_types": [], - "pinned": false, - "conditional_paths": [], - "frozen": false, - "outputs": [ - { - "types": [ - "Message" - ], - "selected": "Message", - "name": "true_result", - "display_name": "True", - "method": "true_response", - "value": "__UNDEFINED__", - "cache": true, - "allows_loop": false, - "group_outputs": true, - "tool_mode": true - }, - { - "types": [ - "Message" - ], - "selected": "Message", - "name": "false_result", - "display_name": "False", - "method": "false_response", - "value": "__UNDEFINED__", - "cache": true, - "allows_loop": false, - "group_outputs": true, - "tool_mode": true - } - ], - "field_order": [], - "beta": false, - "legacy": false, - "edited": false, - "metadata": {}, - "tool_mode": false - }, - "type": "ConditionalRouter", - "id": "ConditionalRouter-router", - "showNode": true - }, - "id": "ConditionalRouter-router", - "measured": { - "width": 320, - "height": 200 - }, - "position": { - "x": 1200, - "y": 300 - }, - "type": "genericNode" - }, - { - "data": { - "node": { - "template": { - "_type": "Component", - "callback_token": { - "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": "callback_token", - "value": "", - "display_name": "Callback Token", - "advanced": false, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageTextInput" - }, - "callback_url": { - "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": "callback_url", - "value": "", - "display_name": "Callback URL", - "advanced": false, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageTextInput" - }, - "code": { - "type": "code", - "required": true, - "placeholder": "", - "list": false, - "show": true, - "multiline": true, - "value": "from lfx.custom import Component\nfrom lfx.io import MessageTextInput, Output\nfrom lfx.schema.message import Message\n\n\nclass ApprovalCallbackComponent(Component):\n display_name = \"Approval Callback\"\n description = \"Meldet einen Sonderfall per HTTP an unsere API und terminiert.\"\n\n inputs = [\n MessageTextInput(name=\"trigger\", display_name=\"Trigger\"),\n MessageTextInput(name=\"run_id\", display_name=\"Run ID\"),\n MessageTextInput(name=\"callback_url\", display_name=\"Callback URL\"),\n MessageTextInput(\n name=\"callback_token\",\n display_name=\"Callback Token\",\n ),\n MessageTextInput(\n name=\"continuation_flow_id\",\n display_name=\"Continuation Flow ID\",\n ),\n MessageTextInput(name=\"facts\", display_name=\"Facts\"),\n ]\n outputs = [\n Output(display_name=\"Result\", name=\"result\", method=\"notify\"),\n ]\n\n def _text(self, 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 def notify(self) -> Message:\n import httpx\n import json\n\n trigger = self._text(self.trigger).strip()\n if not trigger:\n # Zweig wurde nicht genommen: kein Seiteneffekt.\n return Message(text=\"\")\n try:\n facts = json.loads(self._text(self.facts) or \"{}\")\n except Exception:\n facts = {}\n url = self._text(self.callback_url).rstrip(\"/\") + \"/api/approvals\"\n body = {\n \"run_id\": self._text(self.run_id),\n \"title\": f\"Freigabe erforderlich ({trigger})\",\n \"payload\": {\"verdict\": trigger, \"facts\": facts},\n \"continuation_flow_id\": self._text(self.continuation_flow_id) or None,\n }\n headers = {\"X-Callback-Token\": self._text(self.callback_token)}\n try:\n response = httpx.post(url, json=body, headers=headers, timeout=10.0)\n response.raise_for_status()\n except Exception as exc:\n return Message(text=f\"CALLBACK_FAILED: {exc}\")\n return Message(text=\"PENDING_HUMAN_APPROVAL\")\n", - "fileTypes": [], - "file_path": "", - "password": false, - "name": "code", - "advanced": true, - "api_editable": false, - "dynamic": true, - "info": "", - "load_from_db": false, - "title_case": false - }, - "continuation_flow_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": "continuation_flow_id", - "value": "", - "display_name": "Continuation Flow ID", - "advanced": false, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageTextInput" - }, - "facts": { - "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": "facts", - "value": "", - "display_name": "Facts", - "advanced": false, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageTextInput" - }, - "run_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": "run_id", - "value": "", - "display_name": "Run ID", - "advanced": false, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageTextInput" - }, - "trigger": { - "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": "trigger", - "value": "", - "display_name": "Trigger", - "advanced": false, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageTextInput" - } - }, - "description": "Meldet einen Sonderfall per HTTP an unsere API und terminiert.", - "base_classes": [ - "Message" - ], - "display_name": "Approval Callback", - "documentation": "", - "minimized": false, - "custom_fields": {}, - "output_types": [], - "pinned": false, - "conditional_paths": [], - "frozen": false, - "outputs": [ - { - "types": [ - "Message" - ], - "selected": "Message", - "name": "result", - "display_name": "Result", - "method": "notify", - "value": "__UNDEFINED__", - "cache": true, - "allows_loop": false, - "group_outputs": false, - "tool_mode": true - } - ], - "field_order": [ - "trigger", - "run_id", - "callback_url", - "callback_token", - "continuation_flow_id", - "facts" - ], - "beta": false, - "legacy": false, - "edited": false, - "metadata": { - "module": "custom_components.approval_callback", - "code_hash": "28b2f5894566", - "dependencies": { - "total_dependencies": 2, - "dependencies": [ - { - "name": "lfx", - "version": "1.12.0" - }, - { - "name": "httpx", - "version": "0.28.1" - } - ] - } - }, - "tool_mode": false - }, - "type": "CustomComponent", - "id": "CustomComponent-callback", - "showNode": true - }, - "id": "CustomComponent-callback", - "measured": { - "width": 320, - "height": 200 - }, - "position": { - "x": 1600, - "y": 160 - }, - "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-pending", - "showNode": true - }, - "id": "ChatOutput-pending", - "measured": { - "width": 320, - "height": 200 - }, - "position": { - "x": 2000, - "y": 160 - }, - "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-ok", - "showNode": true - }, - "id": "ChatOutput-ok", - "measured": { - "width": 320, - "height": 200 - }, - "position": { - "x": 2000, - "y": 440 - }, - "type": "genericNode" - } - ], "edges": [ { "animated": false, @@ -1994,6 +318,1682 @@ "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-okœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" } ], + "nodes": [ + { + "data": { + "id": "ChatInput-entry", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Get chat inputs from the Playground.", + "display_name": "Chat Input", + "documentation": "https://docs.langflow.org/chat-input-and-output", + "edited": false, + "field_order": [], + "frozen": false, + "icon": "MessagesSquare", + "legacy": false, + "metadata": {}, + "minimized": true, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Chat Message", + "group_outputs": false, + "method": "message_response", + "name": "message", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "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" + }, + "context_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Context ID", + "dynamic": false, + "info": "The context ID of the chat. Adds an extra layer to the local memory.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "context_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "files": { + "_input_type": "FileInput", + "advanced": true, + "api_editable": false, + "display_name": "Files", + "dynamic": false, + "fileTypes": [ + "csv", + "json", + "pdf", + "txt", + "md", + "mdx", + "yaml", + "yml", + "xml", + "html", + "htm", + "docx", + "py", + "sh", + "sql", + "js", + "ts", + "tsx", + "jpg", + "jpeg", + "png", + "bmp", + "image" + ], + "file_path": "", + "info": "Files to be sent with the message.", + "list": true, + "list_add_label": "Add More", + "name": "files", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "temp_file": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "file", + "value": "" + }, + "input_value": { + "_input_type": "MultilineInput", + "advanced": false, + "ai_enabled": false, + "api_editable": false, + "copy_field": false, + "display_name": "Input Text", + "dynamic": false, + "info": "Message to be passed as input.", + "input_types": [], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "multiline": true, + "name": "input_value", + "override_skip": false, + "password": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "sender": { + "_input_type": "DropdownInput", + "advanced": true, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Sender Type", + "dynamic": false, + "external_options": {}, + "info": "Type of sender.", + "name": "sender", + "options": [ + "Machine", + "User" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "User" + }, + "sender_name": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Sender Name", + "dynamic": false, + "info": "Name of the sender.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "sender_name", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "User" + }, + "session_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Session ID", + "dynamic": false, + "info": "The session ID of the chat. If empty, the current session ID parameter will be used.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "session_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "list_add_label": "Add More", + "name": "should_store_message", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "ChatInput" + }, + "id": "ChatInput-entry", + "measured": { + "height": 200, + "width": 320 + }, + "position": { + "x": 0, + "y": 300 + }, + "type": "genericNode" + }, + { + "data": { + "id": "CustomComponent-envelope", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Zerlegt die Eingabe-Huelle in Payload und Laufzeit-Kontext.", + "display_name": "Envelope", + "documentation": "", + "edited": false, + "field_order": [ + "raw" + ], + "frozen": false, + "legacy": false, + "metadata": { + "code_hash": "02e69613e210", + "dependencies": { + "dependencies": [ + { + "name": "lfx", + "version": "1.12.0" + } + ], + "total_dependencies": 1 + }, + "module": "custom_components.envelope" + }, + "minimized": false, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Payload", + "group_outputs": false, + "method": "get_payload", + "name": "payload", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + }, + { + "allows_loop": false, + "cache": true, + "display_name": "Run ID", + "group_outputs": false, + "method": "get_run_id", + "name": "run_id", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + }, + { + "allows_loop": false, + "cache": true, + "display_name": "Callback URL", + "group_outputs": false, + "method": "get_callback_url", + "name": "callback_url", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + }, + { + "allows_loop": false, + "cache": true, + "display_name": "Continuation", + "group_outputs": false, + "method": "get_continuation", + "name": "continuation", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + }, + { + "allows_loop": false, + "cache": true, + "display_name": "Callback Token", + "group_outputs": false, + "method": "get_callback_token", + "name": "callback_token", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "value": "from lfx.custom import Component\nfrom lfx.io import MessageTextInput, Output\nfrom lfx.schema.message import Message\nimport json\n\n\nclass EnvelopeComponent(Component):\n display_name = \"Envelope\"\n description = \"Zerlegt die Eingabe-Huelle in Payload und Laufzeit-Kontext.\"\n\n inputs = [\n MessageTextInput(name=\"raw\", display_name=\"Raw Input\"),\n ]\n outputs = [\n Output(display_name=\"Payload\", name=\"payload\", method=\"get_payload\"),\n Output(display_name=\"Run ID\", name=\"run_id\", method=\"get_run_id\"),\n Output(\n display_name=\"Callback URL\",\n name=\"callback_url\",\n method=\"get_callback_url\",\n ),\n Output(\n display_name=\"Continuation\",\n name=\"continuation\",\n method=\"get_continuation\",\n ),\n Output(\n display_name=\"Callback Token\",\n name=\"callback_token\",\n method=\"get_callback_token\",\n ),\n ]\n\n def _text(self, 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 def _env(self) -> dict:\n if not hasattr(self, \"_env_cache\"):\n try:\n self._env_cache = json.loads(self._text(self.raw) or \"{}\")\n except Exception:\n self._env_cache = {}\n if not isinstance(self._env_cache, dict):\n self._env_cache = {}\n return self._env_cache\n\n def get_payload(self) -> Message:\n return Message(text=json.dumps(self._env().get(\"payload\", {})))\n\n def get_run_id(self) -> Message:\n return Message(text=str(self._env().get(\"run_id\", \"\")))\n\n def get_callback_url(self) -> Message:\n return Message(text=str(self._env().get(\"callback_url\", \"\")))\n\n def get_continuation(self) -> Message:\n value = self._env().get(\"continuation_flow_id\")\n return Message(text=str(value) if value else \"\")\n\n def get_callback_token(self) -> Message:\n return Message(text=str(self._env().get(\"callback_token\", \"\")))\n" + }, + "raw": { + "_input_type": "MessageTextInput", + "advanced": false, + "api_editable": false, + "display_name": "Raw Input", + "dynamic": false, + "info": "", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "raw", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "CustomComponent" + }, + "id": "CustomComponent-envelope", + "measured": { + "height": 200, + "width": 320 + }, + "position": { + "x": 400, + "y": 300 + }, + "type": "genericNode" + }, + { + "data": { + "id": "CustomComponent-rule", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Bewertet Betrag gegen Schwelle aus JSON-Eingabe.", + "display_name": "Rule Check", + "documentation": "", + "edited": false, + "field_order": [ + "payload" + ], + "frozen": false, + "legacy": false, + "metadata": { + "code_hash": "0b9f8a009fcf", + "dependencies": { + "dependencies": [ + { + "name": "lfx", + "version": "1.12.0" + } + ], + "total_dependencies": 1 + }, + "module": "custom_components.rule_check" + }, + "minimized": false, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Verdict", + "group_outputs": false, + "method": "check", + "name": "verdict", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "value": "from lfx.custom import Component\nfrom lfx.io import MessageTextInput, Output\nfrom lfx.schema.message import Message\nimport json\n\n\nclass RuleCheckComponent(Component):\n display_name = \"Rule Check\"\n description = \"Bewertet Betrag gegen Schwelle aus JSON-Eingabe.\"\n\n inputs = [\n MessageTextInput(name=\"payload\", display_name=\"Payload\"),\n ]\n outputs = [\n Output(display_name=\"Verdict\", name=\"verdict\", method=\"check\"),\n ]\n\n def _text(self, 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 def check(self) -> Message:\n try:\n data = json.loads(self._text(self.payload) or \"{}\")\n except Exception:\n data = {}\n if not isinstance(data, dict):\n data = {}\n # Die API ruft Flows immer mit Huelle auf\n # {\"payload\": {...}, \"run_id\": ..., ...}; rohe Payloads gehen auch.\n if isinstance(data.get(\"payload\"), dict):\n data = data[\"payload\"]\n try:\n betrag = float(data.get(\"betrag\", 0) or 0)\n except (TypeError, ValueError):\n betrag = 0.0\n try:\n schwelle = float(data.get(\"schwelle\", 200000) or 200000)\n except (TypeError, ValueError):\n schwelle = 200000.0\n return Message(text=\"PRUEFUNG\" if betrag > schwelle else \"OK\")\n" + }, + "payload": { + "_input_type": "MessageTextInput", + "advanced": false, + "api_editable": false, + "display_name": "Payload", + "dynamic": false, + "info": "", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "payload", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "CustomComponent" + }, + "id": "CustomComponent-rule", + "measured": { + "height": 200, + "width": 320 + }, + "position": { + "x": 800, + "y": 300 + }, + "type": "genericNode" + }, + { + "data": { + "id": "ConditionalRouter-router", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Routes an input message to a corresponding output based on text comparison.", + "display_name": "If-Else", + "documentation": "https://docs.langflow.org/if-else", + "edited": false, + "field_order": [], + "frozen": false, + "icon": "split", + "legacy": false, + "metadata": {}, + "minimized": false, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "True", + "group_outputs": true, + "method": "true_response", + "name": "true_result", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + }, + { + "allows_loop": false, + "cache": true, + "display_name": "False", + "group_outputs": true, + "method": "false_response", + "name": "false_result", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "case_sensitive": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Case Sensitive", + "dynamic": false, + "info": "If true, the comparison will be case sensitive.", + "list": false, + "list_add_label": "Add More", + "name": "case_sensitive", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + }, + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "value": "import re\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageInput, MessageTextInput, Output\nfrom lfx.schema.message import Message\n\n\nclass ConditionalRouterComponent(Component):\n display_name = \"If-Else\"\n description = \"Routes an input message to a corresponding output based on text comparison.\"\n documentation: str = \"https://docs.langflow.org/if-else\"\n icon = \"split\"\n name = \"ConditionalRouter\"\n\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self.__iteration_updated = False\n\n inputs = [\n MessageTextInput(\n name=\"input_text\",\n display_name=\"Text Input\",\n info=\"The primary text input for the operation.\",\n required=True,\n ),\n DropdownInput(\n name=\"operator\",\n display_name=\"Operator\",\n options=[\n \"equals\",\n \"not equals\",\n \"contains\",\n \"starts with\",\n \"ends with\",\n \"regex\",\n \"less than\",\n \"less than or equal\",\n \"greater than\",\n \"greater than or equal\",\n ],\n info=\"The operator to apply for comparing the texts.\",\n value=\"equals\",\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"match_text\",\n display_name=\"Match Text\",\n info=\"The text input to compare against.\",\n required=True,\n ),\n BoolInput(\n name=\"case_sensitive\",\n display_name=\"Case Sensitive\",\n info=\"If true, the comparison will be case sensitive.\",\n value=True,\n advanced=True,\n ),\n MessageInput(\n name=\"true_case_message\",\n display_name=\"Case True\",\n info=\"The message to pass if the condition is True.\",\n advanced=True,\n ),\n MessageInput(\n name=\"false_case_message\",\n display_name=\"Case False\",\n info=\"The message to pass if the condition is False.\",\n advanced=True,\n ),\n IntInput(\n name=\"max_iterations\",\n display_name=\"Max Iterations\",\n info=\"The maximum number of iterations for the conditional router.\",\n value=10,\n advanced=True,\n ),\n DropdownInput(\n name=\"default_route\",\n display_name=\"Default Route\",\n options=[\"true_result\", \"false_result\"],\n info=\"The default route to take when max iterations are reached.\",\n value=\"false_result\",\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"True\", name=\"true_result\", method=\"true_response\", group_outputs=True),\n Output(display_name=\"False\", name=\"false_result\", method=\"false_response\", group_outputs=True),\n ]\n\n def _pre_run_setup(self):\n self.__iteration_updated = False\n\n def evaluate_condition(self, input_text: str, match_text: str, operator: str, *, case_sensitive: bool) -> bool:\n if not case_sensitive and operator != \"regex\":\n input_text = input_text.lower()\n match_text = match_text.lower()\n\n if operator == \"equals\":\n return input_text == match_text\n if operator == \"not equals\":\n return input_text != match_text\n if operator == \"contains\":\n return match_text in input_text\n if operator == \"starts with\":\n return input_text.startswith(match_text)\n if operator == \"ends with\":\n return input_text.endswith(match_text)\n if operator == \"regex\":\n try:\n return bool(re.match(match_text, input_text))\n except re.error:\n return False # Return False if the regex is invalid\n if operator in [\"less than\", \"less than or equal\", \"greater than\", \"greater than or equal\"]:\n try:\n input_num = float(input_text)\n match_num = float(match_text)\n if operator == \"less than\":\n return input_num < match_num\n if operator == \"less than or equal\":\n return input_num <= match_num\n if operator == \"greater than\":\n return input_num > match_num\n if operator == \"greater than or equal\":\n return input_num >= match_num\n except ValueError:\n return False # Invalid number format for comparison\n return False\n\n def iterate_and_stop_once(self, route_to_stop: str):\n \"\"\"Handles cycle iteration counting and branch exclusion.\n\n Uses two complementary mechanisms:\n 1. stop() - ACTIVE/INACTIVE state for cycle management (gets reset each iteration)\n 2. exclude_branch_conditionally() - Persistent exclusion for conditional routing\n\n When max_iterations is reached, breaks the cycle by allowing the default_route to execute.\n \"\"\"\n if not self.__iteration_updated:\n self.update_ctx({f\"{self._id}_iteration\": self.ctx.get(f\"{self._id}_iteration\", 0) + 1})\n self.__iteration_updated = True\n current_iteration = self.ctx.get(f\"{self._id}_iteration\", 0)\n\n # Check if max iterations reached and we're trying to stop the default route\n if current_iteration >= self.max_iterations and route_to_stop == self.default_route:\n # Clear ALL conditional exclusions to allow default route to execute\n if self._id in self.graph.conditional_exclusion_sources:\n previous_exclusions = self.graph.conditional_exclusion_sources[self._id]\n self.graph.conditionally_excluded_vertices -= previous_exclusions\n del self.graph.conditional_exclusion_sources[self._id]\n\n # Switch which route to stop - stop the NON-default route to break the cycle\n route_to_stop = \"true_result\" if route_to_stop == \"false_result\" else \"false_result\"\n\n # Call stop to break the cycle\n self.stop(route_to_stop)\n # Don't apply conditional exclusion when breaking cycle\n return\n\n # Normal case: Use BOTH mechanisms\n # 1. stop() for cycle management (marks INACTIVE, updates run manager, gets reset)\n self.stop(route_to_stop)\n\n # 2. Conditional exclusion for persistent routing (doesn't get reset except by this router)\n self.graph.exclude_branch_conditionally(self._id, output_name=route_to_stop)\n\n def _resolve_case_message(self, case_message: Message | str | None) -> Message:\n \"\"\"Return the override case message, falling back to the input text when it is blank.\n\n ``Case True`` and ``Case False`` are optional overrides. When left blank the\n ``MessageInput`` resolves to an empty ``Message``, so the original ``Text Input``\n is routed through instead of an empty message.\n\n A ``Message`` is only treated as blank when it carries no payload at all. An\n override with empty text but meaningful content (files or content blocks) is\n preserved as-is so that payload is not dropped.\n\n Blankness is judged by payload only (``text``, ``files``, ``content_blocks``).\n A ``Message`` carrying just metadata (``sender``, ``properties``, ``error`` ...)\n and no payload is intentionally treated as blank: every empty ``Message`` also\n defaults ``timestamp`` / ``category`` / ``properties``, so keying on metadata\n would defeat the blank-field fallback. With no payload there is nothing to\n route, so the ``Text Input`` is used.\n\n ``case_message`` is typed defensively as ``Message | str | None``. ``MessageInput``\n wraps user input into a ``Message`` on the validated graph path, but a raw ``str``\n can still arrive via direct/programmatic assignment (``.set(...)`` stores the raw\n value), so the ``str`` branch is retained rather than dropping such an override.\n \"\"\"\n if isinstance(case_message, Message):\n if case_message.text or case_message.files or case_message.content_blocks:\n return case_message\n return Message(text=self.input_text)\n if isinstance(case_message, str) and case_message:\n return Message(text=case_message)\n return Message(text=self.input_text)\n\n def true_response(self) -> Message:\n result = self.evaluate_condition(\n self.input_text, self.match_text, self.operator, case_sensitive=self.case_sensitive\n )\n\n # Check if we should force output due to max_iterations on default route\n current_iteration = self.ctx.get(f\"{self._id}_iteration\", 0)\n force_output = current_iteration >= self.max_iterations and self.default_route == \"true_result\"\n\n if result or force_output:\n true_message = self._resolve_case_message(self.true_case_message)\n self.status = true_message\n if not force_output: # Only stop the other branch if not forcing due to max iterations\n self.iterate_and_stop_once(\"false_result\")\n return true_message\n self.iterate_and_stop_once(\"true_result\")\n return Message(text=\"\")\n\n def false_response(self) -> Message:\n result = self.evaluate_condition(\n self.input_text, self.match_text, self.operator, case_sensitive=self.case_sensitive\n )\n\n if not result:\n false_message = self._resolve_case_message(self.false_case_message)\n self.status = false_message\n self.iterate_and_stop_once(\"true_result\")\n return false_message\n\n self.iterate_and_stop_once(\"false_result\")\n return Message(text=\"\")\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:\n if field_name == \"operator\":\n if field_value == \"regex\":\n build_config.pop(\"case_sensitive\", None)\n elif \"case_sensitive\" not in build_config:\n case_sensitive_input = next(\n (input_field for input_field in self.inputs if input_field.name == \"case_sensitive\"), None\n )\n if case_sensitive_input:\n build_config[\"case_sensitive\"] = case_sensitive_input.to_dict()\n return build_config\n" + }, + "default_route": { + "_input_type": "DropdownInput", + "advanced": true, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Default Route", + "dynamic": false, + "external_options": {}, + "info": "The default route to take when max iterations are reached.", + "name": "default_route", + "options": [ + "true_result", + "false_result" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "false_result" + }, + "false_case_message": { + "_input_type": "MessageInput", + "advanced": true, + "api_editable": false, + "display_name": "Case False", + "dynamic": false, + "info": "The message to pass if the condition is False.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "false_case_message", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "Standardfall: automatisch freigegeben" + }, + "input_text": { + "_input_type": "MessageTextInput", + "advanced": false, + "api_editable": false, + "display_name": "Text Input", + "dynamic": false, + "info": "The primary text input for the operation.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "input_text", + "override_skip": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "match_text": { + "_input_type": "MessageTextInput", + "advanced": false, + "api_editable": false, + "display_name": "Match Text", + "dynamic": false, + "info": "The text input to compare against.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "match_text", + "override_skip": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "PRUEFUNG" + }, + "max_iterations": { + "_input_type": "IntInput", + "advanced": true, + "api_editable": false, + "display_name": "Max Iterations", + "dynamic": false, + "info": "The maximum number of iterations for the conditional router.", + "list": false, + "list_add_label": "Add More", + "name": "max_iterations", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "int", + "value": 10 + }, + "operator": { + "_input_type": "DropdownInput", + "advanced": false, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Operator", + "dynamic": false, + "external_options": {}, + "info": "The operator to apply for comparing the texts.", + "name": "operator", + "options": [ + "equals", + "not equals", + "contains", + "starts with", + "ends with", + "regex", + "less than", + "less than or equal", + "greater than", + "greater than or equal" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "real_time_refresh": true, + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "equals" + }, + "true_case_message": { + "_input_type": "MessageInput", + "advanced": true, + "api_editable": false, + "display_name": "Case True", + "dynamic": false, + "info": "The message to pass if the condition is True.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "true_case_message", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "Sonderfall: menschliche Pruefung erforderlich" + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "ConditionalRouter" + }, + "id": "ConditionalRouter-router", + "measured": { + "height": 200, + "width": 320 + }, + "position": { + "x": 1200, + "y": 300 + }, + "type": "genericNode" + }, + { + "data": { + "id": "CustomComponent-callback", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Meldet einen Sonderfall per HTTP an unsere API und terminiert.", + "display_name": "Approval Callback", + "documentation": "", + "edited": false, + "field_order": [ + "trigger", + "run_id", + "callback_url", + "callback_token", + "continuation_flow_id", + "facts" + ], + "frozen": false, + "legacy": false, + "metadata": { + "code_hash": "28b2f5894566", + "dependencies": { + "dependencies": [ + { + "name": "lfx", + "version": "1.12.0" + }, + { + "name": "httpx", + "version": "0.28.1" + } + ], + "total_dependencies": 2 + }, + "module": "custom_components.approval_callback" + }, + "minimized": false, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Result", + "group_outputs": false, + "method": "notify", + "name": "result", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "callback_token": { + "_input_type": "MessageTextInput", + "advanced": false, + "api_editable": false, + "display_name": "Callback Token", + "dynamic": false, + "info": "", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "callback_token", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "callback_url": { + "_input_type": "MessageTextInput", + "advanced": false, + "api_editable": false, + "display_name": "Callback URL", + "dynamic": false, + "info": "", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "callback_url", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "value": "from lfx.custom import Component\nfrom lfx.io import MessageTextInput, Output\nfrom lfx.schema.message import Message\n\n\nclass ApprovalCallbackComponent(Component):\n display_name = \"Approval Callback\"\n description = \"Meldet einen Sonderfall per HTTP an unsere API und terminiert.\"\n\n inputs = [\n MessageTextInput(name=\"trigger\", display_name=\"Trigger\"),\n MessageTextInput(name=\"run_id\", display_name=\"Run ID\"),\n MessageTextInput(name=\"callback_url\", display_name=\"Callback URL\"),\n MessageTextInput(\n name=\"callback_token\",\n display_name=\"Callback Token\",\n ),\n MessageTextInput(\n name=\"continuation_flow_id\",\n display_name=\"Continuation Flow ID\",\n ),\n MessageTextInput(name=\"facts\", display_name=\"Facts\"),\n ]\n outputs = [\n Output(display_name=\"Result\", name=\"result\", method=\"notify\"),\n ]\n\n def _text(self, 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 def notify(self) -> Message:\n import httpx\n import json\n\n trigger = self._text(self.trigger).strip()\n if not trigger:\n # Zweig wurde nicht genommen: kein Seiteneffekt.\n return Message(text=\"\")\n try:\n facts = json.loads(self._text(self.facts) or \"{}\")\n except Exception:\n facts = {}\n url = self._text(self.callback_url).rstrip(\"/\") + \"/api/approvals\"\n body = {\n \"run_id\": self._text(self.run_id),\n \"title\": f\"Freigabe erforderlich ({trigger})\",\n \"payload\": {\"verdict\": trigger, \"facts\": facts},\n \"continuation_flow_id\": self._text(self.continuation_flow_id) or None,\n }\n headers = {\"X-Callback-Token\": self._text(self.callback_token)}\n try:\n response = httpx.post(url, json=body, headers=headers, timeout=10.0)\n response.raise_for_status()\n except Exception as exc:\n return Message(text=f\"CALLBACK_FAILED: {exc}\")\n return Message(text=\"PENDING_HUMAN_APPROVAL\")\n" + }, + "continuation_flow_id": { + "_input_type": "MessageTextInput", + "advanced": false, + "api_editable": false, + "display_name": "Continuation Flow ID", + "dynamic": false, + "info": "", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "continuation_flow_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "facts": { + "_input_type": "MessageTextInput", + "advanced": false, + "api_editable": false, + "display_name": "Facts", + "dynamic": false, + "info": "", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "facts", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "run_id": { + "_input_type": "MessageTextInput", + "advanced": false, + "api_editable": false, + "display_name": "Run ID", + "dynamic": false, + "info": "", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "run_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "trigger": { + "_input_type": "MessageTextInput", + "advanced": false, + "api_editable": false, + "display_name": "Trigger", + "dynamic": false, + "info": "", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "trigger", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "CustomComponent" + }, + "id": "CustomComponent-callback", + "measured": { + "height": 200, + "width": 320 + }, + "position": { + "x": 1600, + "y": 160 + }, + "type": "genericNode" + }, + { + "data": { + "id": "ChatOutput-pending", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Display a chat message in the Playground.", + "display_name": "Chat Output", + "documentation": "https://docs.langflow.org/chat-input-and-output", + "edited": false, + "field_order": [], + "frozen": false, + "icon": "MessagesSquare", + "legacy": false, + "metadata": {}, + "minimized": true, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Output Message", + "group_outputs": false, + "method": "message_response", + "name": "message", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "clean_data": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Basic Clean Data", + "dynamic": false, + "info": "Whether to clean data before converting to string.", + "list": false, + "list_add_label": "Add More", + "name": "clean_data", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + }, + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "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" + }, + "context_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Context ID", + "dynamic": false, + "info": "The context ID of the chat. Adds an extra layer to the local memory.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "context_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "data_template": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Data Template", + "dynamic": false, + "info": "Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "data_template", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "{text}" + }, + "input_value": { + "_input_type": "HandleInput", + "advanced": false, + "api_editable": false, + "display_name": "Inputs", + "dynamic": false, + "info": "Message to be passed as output.", + "input_types": [ + "Data", + "JSON", + "DataFrame", + "Table", + "Message" + ], + "list": false, + "list_add_label": "Add More", + "name": "input_value", + "override_skip": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "other", + "value": "" + }, + "sender": { + "_input_type": "DropdownInput", + "advanced": true, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Sender Type", + "dynamic": false, + "external_options": {}, + "info": "Type of sender.", + "name": "sender", + "options": [ + "Machine", + "User" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "Machine" + }, + "sender_name": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Sender Name", + "dynamic": false, + "info": "Name of the sender.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "sender_name", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "AI" + }, + "session_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Session ID", + "dynamic": false, + "info": "The session ID of the chat. If empty, the current session ID parameter will be used.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "session_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "list_add_label": "Add More", + "name": "should_store_message", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "ChatOutput" + }, + "id": "ChatOutput-pending", + "measured": { + "height": 200, + "width": 320 + }, + "position": { + "x": 2000, + "y": 160 + }, + "type": "genericNode" + }, + { + "data": { + "id": "ChatOutput-ok", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Display a chat message in the Playground.", + "display_name": "Chat Output", + "documentation": "https://docs.langflow.org/chat-input-and-output", + "edited": false, + "field_order": [], + "frozen": false, + "icon": "MessagesSquare", + "legacy": false, + "metadata": {}, + "minimized": true, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Output Message", + "group_outputs": false, + "method": "message_response", + "name": "message", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "clean_data": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Basic Clean Data", + "dynamic": false, + "info": "Whether to clean data before converting to string.", + "list": false, + "list_add_label": "Add More", + "name": "clean_data", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + }, + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "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" + }, + "context_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Context ID", + "dynamic": false, + "info": "The context ID of the chat. Adds an extra layer to the local memory.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "context_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "data_template": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Data Template", + "dynamic": false, + "info": "Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "data_template", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "{text}" + }, + "input_value": { + "_input_type": "HandleInput", + "advanced": false, + "api_editable": false, + "display_name": "Inputs", + "dynamic": false, + "info": "Message to be passed as output.", + "input_types": [ + "Data", + "JSON", + "DataFrame", + "Table", + "Message" + ], + "list": false, + "list_add_label": "Add More", + "name": "input_value", + "override_skip": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "other", + "value": "" + }, + "sender": { + "_input_type": "DropdownInput", + "advanced": true, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Sender Type", + "dynamic": false, + "external_options": {}, + "info": "Type of sender.", + "name": "sender", + "options": [ + "Machine", + "User" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "Machine" + }, + "sender_name": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Sender Name", + "dynamic": false, + "info": "Name of the sender.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "sender_name", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "AI" + }, + "session_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Session ID", + "dynamic": false, + "info": "The session ID of the chat. If empty, the current session ID parameter will be used.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "session_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "list_add_label": "Add More", + "name": "should_store_message", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "ChatOutput" + }, + "id": "ChatOutput-ok", + "measured": { + "height": 200, + "width": 320 + }, + "position": { + "x": 2000, + "y": 440 + }, + "type": "genericNode" + } + ], "viewport": { "x": 0, "y": 0, @@ -2002,12 +2002,8 @@ }, "description": "Sonderfall meldet sich per HTTP an unsere API und terminiert (Spike).", "endpoint_name": null, - "icon": null, - "icon_bg_color": null, "id": "7a0a0ba8-cac3-58c4-ac65-dd6f973ab546", "is_component": false, - "locked": false, - "mcp_enabled": false, "name": "approval_handoff", "tags": [], "webhook": false diff --git a/backend/flows/intake_form.json b/backend/flows/intake_form.json index efae03f..3c64649 100644 --- a/backend/flows/intake_form.json +++ b/backend/flows/intake_form.json @@ -1,914 +1,5 @@ { "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": { - "_type": "Component", - "attachment": { - "tool_mode": false, - "trace_as_metadata": true, - "file_path": "", - "fileTypes": [ - "pdf", - "png", - "jpg" - ], - "temp_file": false, - "list": false, - "list_add_label": "Add More", - "override_skip": false, - "required": false, - "placeholder": "", - "show": true, - "name": "attachment", - "value": "", - "display_name": "Attachment", - "advanced": false, - "api_editable": false, - "dynamic": false, - "info": "String reference only for now (no upload yet)", - "title_case": false, - "track_in_telemetry": false, - "type": "file", - "_input_type": "FileInput" - }, - "age": { - "tool_mode": false, - "trace_as_metadata": true, - "list": false, - "list_add_label": "Add More", - "override_skip": false, - "required": false, - "placeholder": "", - "show": true, - "name": "age", - "value": 18, - "display_name": "Age", - "advanced": false, - "api_editable": false, - "dynamic": false, - "info": "Your age in years", - "title_case": false, - "track_in_telemetry": true, - "type": "int", - "_input_type": "IntInput" - }, - "amount": { - "tool_mode": false, - "trace_as_metadata": true, - "list": false, - "list_add_label": "Add More", - "override_skip": false, - "required": false, - "placeholder": "", - "show": true, - "name": "amount", - "value": 0.0, - "display_name": "Amount", - "advanced": false, - "api_editable": false, - "dynamic": false, - "info": "Amount in euros", - "title_case": false, - "track_in_telemetry": true, - "type": "float", - "_input_type": "FloatInput" - }, - "department": { - "tool_mode": false, - "trace_as_metadata": true, - "options": [ - "Music", - "Youth", - "Seniors", - "Admin" - ], - "options_metadata": [], - "combobox": false, - "dialog_inputs": {}, - "toggle": false, - "override_skip": false, - "required": false, - "placeholder": "", - "show": true, - "name": "department", - "value": "Music", - "display_name": "Department", - "advanced": false, - "api_editable": false, - "dynamic": false, - "info": "Which team is this for", - "title_case": false, - "track_in_telemetry": true, - "external_options": {}, - "type": "str", - "_input_type": "DropdownInput" - }, - "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" - }, - "full_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": "full_name", - "value": " ", - "display_name": "Full name", - "advanced": false, - "api_editable": false, - "dynamic": false, - "info": "Your first and last name", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "StrInput" - }, - "newsletter": { - "tool_mode": false, - "trace_as_metadata": true, - "list": false, - "list_add_label": "Add More", - "override_skip": false, - "required": false, - "placeholder": "", - "show": true, - "name": "newsletter", - "value": false, - "display_name": "Newsletter", - "advanced": false, - "api_editable": false, - "dynamic": false, - "info": "Send me the parish newsletter", - "title_case": false, - "track_in_telemetry": true, - "type": "bool", - "_input_type": "BoolInput" - }, - "notes": { - "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": "notes", - "value": "", - "display_name": "Notes", - "advanced": false, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "Anything else we should know", - "title_case": false, - "track_in_telemetry": false, - "copy_field": false, - "password": false, - "type": "str", - "_input_type": "MultilineInput" - }, - "password": { - "load_from_db": true, - "override_skip": false, - "required": false, - "placeholder": "", - "show": true, - "name": "password", - "value": "", - "display_name": "Password", - "advanced": false, - "api_editable": false, - "input_types": [], - "dynamic": false, - "info": "Optional access phrase", - "title_case": false, - "track_in_telemetry": false, - "password": true, - "type": "str", - "_input_type": "SecretStrInput" - }, - "satisfaction": { - "tool_mode": false, - "min_label": "", - "max_label": "", - "min_label_icon": "", - "max_label_icon": "", - "slider_buttons": false, - "slider_buttons_options": [], - "slider_input": false, - "range_spec": { - "step_type": "float", - "min": 1.0, - "max": 5.0, - "step": 1.0 - }, - "override_skip": false, - "required": false, - "placeholder": "", - "show": true, - "name": "satisfaction", - "value": 3, - "display_name": "Satisfaction", - "advanced": false, - "api_editable": false, - "dynamic": false, - "info": "How satisfied are you (1-5)", - "title_case": false, - "track_in_telemetry": false, - "type": "slider", - "_input_type": "SliderInput" - }, - "topics": { - "tool_mode": false, - "trace_as_metadata": true, - "options": [ - "Worship", - "Outreach", - "Courses" - ], - "combobox": false, - "toggle": false, - "list": true, - "list_add_label": "Add More", - "override_skip": false, - "required": false, - "placeholder": "", - "show": true, - "name": "topics", - "value": [], - "display_name": "Topics", - "advanced": false, - "api_editable": false, - "dynamic": false, - "info": "What are you interested in", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MultiselectInput" - }, - "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": "\"\"\"Structured form-input component for kirche-einfach flows.\n\nA flow that starts with :class:`CustomInputForm` declares the fields of its\nweb form. The API reads the exported node template\n(``template`` minus ``code``) and derives ``{json_schema, ui_schema}`` from\nthe ``lfx.io`` input definitions below — one JSON-Schema property per field,\nkeyed by ``name``. Field metadata maps as follows:\n\n* ``display_name`` → ``title``, ``info`` → ``description``,\n ``value`` → ``default``, ``options`` → ``enum``\n* ``required`` → required array, ``advanced`` → ``ui:collapsed`` hint\n* ``_input_type`` → JSON type + widget (see ``app/flows/input_schema.py``)\n\n``FileInput`` is a stub for now: it carries a string reference only, real\nupload comes later.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\n\nfrom lfx.custom import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.io import (\n BoolInput,\n DropdownInput,\n FileInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n MultilineInput,\n MultiselectInput,\n Output,\n SecretStrInput,\n SliderInput,\n StrInput,\n)\nfrom lfx.schema.message import Message\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\nclass CustomInputForm(Component):\n display_name = \"CustomInput Form\"\n description = \"Collects structured form values into a single JSON message.\"\n icon = \"ClipboardList\"\n\n inputs = [\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 StrInput(\n name=\"full_name\",\n display_name=\"Full name\",\n info=\"Your first and last name\",\n value=\"\",\n required=True,\n ),\n MultilineInput(\n name=\"notes\",\n display_name=\"Notes\",\n info=\"Anything else we should know\",\n value=\"\",\n ),\n SecretStrInput(\n name=\"password\",\n display_name=\"Password\",\n info=\"Optional access phrase\",\n value=\"\",\n ),\n IntInput(\n name=\"age\",\n display_name=\"Age\",\n info=\"Your age in years\",\n value=18,\n ),\n FloatInput(\n name=\"amount\",\n display_name=\"Amount\",\n info=\"Amount in euros\",\n value=0.0,\n ),\n BoolInput(\n name=\"newsletter\",\n display_name=\"Newsletter\",\n info=\"Send me the parish newsletter\",\n value=False,\n ),\n DropdownInput(\n name=\"department\",\n display_name=\"Department\",\n info=\"Which team is this for\",\n options=[\"Music\", \"Youth\", \"Seniors\", \"Admin\"],\n value=\"Music\",\n ),\n MultiselectInput(\n name=\"topics\",\n display_name=\"Topics\",\n info=\"What are you interested in\",\n options=[\"Worship\", \"Outreach\", \"Courses\"],\n value=[],\n ),\n SliderInput(\n name=\"satisfaction\",\n display_name=\"Satisfaction\",\n info=\"How satisfied are you (1-5)\",\n value=3,\n range_spec=RangeSpec(min=1, max=5, step=1),\n ),\n FileInput(\n name=\"attachment\",\n display_name=\"Attachment\",\n info=\"String reference only for now (no upload yet)\",\n file_types=[\"pdf\", \"png\", \"jpg\"],\n ),\n ]\n outputs = [\n Output(display_name=\"Form Data\", name=\"form_data\", method=\"collect\"),\n ]\n\n def collect(self) -> Message:\n data = {\n \"full_name\": _text(self.full_name),\n \"notes\": _text(self.notes),\n \"password\": _text(self.password),\n \"age\": self.age,\n \"amount\": self.amount,\n \"newsletter\": self.newsletter,\n \"department\": _text(self.department),\n \"topics\": self.topics,\n \"satisfaction\": self.satisfaction,\n \"attachment\": _text(self.attachment),\n }\n # Submitted values (via form_values) win over the field defaults.\n # Accepts the raw dict or the run envelope {\"payload\": {...}}.\n try:\n override = json.loads(_text(self.form_values) or \"{}\")\n except Exception:\n override = {}\n if isinstance(override, dict):\n if isinstance(override.get(\"payload\"), dict):\n override = override[\"payload\"]\n for key in data:\n if key in override:\n data[key] = override[key]\n return Message(text=json.dumps(data, ensure_ascii=False))\n" - } - }, - "description": "Collects structured form values into a single JSON message.", - "icon": "ClipboardList", - "base_classes": [], - "display_name": "CustomInput Form", - "documentation": "", - "minimized": false, - "custom_fields": {}, - "output_types": [], - "pinned": false, - "conditional_paths": [], - "frozen": false, - "outputs": [ - { - "types": [ - "Message" - ], - "selected": "Message", - "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": "CustomInputForm", - "id": "CustomInputForm-intake", - "showNode": true - }, - "id": "CustomInputForm-intake", - "measured": { - "width": 320, - "height": 200 - }, - "position": { - "x": 400, - "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": 800, - "y": 200 - }, - "type": "genericNode" - } - ], "edges": [ { "animated": false, @@ -971,6 +62,915 @@ "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-collectedœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" } ], + "nodes": [ + { + "data": { + "id": "ChatInput-entry", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Get chat inputs from the Playground.", + "display_name": "Chat Input", + "documentation": "https://docs.langflow.org/chat-input-and-output", + "edited": false, + "field_order": [], + "frozen": false, + "icon": "MessagesSquare", + "legacy": false, + "metadata": {}, + "minimized": true, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Chat Message", + "group_outputs": false, + "method": "message_response", + "name": "message", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "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" + }, + "context_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Context ID", + "dynamic": false, + "info": "The context ID of the chat. Adds an extra layer to the local memory.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "context_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "files": { + "_input_type": "FileInput", + "advanced": true, + "api_editable": false, + "display_name": "Files", + "dynamic": false, + "fileTypes": [ + "csv", + "json", + "pdf", + "txt", + "md", + "mdx", + "yaml", + "yml", + "xml", + "html", + "htm", + "docx", + "py", + "sh", + "sql", + "js", + "ts", + "tsx", + "jpg", + "jpeg", + "png", + "bmp", + "image" + ], + "file_path": "", + "info": "Files to be sent with the message.", + "list": true, + "list_add_label": "Add More", + "name": "files", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "temp_file": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "file", + "value": "" + }, + "input_value": { + "_input_type": "MultilineInput", + "advanced": false, + "ai_enabled": false, + "api_editable": false, + "copy_field": false, + "display_name": "Input Text", + "dynamic": false, + "info": "Message to be passed as input.", + "input_types": [], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "multiline": true, + "name": "input_value", + "override_skip": false, + "password": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "sender": { + "_input_type": "DropdownInput", + "advanced": true, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Sender Type", + "dynamic": false, + "external_options": {}, + "info": "Type of sender.", + "name": "sender", + "options": [ + "Machine", + "User" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "User" + }, + "sender_name": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Sender Name", + "dynamic": false, + "info": "Name of the sender.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "sender_name", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "User" + }, + "session_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Session ID", + "dynamic": false, + "info": "The session ID of the chat. If empty, the current session ID parameter will be used.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "session_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "list_add_label": "Add More", + "name": "should_store_message", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "ChatInput" + }, + "id": "ChatInput-entry", + "measured": { + "height": 200, + "width": 320 + }, + "position": { + "x": 0, + "y": 200 + }, + "type": "genericNode" + }, + { + "data": { + "id": "CustomInputForm-intake", + "node": { + "base_classes": [], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Collects structured form values into a single JSON message.", + "display_name": "CustomInput Form", + "documentation": "", + "edited": false, + "field_order": [], + "frozen": false, + "icon": "ClipboardList", + "legacy": false, + "metadata": {}, + "minimized": false, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Form Data", + "group_outputs": false, + "method": "collect", + "name": "form_data", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "age": { + "_input_type": "IntInput", + "advanced": false, + "api_editable": false, + "display_name": "Age", + "dynamic": false, + "info": "Your age in years", + "list": false, + "list_add_label": "Add More", + "name": "age", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "int", + "value": 18 + }, + "amount": { + "_input_type": "FloatInput", + "advanced": false, + "api_editable": false, + "display_name": "Amount", + "dynamic": false, + "info": "Amount in euros", + "list": false, + "list_add_label": "Add More", + "name": "amount", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "float", + "value": 0.0 + }, + "attachment": { + "_input_type": "FileInput", + "advanced": false, + "api_editable": false, + "display_name": "Attachment", + "dynamic": false, + "fileTypes": [ + "pdf", + "png", + "jpg" + ], + "file_path": "", + "info": "String reference only for now (no upload yet)", + "list": false, + "list_add_label": "Add More", + "name": "attachment", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "temp_file": false, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "file", + "value": "" + }, + "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": "\"\"\"Structured form-input component for kirche-einfach flows.\n\nA flow that starts with :class:`CustomInputForm` declares the fields of its\nweb form. The API reads the exported node template\n(``template`` minus ``code``) and derives ``{json_schema, ui_schema}`` from\nthe ``lfx.io`` input definitions below — one JSON-Schema property per field,\nkeyed by ``name``. Field metadata maps as follows:\n\n* ``display_name`` → ``title``, ``info`` → ``description``,\n ``value`` → ``default``, ``options`` → ``enum``\n* ``required`` → required array, ``advanced`` → ``ui:collapsed`` hint\n* ``_input_type`` → JSON type + widget (see ``app/flows/input_schema.py``)\n\n``FileInput`` is a stub for now: it carries a string reference only, real\nupload comes later.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\n\nfrom lfx.custom import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.io import (\n BoolInput,\n DropdownInput,\n FileInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n MultilineInput,\n MultiselectInput,\n Output,\n SecretStrInput,\n SliderInput,\n StrInput,\n)\nfrom lfx.schema.message import Message\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\nclass CustomInputForm(Component):\n display_name = \"CustomInput Form\"\n description = \"Collects structured form values into a single JSON message.\"\n icon = \"ClipboardList\"\n\n inputs = [\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 StrInput(\n name=\"full_name\",\n display_name=\"Full name\",\n info=\"Your first and last name\",\n value=\"\",\n required=True,\n ),\n MultilineInput(\n name=\"notes\",\n display_name=\"Notes\",\n info=\"Anything else we should know\",\n value=\"\",\n ),\n SecretStrInput(\n name=\"password\",\n display_name=\"Password\",\n info=\"Optional access phrase\",\n value=\"\",\n ),\n IntInput(\n name=\"age\",\n display_name=\"Age\",\n info=\"Your age in years\",\n value=18,\n ),\n FloatInput(\n name=\"amount\",\n display_name=\"Amount\",\n info=\"Amount in euros\",\n value=0.0,\n ),\n BoolInput(\n name=\"newsletter\",\n display_name=\"Newsletter\",\n info=\"Send me the parish newsletter\",\n value=False,\n ),\n DropdownInput(\n name=\"department\",\n display_name=\"Department\",\n info=\"Which team is this for\",\n options=[\"Music\", \"Youth\", \"Seniors\", \"Admin\"],\n value=\"Music\",\n ),\n MultiselectInput(\n name=\"topics\",\n display_name=\"Topics\",\n info=\"What are you interested in\",\n options=[\"Worship\", \"Outreach\", \"Courses\"],\n value=[],\n ),\n SliderInput(\n name=\"satisfaction\",\n display_name=\"Satisfaction\",\n info=\"How satisfied are you (1-5)\",\n value=3,\n range_spec=RangeSpec(min=1, max=5, step=1),\n ),\n FileInput(\n name=\"attachment\",\n display_name=\"Attachment\",\n info=\"String reference only for now (no upload yet)\",\n file_types=[\"pdf\", \"png\", \"jpg\"],\n ),\n ]\n outputs = [\n Output(display_name=\"Form Data\", name=\"form_data\", method=\"collect\"),\n ]\n\n def collect(self) -> Message:\n data = {\n \"full_name\": _text(self.full_name),\n \"notes\": _text(self.notes),\n \"password\": _text(self.password),\n \"age\": self.age,\n \"amount\": self.amount,\n \"newsletter\": self.newsletter,\n \"department\": _text(self.department),\n \"topics\": self.topics,\n \"satisfaction\": self.satisfaction,\n \"attachment\": _text(self.attachment),\n }\n # Submitted values (via form_values) win over the field defaults.\n # Accepts the raw dict or the run envelope {\"payload\": {...}}.\n try:\n override = json.loads(_text(self.form_values) or \"{}\")\n except Exception:\n override = {}\n if isinstance(override, dict):\n if isinstance(override.get(\"payload\"), dict):\n override = override[\"payload\"]\n for key in data:\n if key in override:\n data[key] = override[key]\n return Message(text=json.dumps(data, ensure_ascii=False))\n" + }, + "department": { + "_input_type": "DropdownInput", + "advanced": false, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Department", + "dynamic": false, + "external_options": {}, + "info": "Which team is this for", + "name": "department", + "options": [ + "Music", + "Youth", + "Seniors", + "Admin" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "Music" + }, + "form_values": { + "_input_type": "MessageTextInput", + "advanced": false, + "api_editable": false, + "display_name": "Form values (JSON envelope)", + "dynamic": false, + "info": "Submitted values as JSON; merged over the defaults", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "form_values", + "override_skip": false, + "placeholder": "", + "required": false, + "show": false, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "full_name": { + "_input_type": "StrInput", + "advanced": false, + "api_editable": false, + "display_name": "Full name", + "dynamic": false, + "info": "Your first and last name", + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "full_name", + "override_skip": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": " " + }, + "newsletter": { + "_input_type": "BoolInput", + "advanced": false, + "api_editable": false, + "display_name": "Newsletter", + "dynamic": false, + "info": "Send me the parish newsletter", + "list": false, + "list_add_label": "Add More", + "name": "newsletter", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": false + }, + "notes": { + "_input_type": "MultilineInput", + "advanced": false, + "ai_enabled": false, + "api_editable": false, + "copy_field": false, + "display_name": "Notes", + "dynamic": false, + "info": "Anything else we should know", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "multiline": true, + "name": "notes", + "override_skip": false, + "password": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "password": { + "_input_type": "SecretStrInput", + "advanced": false, + "api_editable": false, + "display_name": "Password", + "dynamic": false, + "info": "Optional access phrase", + "input_types": [], + "load_from_db": true, + "name": "password", + "override_skip": false, + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "satisfaction": { + "_input_type": "SliderInput", + "advanced": false, + "api_editable": false, + "display_name": "Satisfaction", + "dynamic": false, + "info": "How satisfied are you (1-5)", + "max_label": "", + "max_label_icon": "", + "min_label": "", + "min_label_icon": "", + "name": "satisfaction", + "override_skip": false, + "placeholder": "", + "range_spec": { + "max": 5.0, + "min": 1.0, + "step": 1.0, + "step_type": "float" + }, + "required": false, + "show": true, + "slider_buttons": false, + "slider_buttons_options": [], + "slider_input": false, + "title_case": false, + "tool_mode": false, + "track_in_telemetry": false, + "type": "slider", + "value": 3 + }, + "topics": { + "_input_type": "MultiselectInput", + "advanced": false, + "api_editable": false, + "combobox": false, + "display_name": "Topics", + "dynamic": false, + "info": "What are you interested in", + "list": true, + "list_add_label": "Add More", + "name": "topics", + "options": [ + "Worship", + "Outreach", + "Courses" + ], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": [] + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "CustomInputForm" + }, + "id": "CustomInputForm-intake", + "measured": { + "height": 200, + "width": 320 + }, + "position": { + "x": 400, + "y": 200 + }, + "type": "genericNode" + }, + { + "data": { + "id": "ChatOutput-collected", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Display a chat message in the Playground.", + "display_name": "Chat Output", + "documentation": "https://docs.langflow.org/chat-input-and-output", + "edited": false, + "field_order": [], + "frozen": false, + "icon": "MessagesSquare", + "legacy": false, + "metadata": {}, + "minimized": true, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Output Message", + "group_outputs": false, + "method": "message_response", + "name": "message", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "clean_data": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Basic Clean Data", + "dynamic": false, + "info": "Whether to clean data before converting to string.", + "list": false, + "list_add_label": "Add More", + "name": "clean_data", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + }, + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "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" + }, + "context_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Context ID", + "dynamic": false, + "info": "The context ID of the chat. Adds an extra layer to the local memory.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "context_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "data_template": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Data Template", + "dynamic": false, + "info": "Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "data_template", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "{text}" + }, + "input_value": { + "_input_type": "HandleInput", + "advanced": false, + "api_editable": false, + "display_name": "Inputs", + "dynamic": false, + "info": "Message to be passed as output.", + "input_types": [ + "Data", + "JSON", + "DataFrame", + "Table", + "Message" + ], + "list": false, + "list_add_label": "Add More", + "name": "input_value", + "override_skip": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "other", + "value": "" + }, + "sender": { + "_input_type": "DropdownInput", + "advanced": true, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Sender Type", + "dynamic": false, + "external_options": {}, + "info": "Type of sender.", + "name": "sender", + "options": [ + "Machine", + "User" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "Machine" + }, + "sender_name": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Sender Name", + "dynamic": false, + "info": "Name of the sender.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "sender_name", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "AI" + }, + "session_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Session ID", + "dynamic": false, + "info": "The session ID of the chat. If empty, the current session ID parameter will be used.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "session_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "list_add_label": "Add More", + "name": "should_store_message", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "ChatOutput" + }, + "id": "ChatOutput-collected", + "measured": { + "height": 200, + "width": 320 + }, + "position": { + "x": 800, + "y": 200 + }, + "type": "genericNode" + } + ], "viewport": { "x": 0, "y": 0, @@ -979,12 +979,8 @@ }, "description": "Structured intake via CustomInputForm (parsed into a web form).", "endpoint_name": null, - "icon": null, - "icon_bg_color": null, "id": "ec795b9d-f9d2-5730-b016-16e5f9b3bd3f", "is_component": false, - "locked": false, - "mcp_enabled": false, "name": "intake_form", "tags": [], "webhook": false diff --git a/backend/flows/new-flow.json b/backend/flows/new-flow.json new file mode 100644 index 0000000..7b3f462 --- /dev/null +++ b/backend/flows/new-flow.json @@ -0,0 +1,747 @@ +{ + "data": { + "edges": [ + { + "animated": false, + "className": "", + "data": { + "sourceHandle": { + "dataType": "CustomInputForm", + "id": "ext:kirche:CustomInputForm@extra-BhL6Q", + "name": "form_data", + "output_types": [ + "Message" + ] + }, + "targetHandle": { + "fieldName": "input_value", + "id": "ChatOutput-HkK3i", + "inputTypes": [ + "Data", + "JSON", + "DataFrame", + "Table", + "Message" + ], + "type": "other" + } + }, + "id": "xy-edge__ext:kirche:CustomInputForm@extra-BhL6Q{œdataTypeœ:œCustomInputFormœ,œidœ:œext:kirche:CustomInputForm@extra-BhL6Qœ,œnameœ:œform_dataœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-HkK3i{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-HkK3iœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", + "selected": false, + "source": "ext:kirche:CustomInputForm@extra-BhL6Q", + "sourceHandle": "{œdataTypeœ:œCustomInputFormœ,œidœ:œext:kirche:CustomInputForm@extra-BhL6Qœ,œnameœ:œform_dataœ,œoutput_typesœ:[œMessageœ]}", + "target": "ChatOutput-HkK3i", + "targetHandle": "{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-HkK3iœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}" + } + ], + "nodes": [ + { + "data": { + "id": "ext:kirche:CustomInputForm@extra-BhL6Q", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Collects structured form values into a single JSON message.", + "display_name": "CustomInput Form", + "documentation": "", + "edited": false, + "field_order": [ + "form_values", + "full_name", + "notes", + "password", + "age", + "amount", + "newsletter", + "department", + "topics", + "satisfaction", + "attachment" + ], + "frozen": false, + "icon": "ClipboardList", + "legacy": false, + "lf_version": "1.12.0", + "metadata": { + "code_hash": "6d186fa88d37", + "dependencies": { + "dependencies": [ + { + "name": "lfx", + "version": "1.12.0" + } + ], + "total_dependencies": 1 + }, + "module": "custom_components.custominput_form" + }, + "minimized": false, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Form Data", + "group_outputs": false, + "loop_types": null, + "method": "collect", + "name": "form_data", + "options": null, + "required_inputs": null, + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "age": { + "_input_type": "IntInput", + "advanced": false, + "api_editable": false, + "display_name": "Age", + "dynamic": false, + "info": "Your age in years", + "list": false, + "list_add_label": "Add More", + "name": "age", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "int", + "value": 18 + }, + "amount": { + "_input_type": "FloatInput", + "advanced": false, + "api_editable": false, + "display_name": "Amount", + "dynamic": false, + "info": "Amount in euros", + "list": false, + "list_add_label": "Add More", + "name": "amount", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "float", + "value": 0 + }, + "attachment": { + "_input_type": "FileInput", + "advanced": false, + "api_editable": false, + "display_name": "Attachment", + "dynamic": false, + "fileTypes": [ + "pdf", + "png", + "jpg" + ], + "file_path": "", + "info": "String reference only for now (no upload yet)", + "list": false, + "list_add_label": "Add More", + "name": "attachment", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "temp_file": false, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "file", + "value": "" + }, + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "value": "\"\"\"Structured form-input component for kirche-einfach flows.\n\nA flow that starts with :class:`CustomInputForm` declares the fields of its\nweb form. The API reads the exported node template\n(``template`` minus ``code``) and derives ``{json_schema, ui_schema}`` from\nthe ``lfx.io`` input definitions below — one JSON-Schema property per field,\nkeyed by ``name``. Field metadata maps as follows:\n\n* ``display_name`` → ``title``, ``info`` → ``description``,\n ``value`` → ``default``, ``options`` → ``enum``\n* ``required`` → required array, ``advanced`` → ``ui:collapsed`` hint\n* ``_input_type`` → JSON type + widget (see ``app/flows/input_schema.py``)\n\n``FileInput`` is a stub for now: it carries a string reference only, real\nupload comes later.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom typing import ClassVar\n\nfrom lfx.custom import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.io import (\n BoolInput,\n DropdownInput,\n FileInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n MultilineInput,\n MultiselectInput,\n Output,\n SecretStrInput,\n SliderInput,\n StrInput,\n)\nfrom lfx.schema.message import Message\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\nclass CustomInputForm(Component):\n display_name = \"CustomInput Form\"\n description = \"Collects structured form values into a single JSON message.\"\n icon = \"ClipboardList\"\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 StrInput(\n name=\"full_name\",\n display_name=\"Full name\",\n info=\"Your first and last name\",\n value=\"\",\n required=True,\n ),\n MultilineInput(\n name=\"notes\",\n display_name=\"Notes\",\n info=\"Anything else we should know\",\n value=\"\",\n ),\n SecretStrInput(\n name=\"password\",\n display_name=\"Password\",\n info=\"Optional access phrase\",\n value=\"\",\n ),\n IntInput(\n name=\"age\",\n display_name=\"Age\",\n info=\"Your age in years\",\n value=18,\n ),\n FloatInput(\n name=\"amount\",\n display_name=\"Amount\",\n info=\"Amount in euros\",\n value=0.0,\n ),\n BoolInput(\n name=\"newsletter\",\n display_name=\"Newsletter\",\n info=\"Send me the parish newsletter\",\n value=False,\n ),\n DropdownInput(\n name=\"department\",\n display_name=\"Department\",\n info=\"Which team is this for\",\n options=[\"Music\", \"Youth\", \"Seniors\", \"Admin\"],\n value=\"Music\",\n ),\n MultiselectInput(\n name=\"topics\",\n display_name=\"Topics\",\n info=\"What are you interested in\",\n options=[\"Worship\", \"Outreach\", \"Courses\"],\n value=[],\n ),\n SliderInput(\n name=\"satisfaction\",\n display_name=\"Satisfaction\",\n info=\"How satisfied are you (1-5)\",\n value=3,\n range_spec=RangeSpec(min=1, max=5, step=1),\n ),\n FileInput(\n name=\"attachment\",\n display_name=\"Attachment\",\n info=\"String reference only for now (no upload yet)\",\n file_types=[\"pdf\", \"png\", \"jpg\"],\n ),\n ]\n outputs: ClassVar[list] = [\n Output(display_name=\"Form Data\", name=\"form_data\", method=\"collect\"),\n ]\n\n def collect(self) -> Message:\n data = {\n \"full_name\": _text(self.full_name),\n \"notes\": _text(self.notes),\n \"password\": _text(self.password),\n \"age\": self.age,\n \"amount\": self.amount,\n \"newsletter\": self.newsletter,\n \"department\": _text(self.department),\n \"topics\": self.topics,\n \"satisfaction\": self.satisfaction,\n \"attachment\": _text(self.attachment),\n }\n # Submitted values (via form_values) win over the field defaults.\n # Accepts the raw dict or the run envelope {\"payload\": {...}}.\n try:\n override = json.loads(_text(self.form_values) or \"{}\")\n except json.JSONDecodeError:\n override = {}\n if isinstance(override, dict):\n if isinstance(override.get(\"payload\"), dict):\n override = override[\"payload\"]\n for key in data:\n if key in override:\n data[key] = override[key]\n return Message(text=json.dumps(data, ensure_ascii=False))\n" + }, + "department": { + "_input_type": "DropdownInput", + "advanced": false, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Department", + "dynamic": false, + "external_options": {}, + "info": "Which team is this for", + "name": "department", + "options": [ + "Music", + "Youth", + "Seniors", + "Admin" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "Youth" + }, + "form_values": { + "_input_type": "MessageTextInput", + "advanced": false, + "api_editable": false, + "display_name": "Form values (JSON envelope)", + "dynamic": false, + "info": "Submitted values as JSON; merged over the defaults", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "form_values", + "override_skip": false, + "placeholder": "", + "required": false, + "show": false, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "full_name": { + "_input_type": "StrInput", + "advanced": false, + "api_editable": false, + "display_name": "Full name", + "dynamic": false, + "info": "Your first and last name", + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "full_name", + "override_skip": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "Hi there" + }, + "newsletter": { + "_input_type": "BoolInput", + "advanced": false, + "api_editable": false, + "display_name": "Newsletter", + "dynamic": false, + "info": "Send me the parish newsletter", + "list": false, + "list_add_label": "Add More", + "name": "newsletter", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": false + }, + "notes": { + "_input_type": "MultilineInput", + "advanced": false, + "ai_enabled": false, + "api_editable": false, + "copy_field": false, + "display_name": "Notes", + "dynamic": false, + "info": "Anything else we should know", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "multiline": true, + "name": "notes", + "override_skip": false, + "password": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "Blabla" + }, + "password": { + "_input_type": "SecretStrInput", + "advanced": false, + "api_editable": false, + "display_name": "Password", + "dynamic": false, + "info": "Optional access phrase", + "input_types": [], + "load_from_db": false, + "name": "password", + "override_skip": false, + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "track_in_telemetry": false, + "type": "str", + "value": "hidden" + }, + "satisfaction": { + "_input_type": "SliderInput", + "advanced": false, + "api_editable": false, + "display_name": "Satisfaction", + "dynamic": false, + "info": "How satisfied are you (1-5)", + "max_label": "", + "max_label_icon": "", + "min_label": "", + "min_label_icon": "", + "name": "satisfaction", + "override_skip": false, + "placeholder": "", + "range_spec": { + "max": 5, + "min": 1, + "step": 1, + "step_type": "float" + }, + "required": false, + "show": true, + "slider_buttons": false, + "slider_buttons_options": [], + "slider_input": false, + "title_case": false, + "tool_mode": false, + "track_in_telemetry": false, + "type": "slider", + "value": 3 + }, + "topics": { + "_input_type": "MultiselectInput", + "advanced": false, + "api_editable": false, + "combobox": false, + "display_name": "Topics", + "dynamic": false, + "info": "What are you interested in", + "list": true, + "list_add_label": "Add More", + "name": "topics", + "options": [ + "Worship", + "Outreach", + "Courses" + ], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": [ + "Outreach", + "Courses" + ] + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "CustomInputForm" + }, + "dragging": false, + "id": "ext:kirche:CustomInputForm@extra-BhL6Q", + "measured": { + "height": 933, + "width": 320 + }, + "position": { + "x": -2869.215421770588, + "y": -259.29504008742293 + }, + "selected": true, + "type": "genericNode" + }, + { + "data": { + "id": "ChatOutput-HkK3i", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Display a chat message in the Playground.", + "display_name": "Chat Output", + "documentation": "https://docs.langflow.org/chat-input-and-output", + "edited": false, + "field_order": [ + "input_value", + "should_store_message", + "sender", + "sender_name", + "session_id", + "context_id", + "data_template", + "clean_data" + ], + "frozen": false, + "icon": "MessagesSquare", + "legacy": false, + "lf_version": "1.12.0", + "metadata": { + "code_hash": "84009527d08c", + "dependencies": { + "dependencies": [ + { + "name": "orjson", + "version": "3.11.9" + }, + { + "name": "fastapi", + "version": "0.139.2" + }, + { + "name": "lfx", + "version": null + } + ], + "total_dependencies": 3 + }, + "module": "lfx.components.input_output.chat_output.ChatOutput" + }, + "minimized": true, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Output Message", + "group_outputs": false, + "method": "message_response", + "name": "message", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "clean_data": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Basic Clean Data", + "dynamic": false, + "info": "Whether to clean data before converting to string.", + "list": false, + "list_add_label": "Add More", + "name": "clean_data", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + }, + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "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" + }, + "context_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Context ID", + "dynamic": false, + "info": "The context ID of the chat. Adds an extra layer to the local memory.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "context_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "data_template": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Data Template", + "dynamic": false, + "info": "Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "data_template", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "{text}" + }, + "input_value": { + "_input_type": "HandleInput", + "advanced": false, + "api_editable": false, + "display_name": "Inputs", + "dynamic": false, + "info": "Message to be passed as output.", + "input_types": [ + "Data", + "JSON", + "DataFrame", + "Table", + "Message" + ], + "list": false, + "list_add_label": "Add More", + "name": "input_value", + "override_skip": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "other", + "value": "" + }, + "sender": { + "_input_type": "DropdownInput", + "advanced": true, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Sender Type", + "dynamic": false, + "external_options": {}, + "info": "Type of sender.", + "name": "sender", + "options": [ + "Machine", + "User" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "Machine" + }, + "sender_name": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Sender Name", + "dynamic": false, + "info": "Name of the sender.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "sender_name", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "AI" + }, + "session_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Session ID", + "dynamic": false, + "info": "The session ID of the chat. If empty, the current session ID parameter will be used.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "session_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "list_add_label": "Add More", + "name": "should_store_message", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "ChatOutput" + }, + "id": "ChatOutput-HkK3i", + "measured": { + "height": 168, + "width": 320 + }, + "position": { + "x": -2181.727435009515, + "y": 144.80068578973714 + }, + "selected": false, + "type": "genericNode" + } + ], + "viewport": { + "x": 1755.3131372405762, + "y": 167.2405536194492, + "zoom": 0.5755627009646302 + } + }, + "description": "The Pinnacle of Prompt Generation.", + "endpoint_name": null, + "id": "fd440bee-f669-44a2-aa12-6af3bc299731", + "is_component": false, + "name": "New Flow", + "tags": [], + "webhook": false +} diff --git a/backend/flows/simple_rule.json b/backend/flows/simple_rule.json index aba77ad..4d96d4f 100644 --- a/backend/flows/simple_rule.json +++ b/backend/flows/simple_rule.json @@ -1,1256 +1,5 @@ { "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": { - "_type": "Component", - "code": { - "type": "code", - "required": true, - "placeholder": "", - "list": false, - "show": true, - "multiline": true, - "value": "from lfx.custom import Component\nfrom lfx.io import MessageTextInput, Output\nfrom lfx.schema.message import Message\nimport json\n\n\nclass RuleCheckComponent(Component):\n display_name = \"Rule Check\"\n description = \"Bewertet Betrag gegen Schwelle aus JSON-Eingabe.\"\n\n inputs = [\n MessageTextInput(name=\"payload\", display_name=\"Payload\"),\n ]\n outputs = [\n Output(display_name=\"Verdict\", name=\"verdict\", method=\"check\"),\n ]\n\n def _text(self, 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 def check(self) -> Message:\n try:\n data = json.loads(self._text(self.payload) or \"{}\")\n except Exception:\n data = {}\n if not isinstance(data, dict):\n data = {}\n # Die API ruft Flows immer mit Huelle auf\n # {\"payload\": {...}, \"run_id\": ..., ...}; rohe Payloads gehen auch.\n if isinstance(data.get(\"payload\"), dict):\n data = data[\"payload\"]\n try:\n betrag = float(data.get(\"betrag\", 0) or 0)\n except (TypeError, ValueError):\n betrag = 0.0\n try:\n schwelle = float(data.get(\"schwelle\", 200000) or 200000)\n except (TypeError, ValueError):\n schwelle = 200000.0\n return Message(text=\"PRUEFUNG\" if betrag > schwelle else \"OK\")\n", - "fileTypes": [], - "file_path": "", - "password": false, - "name": "code", - "advanced": true, - "api_editable": false, - "dynamic": true, - "info": "", - "load_from_db": false, - "title_case": false - }, - "payload": { - "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": "payload", - "value": "", - "display_name": "Payload", - "advanced": false, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageTextInput" - } - }, - "description": "Bewertet Betrag gegen Schwelle aus JSON-Eingabe.", - "base_classes": [ - "Message" - ], - "display_name": "Rule Check", - "documentation": "", - "minimized": false, - "custom_fields": {}, - "output_types": [], - "pinned": false, - "conditional_paths": [], - "frozen": false, - "outputs": [ - { - "types": [ - "Message" - ], - "selected": "Message", - "name": "verdict", - "display_name": "Verdict", - "method": "check", - "value": "__UNDEFINED__", - "cache": true, - "allows_loop": false, - "group_outputs": false, - "tool_mode": true - } - ], - "field_order": [ - "payload" - ], - "beta": false, - "legacy": false, - "edited": false, - "metadata": { - "module": "custom_components.rule_check", - "code_hash": "0b9f8a009fcf", - "dependencies": { - "total_dependencies": 1, - "dependencies": [ - { - "name": "lfx", - "version": "1.12.0" - } - ] - } - }, - "tool_mode": false - }, - "type": "CustomComponent", - "id": "CustomComponent-rule", - "showNode": true - }, - "id": "CustomComponent-rule", - "measured": { - "width": 320, - "height": 200 - }, - "position": { - "x": 400, - "y": 200 - }, - "type": "genericNode" - }, - { - "data": { - "node": { - "template": { - "_type": "Component", - "case_sensitive": { - "tool_mode": false, - "trace_as_metadata": true, - "list": false, - "list_add_label": "Add More", - "override_skip": false, - "required": false, - "placeholder": "", - "show": true, - "name": "case_sensitive", - "value": true, - "display_name": "Case Sensitive", - "advanced": true, - "api_editable": false, - "dynamic": false, - "info": "If true, the comparison will be case sensitive.", - "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": "import re\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageInput, MessageTextInput, Output\nfrom lfx.schema.message import Message\n\n\nclass ConditionalRouterComponent(Component):\n display_name = \"If-Else\"\n description = \"Routes an input message to a corresponding output based on text comparison.\"\n documentation: str = \"https://docs.langflow.org/if-else\"\n icon = \"split\"\n name = \"ConditionalRouter\"\n\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self.__iteration_updated = False\n\n inputs = [\n MessageTextInput(\n name=\"input_text\",\n display_name=\"Text Input\",\n info=\"The primary text input for the operation.\",\n required=True,\n ),\n DropdownInput(\n name=\"operator\",\n display_name=\"Operator\",\n options=[\n \"equals\",\n \"not equals\",\n \"contains\",\n \"starts with\",\n \"ends with\",\n \"regex\",\n \"less than\",\n \"less than or equal\",\n \"greater than\",\n \"greater than or equal\",\n ],\n info=\"The operator to apply for comparing the texts.\",\n value=\"equals\",\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"match_text\",\n display_name=\"Match Text\",\n info=\"The text input to compare against.\",\n required=True,\n ),\n BoolInput(\n name=\"case_sensitive\",\n display_name=\"Case Sensitive\",\n info=\"If true, the comparison will be case sensitive.\",\n value=True,\n advanced=True,\n ),\n MessageInput(\n name=\"true_case_message\",\n display_name=\"Case True\",\n info=\"The message to pass if the condition is True.\",\n advanced=True,\n ),\n MessageInput(\n name=\"false_case_message\",\n display_name=\"Case False\",\n info=\"The message to pass if the condition is False.\",\n advanced=True,\n ),\n IntInput(\n name=\"max_iterations\",\n display_name=\"Max Iterations\",\n info=\"The maximum number of iterations for the conditional router.\",\n value=10,\n advanced=True,\n ),\n DropdownInput(\n name=\"default_route\",\n display_name=\"Default Route\",\n options=[\"true_result\", \"false_result\"],\n info=\"The default route to take when max iterations are reached.\",\n value=\"false_result\",\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"True\", name=\"true_result\", method=\"true_response\", group_outputs=True),\n Output(display_name=\"False\", name=\"false_result\", method=\"false_response\", group_outputs=True),\n ]\n\n def _pre_run_setup(self):\n self.__iteration_updated = False\n\n def evaluate_condition(self, input_text: str, match_text: str, operator: str, *, case_sensitive: bool) -> bool:\n if not case_sensitive and operator != \"regex\":\n input_text = input_text.lower()\n match_text = match_text.lower()\n\n if operator == \"equals\":\n return input_text == match_text\n if operator == \"not equals\":\n return input_text != match_text\n if operator == \"contains\":\n return match_text in input_text\n if operator == \"starts with\":\n return input_text.startswith(match_text)\n if operator == \"ends with\":\n return input_text.endswith(match_text)\n if operator == \"regex\":\n try:\n return bool(re.match(match_text, input_text))\n except re.error:\n return False # Return False if the regex is invalid\n if operator in [\"less than\", \"less than or equal\", \"greater than\", \"greater than or equal\"]:\n try:\n input_num = float(input_text)\n match_num = float(match_text)\n if operator == \"less than\":\n return input_num < match_num\n if operator == \"less than or equal\":\n return input_num <= match_num\n if operator == \"greater than\":\n return input_num > match_num\n if operator == \"greater than or equal\":\n return input_num >= match_num\n except ValueError:\n return False # Invalid number format for comparison\n return False\n\n def iterate_and_stop_once(self, route_to_stop: str):\n \"\"\"Handles cycle iteration counting and branch exclusion.\n\n Uses two complementary mechanisms:\n 1. stop() - ACTIVE/INACTIVE state for cycle management (gets reset each iteration)\n 2. exclude_branch_conditionally() - Persistent exclusion for conditional routing\n\n When max_iterations is reached, breaks the cycle by allowing the default_route to execute.\n \"\"\"\n if not self.__iteration_updated:\n self.update_ctx({f\"{self._id}_iteration\": self.ctx.get(f\"{self._id}_iteration\", 0) + 1})\n self.__iteration_updated = True\n current_iteration = self.ctx.get(f\"{self._id}_iteration\", 0)\n\n # Check if max iterations reached and we're trying to stop the default route\n if current_iteration >= self.max_iterations and route_to_stop == self.default_route:\n # Clear ALL conditional exclusions to allow default route to execute\n if self._id in self.graph.conditional_exclusion_sources:\n previous_exclusions = self.graph.conditional_exclusion_sources[self._id]\n self.graph.conditionally_excluded_vertices -= previous_exclusions\n del self.graph.conditional_exclusion_sources[self._id]\n\n # Switch which route to stop - stop the NON-default route to break the cycle\n route_to_stop = \"true_result\" if route_to_stop == \"false_result\" else \"false_result\"\n\n # Call stop to break the cycle\n self.stop(route_to_stop)\n # Don't apply conditional exclusion when breaking cycle\n return\n\n # Normal case: Use BOTH mechanisms\n # 1. stop() for cycle management (marks INACTIVE, updates run manager, gets reset)\n self.stop(route_to_stop)\n\n # 2. Conditional exclusion for persistent routing (doesn't get reset except by this router)\n self.graph.exclude_branch_conditionally(self._id, output_name=route_to_stop)\n\n def _resolve_case_message(self, case_message: Message | str | None) -> Message:\n \"\"\"Return the override case message, falling back to the input text when it is blank.\n\n ``Case True`` and ``Case False`` are optional overrides. When left blank the\n ``MessageInput`` resolves to an empty ``Message``, so the original ``Text Input``\n is routed through instead of an empty message.\n\n A ``Message`` is only treated as blank when it carries no payload at all. An\n override with empty text but meaningful content (files or content blocks) is\n preserved as-is so that payload is not dropped.\n\n Blankness is judged by payload only (``text``, ``files``, ``content_blocks``).\n A ``Message`` carrying just metadata (``sender``, ``properties``, ``error`` ...)\n and no payload is intentionally treated as blank: every empty ``Message`` also\n defaults ``timestamp`` / ``category`` / ``properties``, so keying on metadata\n would defeat the blank-field fallback. With no payload there is nothing to\n route, so the ``Text Input`` is used.\n\n ``case_message`` is typed defensively as ``Message | str | None``. ``MessageInput``\n wraps user input into a ``Message`` on the validated graph path, but a raw ``str``\n can still arrive via direct/programmatic assignment (``.set(...)`` stores the raw\n value), so the ``str`` branch is retained rather than dropping such an override.\n \"\"\"\n if isinstance(case_message, Message):\n if case_message.text or case_message.files or case_message.content_blocks:\n return case_message\n return Message(text=self.input_text)\n if isinstance(case_message, str) and case_message:\n return Message(text=case_message)\n return Message(text=self.input_text)\n\n def true_response(self) -> Message:\n result = self.evaluate_condition(\n self.input_text, self.match_text, self.operator, case_sensitive=self.case_sensitive\n )\n\n # Check if we should force output due to max_iterations on default route\n current_iteration = self.ctx.get(f\"{self._id}_iteration\", 0)\n force_output = current_iteration >= self.max_iterations and self.default_route == \"true_result\"\n\n if result or force_output:\n true_message = self._resolve_case_message(self.true_case_message)\n self.status = true_message\n if not force_output: # Only stop the other branch if not forcing due to max iterations\n self.iterate_and_stop_once(\"false_result\")\n return true_message\n self.iterate_and_stop_once(\"true_result\")\n return Message(text=\"\")\n\n def false_response(self) -> Message:\n result = self.evaluate_condition(\n self.input_text, self.match_text, self.operator, case_sensitive=self.case_sensitive\n )\n\n if not result:\n false_message = self._resolve_case_message(self.false_case_message)\n self.status = false_message\n self.iterate_and_stop_once(\"true_result\")\n return false_message\n\n self.iterate_and_stop_once(\"false_result\")\n return Message(text=\"\")\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:\n if field_name == \"operator\":\n if field_value == \"regex\":\n build_config.pop(\"case_sensitive\", None)\n elif \"case_sensitive\" not in build_config:\n case_sensitive_input = next(\n (input_field for input_field in self.inputs if input_field.name == \"case_sensitive\"), None\n )\n if case_sensitive_input:\n build_config[\"case_sensitive\"] = case_sensitive_input.to_dict()\n return build_config\n", - "fileTypes": [], - "file_path": "", - "password": false, - "name": "code", - "advanced": true, - "api_editable": false, - "dynamic": true, - "info": "", - "load_from_db": false, - "title_case": false - }, - "default_route": { - "tool_mode": false, - "trace_as_metadata": true, - "options": [ - "true_result", - "false_result" - ], - "options_metadata": [], - "combobox": false, - "dialog_inputs": {}, - "toggle": false, - "override_skip": false, - "required": false, - "placeholder": "", - "show": true, - "name": "default_route", - "value": "false_result", - "display_name": "Default Route", - "advanced": true, - "api_editable": false, - "dynamic": false, - "info": "The default route to take when max iterations are reached.", - "title_case": false, - "track_in_telemetry": true, - "external_options": {}, - "type": "str", - "_input_type": "DropdownInput" - }, - "false_case_message": { - "trace_as_input": true, - "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": "false_case_message", - "value": "Standardfall: automatisch freigegeben", - "display_name": "Case False", - "advanced": true, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "The message to pass if the condition is False.", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageInput" - }, - "input_text": { - "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": true, - "placeholder": "", - "show": true, - "name": "input_text", - "value": "", - "display_name": "Text Input", - "advanced": false, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "The primary text input for the operation.", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageTextInput" - }, - "match_text": { - "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": true, - "placeholder": "", - "show": true, - "name": "match_text", - "value": "PRUEFUNG", - "display_name": "Match Text", - "advanced": false, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "The text input to compare against.", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageTextInput" - }, - "max_iterations": { - "tool_mode": false, - "trace_as_metadata": true, - "list": false, - "list_add_label": "Add More", - "override_skip": false, - "required": false, - "placeholder": "", - "show": true, - "name": "max_iterations", - "value": 10, - "display_name": "Max Iterations", - "advanced": true, - "api_editable": false, - "dynamic": false, - "info": "The maximum number of iterations for the conditional router.", - "title_case": false, - "track_in_telemetry": true, - "type": "int", - "_input_type": "IntInput" - }, - "operator": { - "tool_mode": false, - "trace_as_metadata": true, - "options": [ - "equals", - "not equals", - "contains", - "starts with", - "ends with", - "regex", - "less than", - "less than or equal", - "greater than", - "greater than or equal" - ], - "options_metadata": [], - "combobox": false, - "dialog_inputs": {}, - "toggle": false, - "override_skip": false, - "required": false, - "placeholder": "", - "show": true, - "name": "operator", - "value": "equals", - "display_name": "Operator", - "advanced": false, - "api_editable": false, - "dynamic": false, - "info": "The operator to apply for comparing the texts.", - "real_time_refresh": true, - "title_case": false, - "track_in_telemetry": true, - "external_options": {}, - "type": "str", - "_input_type": "DropdownInput" - }, - "true_case_message": { - "trace_as_input": true, - "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": "true_case_message", - "value": "Sonderfall: menschliche Pruefung erforderlich", - "display_name": "Case True", - "advanced": true, - "api_editable": false, - "input_types": [ - "Message" - ], - "dynamic": false, - "info": "The message to pass if the condition is True.", - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "_input_type": "MessageInput" - } - }, - "description": "Routes an input message to a corresponding output based on text comparison.", - "icon": "split", - "base_classes": [ - "Message" - ], - "display_name": "If-Else", - "documentation": "https://docs.langflow.org/if-else", - "minimized": false, - "custom_fields": {}, - "output_types": [], - "pinned": false, - "conditional_paths": [], - "frozen": false, - "outputs": [ - { - "types": [ - "Message" - ], - "selected": "Message", - "name": "true_result", - "display_name": "True", - "method": "true_response", - "value": "__UNDEFINED__", - "cache": true, - "allows_loop": false, - "group_outputs": true, - "tool_mode": true - }, - { - "types": [ - "Message" - ], - "selected": "Message", - "name": "false_result", - "display_name": "False", - "method": "false_response", - "value": "__UNDEFINED__", - "cache": true, - "allows_loop": false, - "group_outputs": true, - "tool_mode": true - } - ], - "field_order": [], - "beta": false, - "legacy": false, - "edited": false, - "metadata": {}, - "tool_mode": false - }, - "type": "ConditionalRouter", - "id": "ConditionalRouter-router", - "showNode": true - }, - "id": "ConditionalRouter-router", - "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-ok", - "showNode": true - }, - "id": "ChatOutput-ok", - "measured": { - "width": 320, - "height": 200 - }, - "position": { - "x": 1200, - "y": 60 - }, - "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-sonder", - "showNode": true - }, - "id": "ChatOutput-sonder", - "measured": { - "width": 320, - "height": 200 - }, - "position": { - "x": 1200, - "y": 340 - }, - "type": "genericNode" - } - ], "edges": [ { "animated": false, @@ -1373,6 +122,1257 @@ "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-sonderœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" } ], + "nodes": [ + { + "data": { + "id": "ChatInput-entry", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Get chat inputs from the Playground.", + "display_name": "Chat Input", + "documentation": "https://docs.langflow.org/chat-input-and-output", + "edited": false, + "field_order": [], + "frozen": false, + "icon": "MessagesSquare", + "legacy": false, + "metadata": {}, + "minimized": true, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Chat Message", + "group_outputs": false, + "method": "message_response", + "name": "message", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "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" + }, + "context_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Context ID", + "dynamic": false, + "info": "The context ID of the chat. Adds an extra layer to the local memory.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "context_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "files": { + "_input_type": "FileInput", + "advanced": true, + "api_editable": false, + "display_name": "Files", + "dynamic": false, + "fileTypes": [ + "csv", + "json", + "pdf", + "txt", + "md", + "mdx", + "yaml", + "yml", + "xml", + "html", + "htm", + "docx", + "py", + "sh", + "sql", + "js", + "ts", + "tsx", + "jpg", + "jpeg", + "png", + "bmp", + "image" + ], + "file_path": "", + "info": "Files to be sent with the message.", + "list": true, + "list_add_label": "Add More", + "name": "files", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "temp_file": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "file", + "value": "" + }, + "input_value": { + "_input_type": "MultilineInput", + "advanced": false, + "ai_enabled": false, + "api_editable": false, + "copy_field": false, + "display_name": "Input Text", + "dynamic": false, + "info": "Message to be passed as input.", + "input_types": [], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "multiline": true, + "name": "input_value", + "override_skip": false, + "password": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "sender": { + "_input_type": "DropdownInput", + "advanced": true, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Sender Type", + "dynamic": false, + "external_options": {}, + "info": "Type of sender.", + "name": "sender", + "options": [ + "Machine", + "User" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "User" + }, + "sender_name": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Sender Name", + "dynamic": false, + "info": "Name of the sender.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "sender_name", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "User" + }, + "session_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Session ID", + "dynamic": false, + "info": "The session ID of the chat. If empty, the current session ID parameter will be used.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "session_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "list_add_label": "Add More", + "name": "should_store_message", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "ChatInput" + }, + "id": "ChatInput-entry", + "measured": { + "height": 200, + "width": 320 + }, + "position": { + "x": 0, + "y": 200 + }, + "type": "genericNode" + }, + { + "data": { + "id": "CustomComponent-rule", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Bewertet Betrag gegen Schwelle aus JSON-Eingabe.", + "display_name": "Rule Check", + "documentation": "", + "edited": false, + "field_order": [ + "payload" + ], + "frozen": false, + "legacy": false, + "metadata": { + "code_hash": "0b9f8a009fcf", + "dependencies": { + "dependencies": [ + { + "name": "lfx", + "version": "1.12.0" + } + ], + "total_dependencies": 1 + }, + "module": "custom_components.rule_check" + }, + "minimized": false, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Verdict", + "group_outputs": false, + "method": "check", + "name": "verdict", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "value": "from lfx.custom import Component\nfrom lfx.io import MessageTextInput, Output\nfrom lfx.schema.message import Message\nimport json\n\n\nclass RuleCheckComponent(Component):\n display_name = \"Rule Check\"\n description = \"Bewertet Betrag gegen Schwelle aus JSON-Eingabe.\"\n\n inputs = [\n MessageTextInput(name=\"payload\", display_name=\"Payload\"),\n ]\n outputs = [\n Output(display_name=\"Verdict\", name=\"verdict\", method=\"check\"),\n ]\n\n def _text(self, 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 def check(self) -> Message:\n try:\n data = json.loads(self._text(self.payload) or \"{}\")\n except Exception:\n data = {}\n if not isinstance(data, dict):\n data = {}\n # Die API ruft Flows immer mit Huelle auf\n # {\"payload\": {...}, \"run_id\": ..., ...}; rohe Payloads gehen auch.\n if isinstance(data.get(\"payload\"), dict):\n data = data[\"payload\"]\n try:\n betrag = float(data.get(\"betrag\", 0) or 0)\n except (TypeError, ValueError):\n betrag = 0.0\n try:\n schwelle = float(data.get(\"schwelle\", 200000) or 200000)\n except (TypeError, ValueError):\n schwelle = 200000.0\n return Message(text=\"PRUEFUNG\" if betrag > schwelle else \"OK\")\n" + }, + "payload": { + "_input_type": "MessageTextInput", + "advanced": false, + "api_editable": false, + "display_name": "Payload", + "dynamic": false, + "info": "", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "payload", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "CustomComponent" + }, + "id": "CustomComponent-rule", + "measured": { + "height": 200, + "width": 320 + }, + "position": { + "x": 400, + "y": 200 + }, + "type": "genericNode" + }, + { + "data": { + "id": "ConditionalRouter-router", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Routes an input message to a corresponding output based on text comparison.", + "display_name": "If-Else", + "documentation": "https://docs.langflow.org/if-else", + "edited": false, + "field_order": [], + "frozen": false, + "icon": "split", + "legacy": false, + "metadata": {}, + "minimized": false, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "True", + "group_outputs": true, + "method": "true_response", + "name": "true_result", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + }, + { + "allows_loop": false, + "cache": true, + "display_name": "False", + "group_outputs": true, + "method": "false_response", + "name": "false_result", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "case_sensitive": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Case Sensitive", + "dynamic": false, + "info": "If true, the comparison will be case sensitive.", + "list": false, + "list_add_label": "Add More", + "name": "case_sensitive", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + }, + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "value": "import re\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageInput, MessageTextInput, Output\nfrom lfx.schema.message import Message\n\n\nclass ConditionalRouterComponent(Component):\n display_name = \"If-Else\"\n description = \"Routes an input message to a corresponding output based on text comparison.\"\n documentation: str = \"https://docs.langflow.org/if-else\"\n icon = \"split\"\n name = \"ConditionalRouter\"\n\n def __init__(self, *args, **kwargs):\n super().__init__(*args, **kwargs)\n self.__iteration_updated = False\n\n inputs = [\n MessageTextInput(\n name=\"input_text\",\n display_name=\"Text Input\",\n info=\"The primary text input for the operation.\",\n required=True,\n ),\n DropdownInput(\n name=\"operator\",\n display_name=\"Operator\",\n options=[\n \"equals\",\n \"not equals\",\n \"contains\",\n \"starts with\",\n \"ends with\",\n \"regex\",\n \"less than\",\n \"less than or equal\",\n \"greater than\",\n \"greater than or equal\",\n ],\n info=\"The operator to apply for comparing the texts.\",\n value=\"equals\",\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"match_text\",\n display_name=\"Match Text\",\n info=\"The text input to compare against.\",\n required=True,\n ),\n BoolInput(\n name=\"case_sensitive\",\n display_name=\"Case Sensitive\",\n info=\"If true, the comparison will be case sensitive.\",\n value=True,\n advanced=True,\n ),\n MessageInput(\n name=\"true_case_message\",\n display_name=\"Case True\",\n info=\"The message to pass if the condition is True.\",\n advanced=True,\n ),\n MessageInput(\n name=\"false_case_message\",\n display_name=\"Case False\",\n info=\"The message to pass if the condition is False.\",\n advanced=True,\n ),\n IntInput(\n name=\"max_iterations\",\n display_name=\"Max Iterations\",\n info=\"The maximum number of iterations for the conditional router.\",\n value=10,\n advanced=True,\n ),\n DropdownInput(\n name=\"default_route\",\n display_name=\"Default Route\",\n options=[\"true_result\", \"false_result\"],\n info=\"The default route to take when max iterations are reached.\",\n value=\"false_result\",\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"True\", name=\"true_result\", method=\"true_response\", group_outputs=True),\n Output(display_name=\"False\", name=\"false_result\", method=\"false_response\", group_outputs=True),\n ]\n\n def _pre_run_setup(self):\n self.__iteration_updated = False\n\n def evaluate_condition(self, input_text: str, match_text: str, operator: str, *, case_sensitive: bool) -> bool:\n if not case_sensitive and operator != \"regex\":\n input_text = input_text.lower()\n match_text = match_text.lower()\n\n if operator == \"equals\":\n return input_text == match_text\n if operator == \"not equals\":\n return input_text != match_text\n if operator == \"contains\":\n return match_text in input_text\n if operator == \"starts with\":\n return input_text.startswith(match_text)\n if operator == \"ends with\":\n return input_text.endswith(match_text)\n if operator == \"regex\":\n try:\n return bool(re.match(match_text, input_text))\n except re.error:\n return False # Return False if the regex is invalid\n if operator in [\"less than\", \"less than or equal\", \"greater than\", \"greater than or equal\"]:\n try:\n input_num = float(input_text)\n match_num = float(match_text)\n if operator == \"less than\":\n return input_num < match_num\n if operator == \"less than or equal\":\n return input_num <= match_num\n if operator == \"greater than\":\n return input_num > match_num\n if operator == \"greater than or equal\":\n return input_num >= match_num\n except ValueError:\n return False # Invalid number format for comparison\n return False\n\n def iterate_and_stop_once(self, route_to_stop: str):\n \"\"\"Handles cycle iteration counting and branch exclusion.\n\n Uses two complementary mechanisms:\n 1. stop() - ACTIVE/INACTIVE state for cycle management (gets reset each iteration)\n 2. exclude_branch_conditionally() - Persistent exclusion for conditional routing\n\n When max_iterations is reached, breaks the cycle by allowing the default_route to execute.\n \"\"\"\n if not self.__iteration_updated:\n self.update_ctx({f\"{self._id}_iteration\": self.ctx.get(f\"{self._id}_iteration\", 0) + 1})\n self.__iteration_updated = True\n current_iteration = self.ctx.get(f\"{self._id}_iteration\", 0)\n\n # Check if max iterations reached and we're trying to stop the default route\n if current_iteration >= self.max_iterations and route_to_stop == self.default_route:\n # Clear ALL conditional exclusions to allow default route to execute\n if self._id in self.graph.conditional_exclusion_sources:\n previous_exclusions = self.graph.conditional_exclusion_sources[self._id]\n self.graph.conditionally_excluded_vertices -= previous_exclusions\n del self.graph.conditional_exclusion_sources[self._id]\n\n # Switch which route to stop - stop the NON-default route to break the cycle\n route_to_stop = \"true_result\" if route_to_stop == \"false_result\" else \"false_result\"\n\n # Call stop to break the cycle\n self.stop(route_to_stop)\n # Don't apply conditional exclusion when breaking cycle\n return\n\n # Normal case: Use BOTH mechanisms\n # 1. stop() for cycle management (marks INACTIVE, updates run manager, gets reset)\n self.stop(route_to_stop)\n\n # 2. Conditional exclusion for persistent routing (doesn't get reset except by this router)\n self.graph.exclude_branch_conditionally(self._id, output_name=route_to_stop)\n\n def _resolve_case_message(self, case_message: Message | str | None) -> Message:\n \"\"\"Return the override case message, falling back to the input text when it is blank.\n\n ``Case True`` and ``Case False`` are optional overrides. When left blank the\n ``MessageInput`` resolves to an empty ``Message``, so the original ``Text Input``\n is routed through instead of an empty message.\n\n A ``Message`` is only treated as blank when it carries no payload at all. An\n override with empty text but meaningful content (files or content blocks) is\n preserved as-is so that payload is not dropped.\n\n Blankness is judged by payload only (``text``, ``files``, ``content_blocks``).\n A ``Message`` carrying just metadata (``sender``, ``properties``, ``error`` ...)\n and no payload is intentionally treated as blank: every empty ``Message`` also\n defaults ``timestamp`` / ``category`` / ``properties``, so keying on metadata\n would defeat the blank-field fallback. With no payload there is nothing to\n route, so the ``Text Input`` is used.\n\n ``case_message`` is typed defensively as ``Message | str | None``. ``MessageInput``\n wraps user input into a ``Message`` on the validated graph path, but a raw ``str``\n can still arrive via direct/programmatic assignment (``.set(...)`` stores the raw\n value), so the ``str`` branch is retained rather than dropping such an override.\n \"\"\"\n if isinstance(case_message, Message):\n if case_message.text or case_message.files or case_message.content_blocks:\n return case_message\n return Message(text=self.input_text)\n if isinstance(case_message, str) and case_message:\n return Message(text=case_message)\n return Message(text=self.input_text)\n\n def true_response(self) -> Message:\n result = self.evaluate_condition(\n self.input_text, self.match_text, self.operator, case_sensitive=self.case_sensitive\n )\n\n # Check if we should force output due to max_iterations on default route\n current_iteration = self.ctx.get(f\"{self._id}_iteration\", 0)\n force_output = current_iteration >= self.max_iterations and self.default_route == \"true_result\"\n\n if result or force_output:\n true_message = self._resolve_case_message(self.true_case_message)\n self.status = true_message\n if not force_output: # Only stop the other branch if not forcing due to max iterations\n self.iterate_and_stop_once(\"false_result\")\n return true_message\n self.iterate_and_stop_once(\"true_result\")\n return Message(text=\"\")\n\n def false_response(self) -> Message:\n result = self.evaluate_condition(\n self.input_text, self.match_text, self.operator, case_sensitive=self.case_sensitive\n )\n\n if not result:\n false_message = self._resolve_case_message(self.false_case_message)\n self.status = false_message\n self.iterate_and_stop_once(\"true_result\")\n return false_message\n\n self.iterate_and_stop_once(\"false_result\")\n return Message(text=\"\")\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:\n if field_name == \"operator\":\n if field_value == \"regex\":\n build_config.pop(\"case_sensitive\", None)\n elif \"case_sensitive\" not in build_config:\n case_sensitive_input = next(\n (input_field for input_field in self.inputs if input_field.name == \"case_sensitive\"), None\n )\n if case_sensitive_input:\n build_config[\"case_sensitive\"] = case_sensitive_input.to_dict()\n return build_config\n" + }, + "default_route": { + "_input_type": "DropdownInput", + "advanced": true, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Default Route", + "dynamic": false, + "external_options": {}, + "info": "The default route to take when max iterations are reached.", + "name": "default_route", + "options": [ + "true_result", + "false_result" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "false_result" + }, + "false_case_message": { + "_input_type": "MessageInput", + "advanced": true, + "api_editable": false, + "display_name": "Case False", + "dynamic": false, + "info": "The message to pass if the condition is False.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "false_case_message", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "Standardfall: automatisch freigegeben" + }, + "input_text": { + "_input_type": "MessageTextInput", + "advanced": false, + "api_editable": false, + "display_name": "Text Input", + "dynamic": false, + "info": "The primary text input for the operation.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "input_text", + "override_skip": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "match_text": { + "_input_type": "MessageTextInput", + "advanced": false, + "api_editable": false, + "display_name": "Match Text", + "dynamic": false, + "info": "The text input to compare against.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "match_text", + "override_skip": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "PRUEFUNG" + }, + "max_iterations": { + "_input_type": "IntInput", + "advanced": true, + "api_editable": false, + "display_name": "Max Iterations", + "dynamic": false, + "info": "The maximum number of iterations for the conditional router.", + "list": false, + "list_add_label": "Add More", + "name": "max_iterations", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "int", + "value": 10 + }, + "operator": { + "_input_type": "DropdownInput", + "advanced": false, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Operator", + "dynamic": false, + "external_options": {}, + "info": "The operator to apply for comparing the texts.", + "name": "operator", + "options": [ + "equals", + "not equals", + "contains", + "starts with", + "ends with", + "regex", + "less than", + "less than or equal", + "greater than", + "greater than or equal" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "real_time_refresh": true, + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "equals" + }, + "true_case_message": { + "_input_type": "MessageInput", + "advanced": true, + "api_editable": false, + "display_name": "Case True", + "dynamic": false, + "info": "The message to pass if the condition is True.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "true_case_message", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "Sonderfall: menschliche Pruefung erforderlich" + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "ConditionalRouter" + }, + "id": "ConditionalRouter-router", + "measured": { + "height": 200, + "width": 320 + }, + "position": { + "x": 800, + "y": 200 + }, + "type": "genericNode" + }, + { + "data": { + "id": "ChatOutput-ok", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Display a chat message in the Playground.", + "display_name": "Chat Output", + "documentation": "https://docs.langflow.org/chat-input-and-output", + "edited": false, + "field_order": [], + "frozen": false, + "icon": "MessagesSquare", + "legacy": false, + "metadata": {}, + "minimized": true, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Output Message", + "group_outputs": false, + "method": "message_response", + "name": "message", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "clean_data": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Basic Clean Data", + "dynamic": false, + "info": "Whether to clean data before converting to string.", + "list": false, + "list_add_label": "Add More", + "name": "clean_data", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + }, + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "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" + }, + "context_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Context ID", + "dynamic": false, + "info": "The context ID of the chat. Adds an extra layer to the local memory.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "context_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "data_template": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Data Template", + "dynamic": false, + "info": "Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "data_template", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "{text}" + }, + "input_value": { + "_input_type": "HandleInput", + "advanced": false, + "api_editable": false, + "display_name": "Inputs", + "dynamic": false, + "info": "Message to be passed as output.", + "input_types": [ + "Data", + "JSON", + "DataFrame", + "Table", + "Message" + ], + "list": false, + "list_add_label": "Add More", + "name": "input_value", + "override_skip": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "other", + "value": "" + }, + "sender": { + "_input_type": "DropdownInput", + "advanced": true, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Sender Type", + "dynamic": false, + "external_options": {}, + "info": "Type of sender.", + "name": "sender", + "options": [ + "Machine", + "User" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "Machine" + }, + "sender_name": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Sender Name", + "dynamic": false, + "info": "Name of the sender.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "sender_name", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "AI" + }, + "session_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Session ID", + "dynamic": false, + "info": "The session ID of the chat. If empty, the current session ID parameter will be used.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "session_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "list_add_label": "Add More", + "name": "should_store_message", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "ChatOutput" + }, + "id": "ChatOutput-ok", + "measured": { + "height": 200, + "width": 320 + }, + "position": { + "x": 1200, + "y": 60 + }, + "type": "genericNode" + }, + { + "data": { + "id": "ChatOutput-sonder", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Display a chat message in the Playground.", + "display_name": "Chat Output", + "documentation": "https://docs.langflow.org/chat-input-and-output", + "edited": false, + "field_order": [], + "frozen": false, + "icon": "MessagesSquare", + "legacy": false, + "metadata": {}, + "minimized": true, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Output Message", + "group_outputs": false, + "method": "message_response", + "name": "message", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "clean_data": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Basic Clean Data", + "dynamic": false, + "info": "Whether to clean data before converting to string.", + "list": false, + "list_add_label": "Add More", + "name": "clean_data", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + }, + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "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" + }, + "context_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Context ID", + "dynamic": false, + "info": "The context ID of the chat. Adds an extra layer to the local memory.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "context_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "data_template": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Data Template", + "dynamic": false, + "info": "Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "data_template", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "{text}" + }, + "input_value": { + "_input_type": "HandleInput", + "advanced": false, + "api_editable": false, + "display_name": "Inputs", + "dynamic": false, + "info": "Message to be passed as output.", + "input_types": [ + "Data", + "JSON", + "DataFrame", + "Table", + "Message" + ], + "list": false, + "list_add_label": "Add More", + "name": "input_value", + "override_skip": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "other", + "value": "" + }, + "sender": { + "_input_type": "DropdownInput", + "advanced": true, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Sender Type", + "dynamic": false, + "external_options": {}, + "info": "Type of sender.", + "name": "sender", + "options": [ + "Machine", + "User" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "Machine" + }, + "sender_name": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Sender Name", + "dynamic": false, + "info": "Name of the sender.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "sender_name", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "AI" + }, + "session_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Session ID", + "dynamic": false, + "info": "The session ID of the chat. If empty, the current session ID parameter will be used.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "session_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "list_add_label": "Add More", + "name": "should_store_message", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "ChatOutput" + }, + "id": "ChatOutput-sonder", + "measured": { + "height": 200, + "width": 320 + }, + "position": { + "x": 1200, + "y": 340 + }, + "type": "genericNode" + } + ], "viewport": { "x": 0, "y": 0, @@ -1381,12 +1381,8 @@ }, "description": "Regelpruefung ohne Seiteneffekt (Spike-Fixture, kein LLM).", "endpoint_name": null, - "icon": null, - "icon_bg_color": null, "id": "5adedb2b-d50b-5c3f-9951-2e76b8b94869", "is_component": false, - "locked": false, - "mcp_enabled": false, "name": "simple_rule", "tags": [], "webhook": false diff --git a/backend/flows/test-flow.json b/backend/flows/test-flow.json new file mode 100644 index 0000000..d11759b --- /dev/null +++ b/backend/flows/test-flow.json @@ -0,0 +1,747 @@ +{ + "data": { + "edges": [ + { + "animated": false, + "className": "", + "data": { + "sourceHandle": { + "dataType": "CustomInputForm", + "id": "ext:kirche:CustomInputForm@extra-BhL6Q", + "name": "form_data", + "output_types": [ + "Message" + ] + }, + "targetHandle": { + "fieldName": "input_value", + "id": "ChatOutput-HkK3i", + "inputTypes": [ + "Data", + "JSON", + "DataFrame", + "Table", + "Message" + ], + "type": "other" + } + }, + "id": "xy-edge__ext:kirche:CustomInputForm@extra-BhL6Q{œdataTypeœ:œCustomInputFormœ,œidœ:œext:kirche:CustomInputForm@extra-BhL6Qœ,œnameœ:œform_dataœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-HkK3i{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-HkK3iœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", + "selected": false, + "source": "ext:kirche:CustomInputForm@extra-BhL6Q", + "sourceHandle": "{œdataTypeœ:œCustomInputFormœ,œidœ:œext:kirche:CustomInputForm@extra-BhL6Qœ,œnameœ:œform_dataœ,œoutput_typesœ:[œMessageœ]}", + "target": "ChatOutput-HkK3i", + "targetHandle": "{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-HkK3iœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}" + } + ], + "nodes": [ + { + "data": { + "id": "ext:kirche:CustomInputForm@extra-BhL6Q", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Collects structured form values into a single JSON message.", + "display_name": "CustomInput Form", + "documentation": "", + "edited": false, + "field_order": [ + "form_values", + "full_name", + "notes", + "password", + "age", + "amount", + "newsletter", + "department", + "topics", + "satisfaction", + "attachment" + ], + "frozen": false, + "icon": "ClipboardList", + "legacy": false, + "lf_version": "1.12.0", + "metadata": { + "code_hash": "6d186fa88d37", + "dependencies": { + "dependencies": [ + { + "name": "lfx", + "version": "1.12.0" + } + ], + "total_dependencies": 1 + }, + "module": "custom_components.custominput_form" + }, + "minimized": false, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Form Data", + "group_outputs": false, + "loop_types": null, + "method": "collect", + "name": "form_data", + "options": null, + "required_inputs": null, + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "age": { + "_input_type": "IntInput", + "advanced": false, + "api_editable": false, + "display_name": "Age", + "dynamic": false, + "info": "Your age in years", + "list": false, + "list_add_label": "Add More", + "name": "age", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "int", + "value": 18 + }, + "amount": { + "_input_type": "FloatInput", + "advanced": false, + "api_editable": false, + "display_name": "Amount", + "dynamic": false, + "info": "Amount in euros", + "list": false, + "list_add_label": "Add More", + "name": "amount", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "float", + "value": 0 + }, + "attachment": { + "_input_type": "FileInput", + "advanced": false, + "api_editable": false, + "display_name": "Attachment", + "dynamic": false, + "fileTypes": [ + "pdf", + "png", + "jpg" + ], + "file_path": "", + "info": "String reference only for now (no upload yet)", + "list": false, + "list_add_label": "Add More", + "name": "attachment", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "temp_file": false, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "file", + "value": "" + }, + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "value": "\"\"\"Structured form-input component for kirche-einfach flows.\n\nA flow that starts with :class:`CustomInputForm` declares the fields of its\nweb form. The API reads the exported node template\n(``template`` minus ``code``) and derives ``{json_schema, ui_schema}`` from\nthe ``lfx.io`` input definitions below — one JSON-Schema property per field,\nkeyed by ``name``. Field metadata maps as follows:\n\n* ``display_name`` → ``title``, ``info`` → ``description``,\n ``value`` → ``default``, ``options`` → ``enum``\n* ``required`` → required array, ``advanced`` → ``ui:collapsed`` hint\n* ``_input_type`` → JSON type + widget (see ``app/flows/input_schema.py``)\n\n``FileInput`` is a stub for now: it carries a string reference only, real\nupload comes later.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom typing import ClassVar\n\nfrom lfx.custom import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.io import (\n BoolInput,\n DropdownInput,\n FileInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n MultilineInput,\n MultiselectInput,\n Output,\n SecretStrInput,\n SliderInput,\n StrInput,\n)\nfrom lfx.schema.message import Message\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\nclass CustomInputForm(Component):\n display_name = \"CustomInput Form\"\n description = \"Collects structured form values into a single JSON message.\"\n icon = \"ClipboardList\"\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 StrInput(\n name=\"full_name\",\n display_name=\"Full name\",\n info=\"Your first and last name\",\n value=\"\",\n required=True,\n ),\n MultilineInput(\n name=\"notes\",\n display_name=\"Notes\",\n info=\"Anything else we should know\",\n value=\"\",\n ),\n SecretStrInput(\n name=\"password\",\n display_name=\"Password\",\n info=\"Optional access phrase\",\n value=\"\",\n ),\n IntInput(\n name=\"age\",\n display_name=\"Age\",\n info=\"Your age in years\",\n value=18,\n ),\n FloatInput(\n name=\"amount\",\n display_name=\"Amount\",\n info=\"Amount in euros\",\n value=0.0,\n ),\n BoolInput(\n name=\"newsletter\",\n display_name=\"Newsletter\",\n info=\"Send me the parish newsletter\",\n value=False,\n ),\n DropdownInput(\n name=\"department\",\n display_name=\"Department\",\n info=\"Which team is this for\",\n options=[\"Music\", \"Youth\", \"Seniors\", \"Admin\"],\n value=\"Music\",\n ),\n MultiselectInput(\n name=\"topics\",\n display_name=\"Topics\",\n info=\"What are you interested in\",\n options=[\"Worship\", \"Outreach\", \"Courses\"],\n value=[],\n ),\n SliderInput(\n name=\"satisfaction\",\n display_name=\"Satisfaction\",\n info=\"How satisfied are you (1-5)\",\n value=3,\n range_spec=RangeSpec(min=1, max=5, step=1),\n ),\n FileInput(\n name=\"attachment\",\n display_name=\"Attachment\",\n info=\"String reference only for now (no upload yet)\",\n file_types=[\"pdf\", \"png\", \"jpg\"],\n ),\n ]\n outputs: ClassVar[list] = [\n Output(display_name=\"Form Data\", name=\"form_data\", method=\"collect\"),\n ]\n\n def collect(self) -> Message:\n data = {\n \"full_name\": _text(self.full_name),\n \"notes\": _text(self.notes),\n \"password\": _text(self.password),\n \"age\": self.age,\n \"amount\": self.amount,\n \"newsletter\": self.newsletter,\n \"department\": _text(self.department),\n \"topics\": self.topics,\n \"satisfaction\": self.satisfaction,\n \"attachment\": _text(self.attachment),\n }\n # Submitted values (via form_values) win over the field defaults.\n # Accepts the raw dict or the run envelope {\"payload\": {...}}.\n try:\n override = json.loads(_text(self.form_values) or \"{}\")\n except json.JSONDecodeError:\n override = {}\n if isinstance(override, dict):\n if isinstance(override.get(\"payload\"), dict):\n override = override[\"payload\"]\n for key in data:\n if key in override:\n data[key] = override[key]\n return Message(text=json.dumps(data, ensure_ascii=False))\n" + }, + "department": { + "_input_type": "DropdownInput", + "advanced": false, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Department", + "dynamic": false, + "external_options": {}, + "info": "Which team is this for", + "name": "department", + "options": [ + "Music", + "Youth", + "Seniors", + "Admin" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "Youth" + }, + "form_values": { + "_input_type": "MessageTextInput", + "advanced": false, + "api_editable": false, + "display_name": "Form values (JSON envelope)", + "dynamic": false, + "info": "Submitted values as JSON; merged over the defaults", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "form_values", + "override_skip": false, + "placeholder": "", + "required": false, + "show": false, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "full_name": { + "_input_type": "StrInput", + "advanced": false, + "api_editable": false, + "display_name": "Full name", + "dynamic": false, + "info": "Your first and last name", + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "full_name", + "override_skip": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "Hi there" + }, + "newsletter": { + "_input_type": "BoolInput", + "advanced": false, + "api_editable": false, + "display_name": "Newsletter", + "dynamic": false, + "info": "Send me the parish newsletter", + "list": false, + "list_add_label": "Add More", + "name": "newsletter", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": false + }, + "notes": { + "_input_type": "MultilineInput", + "advanced": false, + "ai_enabled": false, + "api_editable": false, + "copy_field": false, + "display_name": "Notes", + "dynamic": false, + "info": "Anything else we should know", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "multiline": true, + "name": "notes", + "override_skip": false, + "password": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "Blabla" + }, + "password": { + "_input_type": "SecretStrInput", + "advanced": false, + "api_editable": false, + "display_name": "Password", + "dynamic": false, + "info": "Optional access phrase", + "input_types": [], + "load_from_db": false, + "name": "password", + "override_skip": false, + "password": true, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "track_in_telemetry": false, + "type": "str", + "value": "hidden" + }, + "satisfaction": { + "_input_type": "SliderInput", + "advanced": false, + "api_editable": false, + "display_name": "Satisfaction", + "dynamic": false, + "info": "How satisfied are you (1-5)", + "max_label": "", + "max_label_icon": "", + "min_label": "", + "min_label_icon": "", + "name": "satisfaction", + "override_skip": false, + "placeholder": "", + "range_spec": { + "max": 5, + "min": 1, + "step": 1, + "step_type": "float" + }, + "required": false, + "show": true, + "slider_buttons": false, + "slider_buttons_options": [], + "slider_input": false, + "title_case": false, + "tool_mode": false, + "track_in_telemetry": false, + "type": "slider", + "value": 3 + }, + "topics": { + "_input_type": "MultiselectInput", + "advanced": false, + "api_editable": false, + "combobox": false, + "display_name": "Topics", + "dynamic": false, + "info": "What are you interested in", + "list": true, + "list_add_label": "Add More", + "name": "topics", + "options": [ + "Worship", + "Outreach", + "Courses" + ], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": [ + "Outreach", + "Courses" + ] + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "CustomInputForm" + }, + "dragging": false, + "id": "ext:kirche:CustomInputForm@extra-BhL6Q", + "measured": { + "height": 933, + "width": 320 + }, + "position": { + "x": -2869.215421770588, + "y": -259.29504008742293 + }, + "selected": true, + "type": "genericNode" + }, + { + "data": { + "id": "ChatOutput-HkK3i", + "node": { + "base_classes": [ + "Message" + ], + "beta": false, + "conditional_paths": [], + "custom_fields": {}, + "description": "Display a chat message in the Playground.", + "display_name": "Chat Output", + "documentation": "https://docs.langflow.org/chat-input-and-output", + "edited": false, + "field_order": [ + "input_value", + "should_store_message", + "sender", + "sender_name", + "session_id", + "context_id", + "data_template", + "clean_data" + ], + "frozen": false, + "icon": "MessagesSquare", + "legacy": false, + "lf_version": "1.12.0", + "metadata": { + "code_hash": "84009527d08c", + "dependencies": { + "dependencies": [ + { + "name": "orjson", + "version": "3.11.9" + }, + { + "name": "fastapi", + "version": "0.139.2" + }, + { + "name": "lfx", + "version": null + } + ], + "total_dependencies": 3 + }, + "module": "lfx.components.input_output.chat_output.ChatOutput" + }, + "minimized": true, + "output_types": [], + "outputs": [ + { + "allows_loop": false, + "cache": true, + "display_name": "Output Message", + "group_outputs": false, + "method": "message_response", + "name": "message", + "selected": "Message", + "tool_mode": true, + "types": [ + "Message" + ], + "value": "__UNDEFINED__" + } + ], + "pinned": false, + "template": { + "_type": "Component", + "clean_data": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Basic Clean Data", + "dynamic": false, + "info": "Whether to clean data before converting to string.", + "list": false, + "list_add_label": "Add More", + "name": "clean_data", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + }, + "code": { + "advanced": true, + "api_editable": false, + "dynamic": true, + "fileTypes": [], + "file_path": "", + "info": "", + "list": false, + "load_from_db": false, + "multiline": true, + "name": "code", + "password": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "type": "code", + "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" + }, + "context_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Context ID", + "dynamic": false, + "info": "The context ID of the chat. Adds an extra layer to the local memory.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "context_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "data_template": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Data Template", + "dynamic": false, + "info": "Template to convert Data to Text. If left empty, it will be dynamically set to the Data's text key.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "data_template", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "{text}" + }, + "input_value": { + "_input_type": "HandleInput", + "advanced": false, + "api_editable": false, + "display_name": "Inputs", + "dynamic": false, + "info": "Message to be passed as output.", + "input_types": [ + "Data", + "JSON", + "DataFrame", + "Table", + "Message" + ], + "list": false, + "list_add_label": "Add More", + "name": "input_value", + "override_skip": false, + "placeholder": "", + "required": true, + "show": true, + "title_case": false, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "other", + "value": "" + }, + "sender": { + "_input_type": "DropdownInput", + "advanced": true, + "api_editable": false, + "combobox": false, + "dialog_inputs": {}, + "display_name": "Sender Type", + "dynamic": false, + "external_options": {}, + "info": "Type of sender.", + "name": "sender", + "options": [ + "Machine", + "User" + ], + "options_metadata": [], + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "toggle": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "str", + "value": "Machine" + }, + "sender_name": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Sender Name", + "dynamic": false, + "info": "Name of the sender.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "sender_name", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "AI" + }, + "session_id": { + "_input_type": "MessageTextInput", + "advanced": true, + "api_editable": false, + "display_name": "Session ID", + "dynamic": false, + "info": "The session ID of the chat. If empty, the current session ID parameter will be used.", + "input_types": [ + "Message" + ], + "list": false, + "list_add_label": "Add More", + "load_from_db": false, + "name": "session_id", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "str", + "value": "" + }, + "should_store_message": { + "_input_type": "BoolInput", + "advanced": true, + "api_editable": false, + "display_name": "Store Messages", + "dynamic": false, + "info": "Store the message in the history.", + "list": false, + "list_add_label": "Add More", + "name": "should_store_message", + "override_skip": false, + "placeholder": "", + "required": false, + "show": true, + "title_case": false, + "tool_mode": false, + "trace_as_metadata": true, + "track_in_telemetry": true, + "type": "bool", + "value": true + } + }, + "tool_mode": false + }, + "showNode": true, + "type": "ChatOutput" + }, + "id": "ChatOutput-HkK3i", + "measured": { + "height": 168, + "width": 320 + }, + "position": { + "x": -2181.727435009515, + "y": 144.80068578973714 + }, + "selected": false, + "type": "genericNode" + } + ], + "viewport": { + "x": 1755.3131372405762, + "y": 167.2405536194492, + "zoom": 0.5755627009646302 + } + }, + "description": "The Pinnacle of Prompt Generation.", + "endpoint_name": null, + "id": "fd440bee-f669-44a2-aa12-6af3bc299731", + "is_component": false, + "name": "test_flow", + "tags": [], + "webhook": false +} diff --git a/backend/tests/test_export.py b/backend/tests/test_export.py index c05d4db..9b47657 100644 --- a/backend/tests/test_export.py +++ b/backend/tests/test_export.py @@ -37,3 +37,30 @@ def test_iter_flow_files_only_top_level_json(tmp_path): sub.mkdir() (sub / "c.json").write_text("{}") assert export.iter_flow_files(tmp_path) == [tmp_path / "a.json"] + + +def test_find_reuse_path_matches_identical_content_first(tmp_path): + doc = {"name": "Simple Rule", "data": {"nodes": [], "edges": []}} + other = tmp_path / "other-name.json" + other.write_text(export.canonical_bytes(doc).decode()) + path, identical = export.find_reuse_path(tmp_path, dict(doc)) + assert (path, identical) == (other, True) + + +def test_find_reuse_path_matches_same_name(tmp_path): + old = {"name": "Simple Rule", "data": {"nodes": [], "edges": []}, "extra": 1} + (tmp_path / "simple_rule.json").write_text(json.dumps(old)) + new = {"name": "Simple Rule", "data": {"nodes": [], "edges": []}} + path, identical = export.find_reuse_path(tmp_path, new) + assert (path, identical) == (tmp_path / "simple_rule.json", False) + + +def test_find_reuse_path_no_match_and_missing_dir(tmp_path): + (tmp_path / "a.json").write_text(json.dumps({"name": "Other"})) + assert export.find_reuse_path(tmp_path, {"name": "Neu"}) == (None, False) + assert export.find_reuse_path(tmp_path / "gibts-nicht", {"name": "Neu"}) == (None, False) + + +def test_find_reuse_path_skips_unreadable_files(tmp_path): + (tmp_path / "kaputt.json").write_text("{kein json") + assert export.find_reuse_path(tmp_path, {"name": "Neu"}) == (None, False) diff --git a/backend/tests/test_sync.py b/backend/tests/test_sync.py index 5b1fc58..5b74204 100644 --- a/backend/tests/test_sync.py +++ b/backend/tests/test_sync.py @@ -176,6 +176,31 @@ async def test_sync_endpoint_pulls_from_langflow_then_syncs( assert body["skipped"] == ["gehaltserh-hung"] +async def test_sync_endpoint_pull_reuses_same_named_file( + client, lfx_server, tmp_path, monkeypatch, login, override_langflow +): + """A pulled flow seeded from simple_rule.json updates it in place + instead of duplicating it as simple-rule.json (slug drift).""" + await login() + monkeypatch.setattr(settings, "flows_dir", str(tmp_path)) + _seed_dir(tmp_path) # fixture shape, name "simple_rule" + langflow_id = "44444444-5555-4666-8777-888888888888" + override_langflow( + flows=[{"id": langflow_id, "name": "simple_rule"}], + docs={langflow_id: _langflow_doc("simple_rule", langflow_id)}, + ) + response = await client.post("/api/flows/sync") + assert response.status_code == 200, response.text + body = response.json() + assert body["pulled"] == ["simple_rule"] + assert body["added"] == ["simple_rule"] + assert body["errors"] == {} + assert not (tmp_path / "simple-rule.json").exists() + + response = await client.post("/api/flows/sync") + assert response.json()["skipped"] == ["simple_rule"] + + async def test_sync_endpoint_pull_error_continues( client, lfx_server, tmp_path, monkeypatch, login, override_langflow ): diff --git a/lfx-entrypoint.sh b/lfx-entrypoint.sh new file mode 100644 index 0000000..a506a36 --- /dev/null +++ b/lfx-entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# LFX entrypoint (runs as root, drops privileges for the server). +# The named volume arrives root-owned (Docker creates it as root and the +# image USER cannot chown it) — without this every flow upload fails with +# "Permission denied" and synced flows stay version-less (invisible in UI). +set -e + +chown -R lfx:lfx /flows +exec gosu lfx "$@"