Workflow to Langflow Graph to Custom Component Generator
Workflow to Langflow Graph to Custom Component Generator is a verified WORKFLOW blueprint on Promptally in Make.com & Langflow Graphs optimized for leading AI models. Multi-step automated node blueprint for Langflow Graph to Custom Component Generator. Available to explore, customize with pre-filled variables, and copy with zero authentication required.
Technical Specifications
| Specification | Value |
|---|---|
| Artifact type | WORKFLOW (Make.com & Langflow Graphs) |
| Compatible engines | Universal / Model Agnostic |
| Input variables | None (pre-filled) |
| Approximate tokens | 52,263 |
| Source file | Custom Component Generator.json |
| Access tier | Free public (MIT) |
Pre-Filled Template
{
"data": {
"edges": [
{
"animated": false,
"className": "",
"data": {
"sourceHandle": {
"dataType": "ChatInput",
"id": "ChatInput-PxRxr",
"name": "message",
"output_types": [
"Message"
]
},
"targetHandle": {
"fieldName": "input_value",
"id": "Agent-I0nts",
"inputTypes": [
"Message"
],
"type": "str"
}
},
"id": "reactflow__edge-ChatInput-PxRxr{\u0153dataType\u0153:\u0153ChatInput\u0153,\u0153id\u0153:\u0153ChatInput-PxRxr\u0153,\u0153name\u0153:\u0153message\u0153,\u0153output_types\u0153:[\u0153Message\u0153]}-Agent-I0nts{\u0153fieldName\u0153:\u0153input_value\u0153,\u0153id\u0153:\u0153Agent-I0nts\u0153,\u0153inputTypes\u0153:[\u0153Message\u0153],\u0153type\u0153:\u0153str\u0153}",
"selected": false,
"source": "ChatInput-PxRxr",
"sourceHandle": "{\u0153dataType\u0153: \u0153ChatInput\u0153, \u0153id\u0153: \u0153ChatInput-PxRxr\u0153, \u0153name\u0153: \u0153message\u0153, \u0153output_types\u0153: [\u0153Message\u0153]}",
"target": "Agent-I0nts",
"targetHandle": "{\u0153fieldName\u0153: \u0153input_value\u0153, \u0153id\u0153: \u0153Agent-I0nts\u0153, \u0153inputTypes\u0153: [\u0153Message\u0153], \u0153type\u0153: \u0153str\u0153}"
},
{
"animated": false,
"className": "",
"data": {
"sourceHandle": {
"dataType": "URLComponent",
"id": "URLComponent-P2u21",
"name": "component_as_tool",
"output_types": [
"Tool"
]
},
"targetHandle": {
"fieldName": "tools",
"id": "Agent-I0nts",
"inputTypes": [
"Tool"
],
"type": "other"
}
},
"id": "reactflow__edge-URLComponent-P2u21{\u0153dataType\u0153:\u0153URLComponent\u0153,\u0153id\u0153:\u0153URLComponent-P2u21\u0153,\u0153name\u0153:\u0153component_as_tool\u0153,\u0153output_types\u0153:[\u0153Tool\u0153]}-Agent-I0nts{\u0153fieldName\u0153:\u0153tools\u0153,\u0153id\u0153:\u0153Agent-I0nts\u0153,\u0153inputTypes\u0153:[\u0153Tool\u0153],\u0153type\u0153:\u0153other\u0153}",
"selected": false,
"source": "URLComponent-P2u21",
"sourceHandle": "{\u0153dataType\u0153: \u0153URLComponent\u0153, \u0153id\u0153: \u0153URLComponent-P2u21\u0153, \u0153name\u0153: \u0153component_as_tool\u0153, \u0153output_types\u0153: [\u0153Tool\u0153]}",
"target": "Agent-I0nts",
"targetHandle": "{\u0153fieldName\u0153: \u0153tools\u0153, \u0153id\u0153: \u0153Agent-I0nts\u0153, \u0153inputTypes\u0153: [\u0153Tool\u0153], \u0153type\u0153: \u0153other\u0153}"
},
{
"animated": false,
"className": "",
"data": {
"sourceHandle": {
"dataType": "MemoryBase",
"id": "MemoryBase-lgwzl",
"name": "component_as_tool",
"output_types": [
"Tool"
]
},
"targetHandle": {
"fieldName": "tools",
"id": "Agent-I0nts",
"inputTypes": [
"Tool"
],
"type": "other"
}
},
"id": "reactflow__edge-MemoryBase-lgwzl{\u0153dataType\u0153:\u0153MemoryBase\u0153,\u0153id\u0153:\u0153MemoryBase-lgwzl\u0153,\u0153name\u0153:\u0153component_as_tool\u0153,\u0153output_types\u0153:[\u0153Tool\u0153]}-Agent-I0nts{\u0153fieldName\u0153:\u0153tools\u0153,\u0153id\u0153:\u0153Agent-I0nts\u0153,\u0153inputTypes\u0153:[\u0153Tool\u0153],\u0153type\u0153:\u0153other\u0153}",
"selected": false,
"source": "MemoryBase-lgwzl",
"sourceHandle": "{\u0153dataType\u0153: \u0153MemoryBase\u0153, \u0153id\u0153: \u0153MemoryBase-lgwzl\u0153, \u0153name\u0153: \u0153component_as_tool\u0153, \u0153output_types\u0153: [\u0153Tool\u0153]}",
"target": "Agent-I0nts",
"targetHandle": "{\u0153fieldName\u0153: \u0153tools\u0153, \u0153id\u0153: \u0153Agent-I0nts\u0153, \u0153inputTypes\u0153: [\u0153Tool\u0153], \u0153type\u0153: \u0153other\u0153}"
},
{
"animated": false,
"className": "",
"data": {
"sourceHandle": {
"dataType": "Agent",
"id": "Agent-I0nts",
"name": "response",
"output_types": [
"Message"
]
},
"targetHandle": {
"fieldName": "input_value",
"id": "ChatOutput-c7KeD",
"inputTypes": [
"Data",
"JSON",
"DataFrame",
"Table",
"Message"
],
"type": "other"
}
},
"id": "reactflow__edge-Agent-I0nts{\u0153dataType\u0153:\u0153Agent\u0153,\u0153id\u0153:\u0153Agent-I0nts\u0153,\u0153name\u0153:\u0153response\u0153,\u0153output_types\u0153:[\u0153Message\u0153]}-ChatOutput-c7KeD{\u0153fieldName\u0153:\u0153input_value\u0153,\u0153id\u0153:\u0153ChatOutput-c7KeD\u0153,\u0153inputTypes\u0153:[\u0153Data\u0153,\u0153JSON\u0153,\u0153DataFrame\u0153,\u0153Table\u0153,\u0153Message\u0153],\u0153type\u0153:\u0153other\u0153}",
"selected": false,
"source": "Agent-I0nts",
"sourceHandle": "{\u0153dataType\u0153: \u0153Agent\u0153, \u0153id\u0153: \u0153Agent-I0nts\u0153, \u0153name\u0153: \u0153response\u0153, \u0153output_types\u0153: [\u0153Message\u0153]}",
"target": "ChatOutput-c7KeD",
"targetHandle": "{\u0153fieldName\u0153: \u0153input_value\u0153, \u0153id\u0153: \u0153ChatOutput-c7KeD\u0153, \u0153inputTypes\u0153: [\u0153Data\u0153, \u0153JSON\u0153, \u0153DataFrame\u0153, \u0153Table\u0153, \u0153Message\u0153], \u0153type\u0153: \u0153other\u0153}"
}
],
"nodes": [
{
"data": {
"id": "ChatInput-PxRxr",
"node": {
"base_classes": [
"Message"
],
"beta": false,
"category": "inputs",
"conditional_paths": [],
"custom_fields": {},
"description": "Get chat inputs from the Playground.",
"display_name": "Chat Input",
"documentation": "",
"edited": false,
"field_order": [
"input_value",
"should_store_message",
"sender",
"sender_name",
"session_id",
"context_id",
"files"
],
"frozen": false,
"icon": "MessagesSquare",
"key": "ChatInput",
"legacy": false,
"lf_version": "1.6.0",
"metadata": {
"code_hash": "45974e6b1909",
"component_class_name": "ChatInput",
"dependencies": {
"dependencies": [
{
"name": "lfx",
"version": null
}
],
"total_dependencies": 1
},
"module": "lfx.components.input_output.chat.ChatInput"
},
"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,
"score": 0.0020353564437605998,
"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,
"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",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"type": "str",
"value": ""
},
"files": {
"_input_type": "FileInput",
"advanced": true,
"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",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"trace_as_metadata": true,
"type": "file",
"value": ""
},
"input_value": {
"_input_type": "MultilineInput",
"advanced": 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",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"type": "str",
"value": ""
},
"sender": {
"_input_type": "DropdownInput",
"advanced": true,
"combobox": false,
"dialog_inputs": {},
"display_name": "Sender Type",
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options_metadata": [],
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"type": "str",
"value": "User"
},
"sender_name": {
"_input_type": "MessageTextInput",
"advanced": true,
"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",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"type": "str",
"value": "User"
},
"session_id": {
"_input_type": "MessageTextInput",
"advanced": true,
"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",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"type": "str",
"value": ""
},
"should_store_message": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Store Messages",
"dynamic": false,
"info": "Store the message in the history.",
"list": false,
"list_add_label": "Add More",
"name": "should_store_message",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"type": "bool",
"value": true
}
},
"tool_mode": false
},
"selected_output": "message",
"showNode": false,
"type": "ChatInput"
},
"dragging": false,
"id": "ChatInput-PxRxr",
"measured": {
"height": 52,
"width": 192
},
"position": {
"x": 2280.3908009734805,
"y": 1123.5918510343654
},
"selected": false,
"type": "genericNode"
},
{
"data": {
"_connectionMode": false,
"id": "Agent-I0nts",
"node": {
"base_classes": [
"Data",
"JSON",
"Message"
],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
"description": "Define the agent's instructions, then enter a task to complete using tools.",
"display_name": "Agent",
"documentation": "https://docs.langflow.org/agents",
"edited": false,
"field_order": [
"model",
"api_key",
"base_url_ibm_watsonx",
"project_id",
"system_prompt",
"context_id",
"n_messages",
"max_tokens",
"format_instructions",
"output_schema",
"tools",
"input_value",
"handle_parsing_errors",
"max_iterations",
"stream",
"add_current_date_tool",
"add_calculator_tool"
],
"frozen": false,
"icon": "bot",
"last_updated": "2026-06-26T12:09:59.745Z",
"legacy": false,
"lf_version": "1.10.1rc3",
"metadata": {
"code_hash": "9fcfe75813e7",
"component_class_name": "AgentComponent",
"dependencies": {
"dependencies": [
{
"name": "langchain",
"version": "1.3.14"
},
{
"name": "langgraph",
"version": "1.2.19"
},
{
"name": "lfx",
"version": null
},
{
"name": "langchain_core",
"version": "1.5.1"
}
],
"total_dependencies": 4
},
"module": "lfx.components.models_and_agents.agent.AgentComponent"
},
"minimized": false,
"output_types": [],
"outputs": [
{
"allows_loop": false,
"cache": true,
"display_name": "Response",
"group_outputs": false,
"method": "message_response",
"name": "response",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"value": "__UNDEFINED__"
},
{
"allows_loop": false,
"cache": true,
"display_name": "Structured Response",
"group_outputs": false,
"method": "json_response",
"name": "structured_response",
"selected": "JSON",
"tool_mode": true,
"types": [
"Data",
"JSON"
],
"value": "__UNDEFINED__"
}
],
"pinned": false,
"template": {
"_type": "Component",
"add_calculator_tool": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Calculator",
"dynamic": false,
"info": "If true, adds a zero-config arithmetic calculator tool to the agent (safe: only +, -, *, /, ** operators via AST).",
"input_types": [],
"list": false,
"list_add_label": "Add More",
"name": "add_calculator_tool",
"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
},
"add_current_date_tool": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Current Date",
"dynamic": false,
"info": "If true, will add a tool to the agent that returns the current date.",
"input_types": [],
"list": false,
"list_add_label": "Add More",
"name": "add_current_date_tool",
"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
},
"api_key": {
"_input_type": "SecretStrInput",
"advanced": true,
"display_name": "API Key",
"dynamic": false,
"info": "Overrides global provider settings. Leave blank to use your pre-configured API Key.",
"input_types": [],
"load_from_db": true,
"name": "api_key",
"override_skip": false,
"password": true,
"placeholder": "",
"real_time_refresh": true,
"required": false,
"show": true,
"title_case": false,
"track_in_telemetry": false,
"type": "str",
"value": ""
},
"base_url_ibm_watsonx": {
"_input_type": "DropdownInput",
"advanced": false,
"combobox": true,
"dialog_inputs": {},
"display_name": "watsonx API Endpoint",
"dynamic": false,
"external_options": {},
"info": "The base URL of the API (IBM watsonx.ai only)",
"input_types": [],
"name": "base_url_ibm_watsonx",
"options": [
"https://us-south.ml.cloud.ibm.com",
"https://eu-de.ml.cloud.ibm.com",
"https://eu-gb.ml.cloud.ibm.com",
"https://au-syd.ml.cloud.ibm.com",
"https://jp-tok.ml.cloud.ibm.com",
"https://ca-tor.ml.cloud.ibm.com"
],
"options_metadata": [],
"override_skip": false,
"placeholder": "",
"real_time_refresh": true,
"required": false,
"show": false,
"title_case": false,
"toggle": false,
"tool_mode": false,
"trace_as_metadata": true,
"track_in_telemetry": true,
"type": "str",
"value": "https://us-south.ml.cloud.ibm.com"
},
"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 __future__ import annotations\n\nimport uuid\nfrom collections.abc import Mapping\nfrom contextlib import contextmanager\nfrom datetime import datetime, timezone\nfrom typing import TYPE_CHECKING, Any, cast\n\nfrom langchain.agents import create_agent\nfrom langchain.agents.middleware import (\n HumanInTheLoopMiddleware,\n ModelCallLimitMiddleware,\n ToolRetryMiddleware,\n)\nfrom langgraph.types import Command\n\nfrom lfx.components.models_and_agents.agent_helpers.graph_event_adapter import (\n adapt_graph_events_to_executor_shape,\n)\nfrom lfx.components.models_and_agents.agent_helpers.messages_input_builder import (\n build_initial_messages,\n)\nfrom lfx.components.models_and_agents.agent_helpers.placeholder_corrective_middleware import (\n WatsonXPlaceholderMiddleware,\n)\nfrom lfx.components.models_and_agents.agent_helpers.single_tool_call_middleware import (\n SingleToolCallMiddleware,\n)\nfrom lfx.components.models_and_agents.agent_helpers.tool_approval import ToolApprovalMixin\nfrom lfx.components.models_and_agents.agent_helpers.tool_call_id_middleware import ToolCallIDMiddleware\nfrom lfx.components.models_and_agents.memory import MemoryComponent, _safe_graph_user_id, aget_agent_chat_history\n\nif TYPE_CHECKING:\n from collections.abc import Awaitable, Callable\n\n from langchain_core.tools import Tool\n\n from lfx.schema.log import OnTokenFunctionType, SendMessageFunctionType\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.callback import AgentAsyncHandler\nfrom lfx.base.agents.default_system_prompt import DEFAULT_SYSTEM_PROMPT_TEMPLATE\nfrom lfx.base.agents.events import AgentPausedError, ExceptionWithMessageError, process_agent_events\nfrom lfx.base.agents.token_callback import TokenUsageCallbackHandler\nfrom lfx.base.agents.utils import get_chat_output_sender_name\nfrom lfx.base.constants import STREAM_INFO_TEXT\nfrom lfx.base.models.unified_models import (\n get_language_model_options,\n get_llm,\n handle_model_input_update,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.agentics.helpers.model_config import validate_model_selection\nfrom lfx.components.helpers import CalculatorComponent, CurrentDateComponent\nfrom lfx.components.langchain_utilities.ibm_granite_handler import is_watsonx_model\nfrom lfx.components.langchain_utilities.tool_calling import ToolCallingAgentComponent\nfrom lfx.custom.custom_component.component import get_component_toolkit\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, ModelInput, StrInput\nfrom lfx.io import IntInput, MessageTextInput, MultilineInput, Output, SecretStrInput, TableInput\nfrom lfx.log.logger import logger\nfrom lfx.memory import delete_message\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\nfrom lfx.utils.constants import MESSAGE_SENDER_AI\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\ndef _agent_base_inputs():\n \"\"\"Return base inputs tailored to AgentComponent's create_agent path.\n\n `get_base_inputs()` returns a shared list \u2014 replace, don't mutate. We drop\n inputs that are no-ops here and override info text on the inputs whose\n semantics shifted under create_agent.\n\n `verbose` is dropped because the create_agent event stream already surfaces\n every agent step via the \"Agent Steps\" content blocks; the legacy boolean\n has nothing to toggle. Saved flows that still carry a `verbose` value just\n ignore it on load (the schema no longer declares it).\n \"\"\"\n drop = {\"verbose\"}\n overrides = {\n \"handle_parsing_errors\": BoolInput(\n name=\"handle_parsing_errors\",\n display_name=\"Handle Parse Errors\",\n value=True,\n advanced=True,\n info=(\n \"Adds tool-execution retry as a safety net. `create_agent` already \"\n \"feeds tool-call validation errors back to the LLM automatically; \"\n \"this flag layers `ToolRetryMiddleware` on top so transient tool \"\n \"runtime failures are retried (max 2 retries).\"\n ),\n ),\n \"max_iterations\": IntInput(\n name=\"max_iterations\",\n display_name=\"Max Iterations\",\n value=15,\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n info=(\n \"Maximum number of model calls the agent can make before stopping \"\n \"(maps to `ModelCallLimitMiddleware.run_limit` on the create_agent \"\n \"path). Must be at least 1 \u2014 it is a safety cap, never 'unlimited'.\"\n ),\n ),\n }\n return [overrides.get(inp.name, inp) for inp in LCToolsAgentComponent.get_base_inputs() if inp.name not in drop]\n\n\ndef _extract_text_content(value) -> str:\n \"\"\"Pull a string payload from a Message-like, AIMessage-like, or string value.\"\"\"\n if isinstance(value, str):\n return value\n text = getattr(value, \"text\", None)\n if isinstance(text, str):\n return text\n content = getattr(value, \"content\", None)\n if isinstance(content, str):\n return content\n return str(value) if value is not None else \"\"\n\n\n@contextmanager\ndef _suppress_send_message(component: Any):\n \"\"\"Temporarily replace component.send_message with a no-op for the duration of the block.\n\n Used during the structured-output prompt fallback: run_agent streams the agent's\n final answer through self.send_message (correct for message_response), but in\n json_response the orchestrator parses that text into structured Data which the\n downstream Chat Output emits \u2014 leaving the original emission in place produces a\n duplicate message in the playground. The original method is always restored on exit,\n even when the wrapped call raises.\n \"\"\"\n original = component.send_message\n\n async def _noop(message, *_args, **_kwargs):\n return message\n\n component.send_message = _noop\n try:\n yield\n finally:\n component.send_message = original\n\n\nclass AgentComponent(ToolApprovalMixin, ToolCallingAgentComponent):\n display_name: str = \"Agent\"\n description: str = \"Define the agent's instructions, then enter a task to complete using tools.\"\n documentation: str = \"https://docs.langflow.org/agents\"\n icon = \"bot\"\n beta = False\n name = \"Agent\"\n\n memory_inputs = [set_advanced_true(component_input) for component_input in MemoryComponent().inputs]\n\n inputs = [\n ModelInput(\n name=\"model\",\n display_name=\"Language Model\",\n info=\"Select your model provider\",\n real_time_refresh=True,\n required=True,\n # Agents require tool calling \u2014 the filter is honored by\n # ``handle_model_input_update`` so models that can't run with\n # tools never reach the picker (and any saved selection that\n # no longer satisfies the constraint is auto-replaced).\n filters={\"tool_calling\": True},\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Overrides global provider settings. Leave blank to use your pre-configured API Key.\",\n real_time_refresh=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"base_url_ibm_watsonx\",\n display_name=\"watsonx API Endpoint\",\n info=\"The base URL of the API (IBM watsonx.ai only)\",\n options=IBM_WATSONX_URLS,\n value=IBM_WATSONX_URLS[0],\n combobox=True,\n show=False,\n real_time_refresh=True,\n ),\n StrInput(\n name=\"project_id\",\n display_name=\"watsonx Project ID\",\n info=\"The project ID associated with the foundation model (IBM watsonx.ai only)\",\n show=False,\n required=False,\n ),\n MultilineInput(\n name=\"system_prompt\",\n display_name=\"Agent Instructions\",\n info=(\n \"System Prompt: Initial instructions and context provided to guide the agent's behavior. \"\n \"Supports dynamic placeholders: {current_date}, {model_name}, {optional_user_context}.\"\n ),\n value=DEFAULT_SYSTEM_PROMPT_TEMPLATE,\n advanced=False,\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 IntInput(\n name=\"n_messages\",\n display_name=\"Number of Chat History Messages\",\n value=100,\n info=\"Number of chat history messages to retrieve.\",\n advanced=True,\n show=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Set to 0 for no explicit limit. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=0, max=128000, step=1, step_type=\"int\"),\n ),\n MultilineInput(\n name=\"format_instructions\",\n display_name=\"Output Format Instructions\",\n info=\"Generic Template for structured output formatting. Valid only with Structured response.\",\n value=(\n \"You are an AI that extracts structured JSON objects from unstructured text. \"\n \"Use a predefined schema with expected types (str, int, float, bool, dict). \"\n \"Extract ALL relevant instances that match the schema - if multiple patterns exist, capture them all. \"\n \"Fill missing or ambiguous values with defaults: null for missing values. \"\n \"Remove exact duplicates but keep variations that have different field values. \"\n \"Always return valid JSON in the expected format, never throw errors. \"\n \"If multiple objects can be extracted, return them all in the structured format.\"\n ),\n advanced=True,\n ),\n TableInput(\n name=\"output_schema\",\n display_name=\"Output Schema\",\n info=(\n \"Schema Validation: Define the structure and data types for structured output. \"\n \"No validation if no output schema.\"\n ),\n advanced=True,\n required=False,\n value=[],\n table_schema=[\n {\n \"name\": \"name\",\n \"display_name\": \"Name\",\n \"type\": \"str\",\n \"description\": \"Specify the name of the output field.\",\n \"default\": \"field\",\n \"edit_mode\": EditMode.INLINE,\n },\n {\n \"name\": \"description\",\n \"display_name\": \"Description\",\n \"type\": \"str\",\n \"description\": \"Describe the purpose of the output field.\",\n \"default\": \"description of field\",\n \"edit_mode\": EditMode.POPOVER,\n },\n {\n \"name\": \"type\",\n \"display_name\": \"Type\",\n \"type\": \"str\",\n \"edit_mode\": EditMode.INLINE,\n \"description\": (\"Indicate the data type of the output field (e.g., str, int, float, bool, dict).\"),\n \"options\": [\"str\", \"int\", \"float\", \"bool\", \"dict\"],\n \"default\": \"str\",\n },\n {\n \"name\": \"multiple\",\n \"display_name\": \"As List\",\n \"type\": \"boolean\",\n \"description\": \"Set to True if this output field should be a list of the specified type.\",\n \"default\": \"False\",\n \"edit_mode\": EditMode.INLINE,\n },\n ],\n ),\n *_agent_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=STREAM_INFO_TEXT,\n value=True,\n advanced=True,\n ),\n BoolInput(\n name=\"add_current_date_tool\",\n display_name=\"Current Date\",\n advanced=True,\n info=\"If true, will add a tool to the agent that returns the current date.\",\n value=True,\n ),\n BoolInput(\n name=\"add_calculator_tool\",\n display_name=\"Calculator\",\n advanced=True,\n info=(\n \"If true, adds a zero-config arithmetic calculator tool to the agent \"\n \"(safe: only +, -, *, /, ** operators via AST).\"\n ),\n value=True,\n ),\n ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\n Output(\n name=\"structured_response\",\n display_name=\"Structured Response\",\n method=\"json_response\",\n types=[\"Data\"],\n ),\n ]\n\n async def _additional_model_provider_policy_ids(self, purpose, parameters=None) -> tuple[str, ...]:\n \"\"\"Gate the provider selector retained by legacy Agent/ALTK flows.\"\"\"\n _ = purpose\n from lfx.base.models.provider_registry import resolve_provider_id\n\n effective_parameters = parameters if isinstance(parameters, Mapping) else getattr(self, \"_parameters\", None)\n if not isinstance(effective_parameters, Mapping):\n effective_parameters = {}\n selected_model = effective_parameters.get(\"model\", getattr(self, \"model\", None))\n if selected_model:\n # The shared Component hook already handles a selected ModelInput;\n # connected model objects are gated by their upstream vertex.\n return ()\n legacy_provider = effective_parameters.get(\"agent_llm\", getattr(self, \"agent_llm\", None))\n if not isinstance(legacy_provider, str) or not legacy_provider.strip() or legacy_provider == \"Custom\":\n return ()\n return (resolve_provider_id(legacy_provider),)\n\n async def _filter_legacy_provider_options(self, build_config: Mapping[str, Any]) -> None:\n \"\"\"Filter ALTK/legacy Agent provider choices through the active scope.\"\"\"\n from lfx.services.model_provider_policy import ModelProviderPolicyPurpose, aresolve_model_provider_policy\n\n provider_field = build_config.get(\"agent_llm\")\n if not isinstance(provider_field, dict):\n return\n options = provider_field.get(\"options\")\n if not isinstance(options, list):\n return\n candidates = [option for option in options if isinstance(option, str) and option != \"Custom\"]\n if not candidates:\n return\n snapshot = await aresolve_model_provider_policy(\n user_id=self.user_id,\n providers=candidates,\n purpose=ModelProviderPolicyPurpose.CONFIGURE,\n )\n allowed = set(snapshot.filter(candidates))\n keep_indexes = [index for index, option in enumerate(options) if option == \"Custom\" or option in allowed]\n provider_field[\"options\"] = [options[index] for index in keep_indexes]\n metadata = provider_field.get(\"options_metadata\")\n if isinstance(metadata, list) and len(metadata) == len(options):\n provider_field[\"options_metadata\"] = [metadata[index] for index in keep_indexes]\n\n def _resolve_selected_model(self):\n \"\"\"Resolve the selected model, including legacy agent_llm/model_name inputs.\"\"\"\n try:\n from langchain_core.language_models import BaseLanguageModel\n\n if isinstance(self.model, BaseLanguageModel):\n return self.model\n except ImportError:\n pass\n\n if isinstance(self.model, list) and self.model:\n return self.model\n\n legacy_provider = getattr(self, \"agent_llm\", None)\n legacy_model_name = getattr(self, \"model_name\", None)\n if not legacy_provider or not legacy_model_name:\n return self.model\n\n options = get_language_model_options(user_id=self.user_id)\n for option in options:\n if option.get(\"provider\") == legacy_provider and option.get(\"name\") == legacy_model_name:\n return [option]\n\n return [\n {\n \"name\": legacy_model_name,\n \"provider\": legacy_provider,\n \"metadata\": {},\n }\n ]\n\n def _get_max_tokens_value(self):\n \"\"\"Return the user-supplied max_tokens or None when unset/zero.\"\"\"\n val = getattr(self, \"max_tokens\", None)\n if val in {\"\", 0}:\n return None\n return val\n\n def _get_llm(self):\n \"\"\"Override parent to include max_tokens from the Agent's input field.\n\n Streaming is mandatory for AgentComponent: ``runnable.astream_events(v2)`` only\n emits ``on_chat_model_stream`` chunks when the underlying chat model is\n instantiated with ``streaming=True``. Unlike the LanguageModel component (where\n ``stream`` is a user-facing toggle), the Agent has no opt-out \u2014 the toggle is\n kept in the UI for backwards compatibility but is intentionally ignored here.\n Without ``stream=True``, the chat model accumulates the whole response and\n only emits ``on_chat_model_end``, silently disabling the Playground's live-\n typing view and breaking the streaming contract on the /events surface.\n \"\"\"\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\n stream=True,\n max_tokens=self._get_max_tokens_value(),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n overrides=getattr(self, \"_model_overrides\", None),\n )\n\n async def get_agent_requirements(self):\n \"\"\"Get the agent requirements for the agent.\"\"\"\n from langchain_core.tools import StructuredTool\n\n from lfx.services.model_provider_policy import ModelProviderPolicyPurpose\n\n await self.arequire_model_provider_policy(ModelProviderPolicyPurpose.USE)\n\n selected_model = self._resolve_selected_model()\n try:\n from langchain_core.language_models import BaseLanguageModel\n\n is_connected_model = isinstance(selected_model, BaseLanguageModel)\n except ImportError:\n is_connected_model = False\n\n if not is_connected_model:\n validate_model_selection(selected_model)\n\n # Ensure _get_llm() uses the resolved model (e.g. from legacy agent_llm/model_name)\n self.model = selected_model\n llm_model = self._get_llm()\n if llm_model is None:\n msg = \"No language model selected. Please choose a model to proceed.\"\n raise ValueError(msg)\n\n # Get memory data\n self.chat_history = await self.get_memory_data()\n await logger.adebug(f\"Retrieved {len(self.chat_history)} chat history messages\")\n if isinstance(self.chat_history, Message):\n self.chat_history = [self.chat_history]\n\n # Add current date tool if enabled\n if self.add_current_date_tool:\n if not isinstance(self.tools, list): # type: ignore[has-type]\n self.tools = []\n current_date_tool = (await CurrentDateComponent(**self.get_base_args()).to_toolkit()).pop(0)\n\n if not isinstance(current_date_tool, StructuredTool):\n msg = \"CurrentDateComponent must be converted to a StructuredTool\"\n raise TypeError(msg)\n # Skip if an externally-connected tool already provides the same name.\n # Duplicate tool names are rejected by Anthropic/Gemini with HTTP 400.\n if not any(getattr(t, \"name\", None) == current_date_tool.name for t in self.tools):\n self.tools.append(current_date_tool)\n\n # Add calculator tool if enabled (zero-config arithmetic)\n if getattr(self, \"add_calculator_tool\", False):\n if not isinstance(self.tools, list): # type: ignore[has-type]\n self.tools = []\n calculator_tool = (await CalculatorComponent(**self.get_base_args()).to_toolkit()).pop(0)\n\n if not isinstance(calculator_tool, StructuredTool):\n msg = \"CalculatorComponent must be converted to a StructuredTool\"\n raise TypeError(msg)\n # Skip if an externally-connected tool already provides the same name.\n # Duplicate tool names are rejected by Anthropic/Gemini with HTTP 400.\n if not any(getattr(t, \"name\", None) == calculator_tool.name for t in self.tools):\n self.tools.append(calculator_tool)\n\n # Set shared callbacks for tracing the tools used by the agent\n self.set_tools_callbacks(self.tools, self._get_shared_callbacks())\n\n return llm_model, self.chat_history, self.tools\n\n def _get_resolved_model_name(self) -> str:\n \"\"\"Best-effort human-readable model name for {model_name} injection.\"\"\"\n try:\n from langchain_core.language_models import BaseLanguageModel\n\n if isinstance(self.model, BaseLanguageModel):\n return type(self.model).__name__\n except ImportError:\n pass\n\n if isinstance(self.model, list) and self.model:\n first = self.model[0]\n if isinstance(first, dict):\n name = first.get(\"name\")\n if isinstance(name, str) and name:\n return name\n\n legacy_model_name = getattr(self, \"model_name\", None)\n if isinstance(legacy_model_name, str) and legacy_model_name:\n return legacy_model_name\n return \"\"\n\n def _inject_dynamic_prompt_values(self, prompt: Any | None) -> str | None:\n \"\"\"Replace known env placeholders in the system prompt.\n\n Handles {current_date}, {model_name}, and {optional_user_context} (the\n last one ships with the structured DEFAULT_SYSTEM_PROMPT_TEMPLATE and\n is currently unused at the AgentComponent layer, so it resolves to \"\").\n Uses str.replace (not str.format) so user prompts containing literal\n braces such as JSON examples ({\"key\": 1}) never break the agent.\n\n `system_prompt` is a connectable MultilineInput, so the value can arrive\n as a Message (e.g. a Prompt node wired in). Normalize it to text first \u2014\n a raw Message has no `.replace` and used to crash the agent build.\n \"\"\"\n if prompt is None:\n return None\n prompt = _extract_text_content(prompt)\n if not prompt:\n return prompt\n replacements = {\n # Day precision keeps the system prompt byte-identical across turns so\n # providers can serve the chat history from their prompt cache.\n \"{current_date}\": datetime.now(tz=timezone.utc).date().isoformat(),\n \"{model_name}\": self._get_resolved_model_name(),\n \"{optional_user_context}\": \"\",\n }\n for placeholder, value in replacements.items():\n prompt = prompt.replace(placeholder, value)\n return prompt\n\n def create_agent_runnable(self, *, allow_interrupts: bool = True):\n \"\"\"Build the LangGraph `CompiledStateGraph` via `langchain.agents.create_agent`.\n\n Replaces the legacy `AgentExecutor` runnable inherited from\n `ToolCallingAgentComponent`. Other agent components (tool_calling, csv, json,\n openapi, sql*, vector_store_router) keep the legacy path \u2014 only AgentComponent\n runs on the new graph API.\n\n `max_iterations` and `handle_parsing_errors` (legacy AgentExecutor knobs) are\n translated to LangGraph middleware. Without that translation those user inputs\n would silently become no-ops on the new API.\n\n Provider notes:\n - WatsonX/Granite work natively with create_agent \u2014 `ChatWatsonx.bind_tools`\n handles tool_choice correctly. The legacy `create_granite_agent` path was\n dropped because it hardcoded `tool_choice='required'`, which the WatsonX\n API now rejects.\n - Ollama and other small/local models often emit malformed tool args. The\n ToolRetryMiddleware (default `retry_on=(Exception,)`, `on_failure='continue'`)\n catches Pydantic ValidationErrors from bad args and feeds the error back\n to the LLM as a retry signal, so the agent recovers gracefully.\n \"\"\"\n llm = self._get_llm()\n tools = self.tools or []\n\n # Eager bind_tools validation. `create_agent(...)` is lazy \u2014 without this,\n # an LLM that doesn't support tool calling fails on the first user message\n # instead of when the user wires up the component, which is a much worse UX.\n # Gated on a non-empty tools list so a no-tool Agent on a plain chat model\n # (which legitimately has no `bind_tools`) isn't shut out at flow-build time.\n # Providers signal \"no tool calling\" inconsistently \u2014 `NotImplementedError`\n # (langchain default), `AttributeError` (no `bind_tools` attr), or `TypeError`\n # (signature mismatch). Treat all three as the same UX failure.\n if tools:\n try:\n llm.bind_tools(tools)\n except (NotImplementedError, AttributeError, TypeError) as exc:\n # Include the underlying error so a broken tool schema or a\n # provider implementation bug is not silently disguised as a\n # \"model can't call tools\" UX error.\n msg = (\n f\"{self.display_name} does not support tool calling, \"\n \"or one of the connected tools failed to bind. \"\n \"Please connect a tool-calling capable language model and \"\n f\"verify your tools are well-formed. Underlying error: {exc!s}\"\n )\n raise NotImplementedError(msg) from exc\n\n middleware = self._build_middleware(llm, allow_interrupts=allow_interrupts)\n checkpointer = self._build_agent_checkpointer() if allow_interrupts else None\n return create_agent(\n model=llm,\n tools=tools,\n system_prompt=self.system_prompt or \"\",\n middleware=middleware or None,\n checkpointer=checkpointer,\n )\n\n def _compute_recursion_limit(self) -> int:\n \"\"\"Derive the LangGraph recursion_limit from the user-set max_iterations.\n\n Mirrors the clamp in `_build_middleware` (max(1, max_iterations)) so a\n saved 0 or negative value cannot under-cap the graph below one full\n iteration.\n\n A tool-calling cycle visits the model-call limiter's before_model and\n after_model nodes, the model, and tools. When any tool requires approval,\n HumanInTheLoopMiddleware adds another after_model node to every cycle,\n even if the model repeatedly calls an ungated tool. Leave transition\n room so the model-call limiter can end the run first.\n \"\"\"\n raw = getattr(self, \"max_iterations\", None)\n run_limit = max(1, int(raw)) if raw is not None else 15\n steps_per_cycle = 4 + bool(self._gated_interrupt_on())\n return run_limit * steps_per_cycle + 10\n\n def _build_middleware(self, llm: Any, *, allow_interrupts: bool = True) -> list:\n # `llm` is passed in (rather than re-fetched via `self._get_llm()`)\n # because some providers do credential resolution / client instantiation\n # lazily on each call. The caller \u2014 `create_agent_runnable` \u2014 already\n # resolved it once for `bind_tools`, so reuse that instance here.\n middleware: list = []\n # LangChain accepts missing IDs on AIMessage.tool_calls, but LangGraph's\n # invalid-call path and ToolRetryMiddleware both require a string when\n # they construct an error ToolMessage. Normalize at the model boundary\n # so either recovery path can return the error to the model instead of\n # crashing the flow.\n if self.tools:\n middleware.append(ToolCallIDMiddleware())\n max_iterations = getattr(self, \"max_iterations\", None)\n if max_iterations is not None:\n # `max_iterations` is a safety cap, not an \"unlimited\" toggle. A saved\n # 0 or negative value (falsy) must NOT silently drop the limiter and\n # allow an unbounded model/tool loop \u2014 clamp it to a real minimum.\n run_limit = max(1, int(max_iterations))\n middleware.append(ModelCallLimitMiddleware(run_limit=run_limit))\n # ToolRetryMiddleware only matters when there ARE tools to retry. Attaching\n # it on a no-tools agent inflates the compiled graph and adds per-invocation\n # middleware overhead for nothing, which is a measurable contributor to\n # trivial-prompt latency (QA UI-003).\n if getattr(self, \"handle_parsing_errors\", False) and self.tools:\n middleware.append(ToolRetryMiddleware(max_retries=2))\n # WatsonX models have two known platform quirks; both still reproduce on\n # the current API, so we keep the protections from the legacy\n # `create_granite_agent` path.\n # 1. Multi-tool-call assistant turns are rejected (\"This model only\n # supports single tool-calls at once!\"). Clamp to one per turn.\n # 2. Tool args occasionally come back as literal placeholder strings\n # (e.g. `<result-from-search>`). Re-invoke once with a corrective\n # SystemMessage.\n # Order: SingleToolCallMiddleware first (outermost) so the clamp is\n # applied to the final response, including any corrective re-invoke\n # produced by WatsonXPlaceholderMiddleware.\n if is_watsonx_model(llm):\n middleware.append(SingleToolCallMiddleware())\n middleware.append(WatsonXPlaceholderMiddleware())\n # Human-in-the-loop: attach only when a tool is gated AND interrupts are allowed\n # (the structured-output path disables them), keeping ungated flows unchanged.\n interrupt_on = self._gated_interrupt_on() if allow_interrupts else {}\n if interrupt_on:\n middleware.append(HumanInTheLoopMiddleware(interrupt_on=interrupt_on))\n return middleware\n\n async def run_agent(self, agent) -> Message:\n \"\"\"Run the LangGraph `CompiledStateGraph` and return the final agent Message.\n\n Overrides the legacy `LCAgentComponent.run_agent` (which builds an\n `{\"input\": str, \"chat_history\": [...]}` dict for `AgentExecutor`). The graph\n wants `{\"messages\": [BaseMessage, ...]}`. The event stream is wrapped with\n `adapt_graph_events_to_executor_shape` so the legacy `process_agent_events`\n (in `lfx.base.agents.events`) can be reused unchanged.\n \"\"\"\n messages = build_initial_messages(\n input_value=self.input_value,\n chat_history=getattr(self, \"chat_history\", None),\n )\n input_dict = {\"messages\": messages}\n\n agent_message = self._build_initial_agent_message()\n token_usage_handler = TokenUsageCallbackHandler()\n\n # Stream tokens to the event manager when running inside the Langflow runtime.\n # This is what powers the live-typing view in the chat UI.\n on_token_callback: OnTokenFunctionType | None = None\n if getattr(self, \"_event_manager\", None):\n on_token_callback = cast(\"OnTokenFunctionType\", self._event_manager.on_token)\n\n # Align LangGraph's `recursion_limit` with `max_iterations` so the\n # middleware cap (ModelCallLimitMiddleware) is what bounds the loop \u2014\n # not LangGraph's default 25-step guard, which fires at ~12 model+tool\n # iterations and raises a raw GraphRecursionError (QA UI-009/UI-010).\n # Include the extra after_model node when tool approval is configured.\n recursion_limit = self._compute_recursion_limit()\n\n agent_config: dict[str, Any] = {\n \"callbacks\": [\n AgentAsyncHandler(self.log),\n token_usage_handler,\n *self._get_shared_callbacks(),\n ],\n \"recursion_limit\": recursion_limit,\n }\n # The durable checkpointer keys on the per-run thread_id; it must be in the\n # astream_events config (not just create_agent) for both initial run and resume.\n thread_id = self._agent_thread_id()\n get_pending_interrupt = None\n stream_input: Any = input_dict\n # A checkpointer is only present when the agent was built with interrupts enabled\n # (the structured-output path builds without one); never probe its state otherwise.\n interrupts_enabled = getattr(agent, \"checkpointer\", None) is not None\n if interrupts_enabled and thread_id and self._gated_interrupt_on():\n agent_config[\"configurable\"] = {\"thread_id\": thread_id}\n get_pending_interrupt = self._pending_interrupt_getter(agent, agent_config)\n if self._has_candidate_decision(thread_id):\n # The injected decision must match the pending interrupt's nonce, so read\n # the interrupt first; a matched decision resumes the checkpointed thread.\n value, interrupt_id = await self._read_pending_interrupt(agent, agent_config)\n decision = self._injected_agent_decision(thread_id, interrupt_id)\n if decision is not None:\n action_requests = (value or {}).get(\"action_requests\") or []\n stream_input = Command(\n resume={\"decisions\": self._build_resume_decisions(decision, action_requests)}\n )\n stream = adapt_graph_events_to_executor_shape(\n agent.astream_events(stream_input, config=agent_config, version=\"v2\")\n )\n try:\n result = await process_agent_events(\n stream,\n agent_message,\n cast(\"SendMessageFunctionType\", self.send_message),\n on_token_callback,\n get_pending_interrupt=get_pending_interrupt,\n )\n except AgentPausedError as e:\n # Why: retract the empty partial bubble (leaks as \"[]\"); the HITL card supersedes it, resume re-emits it.\n paused_message = e.agent_message or agent_message\n msg_id = paused_message.get_id()\n if msg_id:\n await delete_message(id_=msg_id)\n await self._send_message_event(paused_message, category=\"remove_message\")\n return self._suspend_for_tool_approval(e.request, paused_message)\n except ExceptionWithMessageError as e:\n # Drop the half-stored partial message from the DB (only if it was\n # actually persisted) and tell the frontend to remove the stale bubble.\n if hasattr(e, \"agent_message\"):\n msg_id = e.agent_message.get_id()\n if msg_id:\n await delete_message(id_=msg_id)\n await self._send_message_event(e.agent_message, category=\"remove_message\")\n # exception(), not error(f\"...{e}\"): this class's str() interpolates the model's\n # partial completion, and passing the exception rather than formatting it is what\n # gives the exported record an error.type to triage on.\n logger.exception(\"Agent run failed after a partial message was emitted\")\n raise\n\n usage_data = token_usage_handler.get_usage()\n if usage_data:\n self._token_usage = usage_data\n result.properties.usage = usage_data\n # Only round-trip the DB when the message was stored (Chat Output wired).\n # `_should_skip_message=True` leaves `result.get_id()` empty; persisting\n # then would create a phantom row.\n if result.get_id():\n stored_result = await self._update_stored_message(result)\n await self._send_message_event(stored_result)\n result = stored_result\n\n self.status = result\n return result\n\n def _build_initial_agent_message(self) -> Message:\n \"\"\"Construct the placeholder agent Message that `process_agent_events` mutates.\"\"\"\n if hasattr(self, \"graph\"):\n session_id = self.graph.session_id\n elif hasattr(self, \"_session_id\"):\n session_id = self._session_id\n else:\n session_id = None\n\n sender_name = get_chat_output_sender_name(self) or self.display_name or \"AI\"\n return Message(\n sender=MESSAGE_SENDER_AI,\n sender_name=sender_name,\n properties={\"icon\": \"Bot\", \"state\": \"partial\"},\n # `text=\"\"` sentinel so MessageTable's no_content check accepts\n # an in-flight agent message whose content_blocks haven't been\n # populated yet. Mirrors ChatInput's convention.\n text=\"\",\n # Flat chronological event log; see lfx.base.agents.events.\n content_blocks=[],\n session_id=session_id or uuid.uuid4(),\n )\n\n def _selected_model_remediation_context(self) -> tuple[str | None, str | None, Any | None]:\n \"\"\"Return provider/name plus a connected model target, when present.\"\"\"\n try:\n selected = self._resolve_selected_model()\n if isinstance(selected, list) and selected and isinstance(selected[0], dict):\n return selected[0].get(\"provider\"), selected[0].get(\"name\"), None\n\n from langchain_core.language_models import BaseLanguageModel\n\n if isinstance(selected, BaseLanguageModel):\n model_name = None\n for attr in (\"model_name\", \"model\", \"model_id\"):\n value = getattr(selected, attr, None)\n if isinstance(value, str) and value:\n model_name = value\n break\n return self._connected_model_provider(selected), model_name, selected\n except (AttributeError, TypeError, ValueError, KeyError, ImportError):\n pass\n return None, None, None\n\n def _connected_model_provider(self, model: Any) -> str | None:\n \"\"\"Resolve a connected model's provider from the source component.\n\n Runtime model classes are not provider identities: OpenAI, OpenRouter,\n and compatible endpoints can all produce ``ChatOpenAI``. The incoming\n model edge preserves the source component, so prefer its explicit\n provider override or selected-model metadata, then its provider display\n name. If the Agent is used without graph provenance, leave the provider\n unknown so provider-scoped remediations remain disabled.\n \"\"\"\n vertex = getattr(self, \"_vertex\", None)\n if vertex is None:\n return None\n source_id = vertex.get_incoming_edge_by_target_param(\"model\")\n if not source_id:\n return None\n source = vertex.graph.get_vertex(source_id)\n component = getattr(source, \"custom_component\", None)\n if component is None:\n return None\n\n candidate = getattr(component, \"provider\", None)\n\n if not isinstance(candidate, str) or not candidate:\n selected_model = getattr(component, \"model\", None)\n if isinstance(selected_model, list) and selected_model and isinstance(selected_model[0], dict):\n candidate = selected_model[0].get(\"provider\")\n\n if not isinstance(candidate, str) or not candidate:\n candidate = getattr(component, \"display_name\", None)\n if not isinstance(candidate, str) or not candidate:\n return None\n\n from lfx.base.models.unified_models.provider_queries import get_model_provider_metadata\n\n provider_metadata = get_model_provider_metadata().get(candidate, {})\n expected_class = provider_metadata.get(\"mapping\", {}).get(\"model_class\")\n model_classes = {base.__name__ for base in type(model).__mro__}\n return candidate if expected_class in model_classes else None\n\n async def _run_agent_with_model_remediation(\n self,\n run_once: Callable[[], Awaitable[Message]],\n ) -> Message:\n \"\"\"Run one Agent operation, retrying safe provider-validation failures.\n\n Registered remediations must identify request-validation failures that\n occur before tool execution. A failure that can happen after a tool runs\n must instead retry at the model-call boundary to avoid repeating tool\n side effects.\n \"\"\"\n from lfx.base.models.model_remediation import apply_overrides_to_model, find_remediation, remember\n\n provider, model_name, connected_model = self._selected_model_remediation_context()\n applied: set[str] = set()\n while True:\n try:\n result = await run_once()\n except (ValueError, TypeError, KeyError):\n await logger.aexception(\"Agent run failed\")\n raise\n except Exception as exc:\n # run_agent may wrap the provider error in ExceptionWithMessageError, whose\n # str() carries the model's partial completion. Matching on it locally is fine;\n # logging it is not, so the record below passes the exception instead.\n error_text = f\"{exc} {getattr(exc, '__cause__', '') or ''}\"\n remediation = find_remediation(error_text, provider, already_applied=applied)\n if remediation is None:\n await logger.aexception(\"Agent run failed with no remediation available\")\n raise\n if connected_model is not None and not apply_overrides_to_model(connected_model, remediation.overrides):\n await logger.aerror(\n f\"model.remediation.unapplied name={remediation.name} provider={provider} model={model_name}\"\n )\n raise\n applied.add(remediation.name)\n if connected_model is None:\n self._model_overrides = {\n **(getattr(self, \"_model_overrides\", None) or {}),\n **remediation.overrides,\n }\n else:\n # Connected outputs are shared objects across downstream flow\n # branches. The mutation is intentionally flow-scoped: sibling\n # branches holding this model will see the matched override too.\n await logger.adebug(\n f\"model.remediation.shared_model name={remediation.name} provider={provider} model={model_name}\"\n )\n await logger.awarning(\n f\"model.remediation.applied name={remediation.name} provider={provider} model={model_name}\"\n )\n else:\n if connected_model is None and getattr(self, \"_model_overrides\", None):\n remember(provider, model_name, self._model_overrides)\n return result\n\n async def message_response(self) -> Message:\n async def _run_once() -> Message:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n self.set(\n llm=llm_model,\n tools=self.tools or [],\n chat_history=self.chat_history,\n input_value=self.input_value,\n system_prompt=self._inject_dynamic_prompt_values(self.system_prompt),\n )\n agent = self.create_agent_runnable()\n return await self.run_agent(agent)\n\n result = await self._run_agent_with_model_remediation(_run_once)\n self._agent_result = result\n return result\n\n async def json_response(self) -> Data:\n \"\"\"Produce structured Data via native LLM structured output, with prompt-based fallback.\n\n Native path (no tools, llm has with_structured_output) bypasses the agent loop and\n returns provider-validated JSON. When tools are attached, falls back to running the\n agent with a schema-augmented system prompt and parsing the final message content.\n \"\"\"\n from lfx.components.models_and_agents.structured_output.structured_output_orchestrator import (\n orchestrate_structured_output,\n )\n\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n except (ValueError, TypeError) as exc:\n await logger.aexception(\"json_response.requirements_failed\")\n return Data(data={\"content\": \"\", \"error\": str(exc)})\n\n injected_system_prompt = self._inject_dynamic_prompt_values(getattr(self, \"system_prompt\", \"\") or \"\") or \"\"\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n output_schema = getattr(self, \"output_schema\", None) or []\n has_tools = bool(self.tools)\n\n async def _run_agent_for_fallback(augmented_prompt: str) -> str:\n first_attempt = True\n\n async def _run_once() -> Message:\n nonlocal first_attempt, llm_model\n if first_attempt:\n first_attempt = False\n else:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n self.set(\n llm=llm_model,\n tools=self.tools or [],\n chat_history=self.chat_history,\n input_value=self.input_value,\n system_prompt=augmented_prompt,\n )\n # Structured output cannot suspend mid-parse: disable tool-approval interrupts.\n agent_runnable = self.create_agent_runnable(allow_interrupts=False)\n return await self.run_agent(agent_runnable)\n\n with _suppress_send_message(self):\n result = await self._run_agent_with_model_remediation(_run_once)\n return _extract_text_content(result)\n\n try:\n return await orchestrate_structured_output(\n llm=llm_model,\n output_schema=output_schema,\n system_prompt=injected_system_prompt,\n format_instructions=format_instructions,\n input_value=_extract_text_content(self.input_value),\n run_prompt_fallback=_run_agent_for_fallback,\n prefer_native=not has_tools,\n )\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as exc:\n await logger.aexception(\"json_response.orchestration_failed\")\n return Data(data={\"content\": \"\", \"error\": str(exc)})\n\n async def get_memory_data(self):\n # Scope by flow_id so default playground session names (e.g. \"New Session 0\")\n # cannot leak chat history across unrelated flows. See issue #13059.\n # The helper also returns [] when n_messages == 0, preserving the\n # explicit \"memory disabled\" contract from MemoryComponent.retrieve_messages.\n messages = await aget_agent_chat_history(\n session_id=self.graph.session_id,\n flow_id=getattr(self.graph, \"flow_id\", None),\n context_id=self.context_id,\n n_messages=self.n_messages,\n user_id=_safe_graph_user_id(self),\n )\n return [\n message for message in messages if getattr(message, \"id\", None) != getattr(self.input_value, \"id\", None)\n ]\n\n def update_input_types(self, build_config: dotdict) -> dotdict:\n \"\"\"Update input types for all fields in build_config.\"\"\"\n for key, value in build_config.items():\n if isinstance(value, dict):\n if value.get(\"input_types\") is None:\n build_config[key][\"input_types\"] = []\n elif hasattr(value, \"input_types\") and value.input_types is None:\n value.input_types = []\n return build_config\n\n async def update_build_config(\n self,\n build_config: dotdict,\n field_value: list[dict],\n field_name: str | None = None,\n ) -> dotdict:\n from lfx.services.model_provider_policy import ModelProviderPolicyPurpose\n\n policy_parameters = dict(getattr(self, \"_parameters\", {}) or {})\n if field_name:\n policy_parameters[field_name] = field_value\n await self.arequire_model_provider_policy(\n ModelProviderPolicyPurpose.CONFIGURE,\n parameters=policy_parameters,\n )\n # Update model options with caching (for all field changes).\n # The tool-calling constraint lives on the ModelInput's ``filters``\n # field (declared above); ``handle_model_input_update`` reads it\n # and applies the filter to both the dropdown options and the\n # sticky-default re-injection path.\n build_config = handle_model_input_update(\n component=self,\n build_config=dict(build_config),\n field_value=field_value,\n field_name=field_name,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n # Validate required keys. `verbose` was dropped from the input set\n # (see `_agent_base_inputs` \u2014 the create_agent event stream already\n # surfaces every step), so it is intentionally NOT required here.\n # Saved flows that still carry a `verbose` value just ignore it on\n # load.\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"add_calculator_tool\",\n \"system_prompt\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n ]\n missing_keys = [key for key in default_keys if key not in build_config]\n if missing_keys:\n msg = f\"Missing required keys in build_config: {missing_keys}\"\n raise ValueError(msg)\n await self._filter_legacy_provider_options(build_config)\n return dotdict({k: v.to_dict() if hasattr(v, \"to_dict\") else v for k, v in build_config.items()})\n\n async def _get_tools(self) -> list[Tool]:\n component_toolkit = get_component_toolkit()\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n # here we do not use the shared callbacks as we are exposing the agent as a tool\n callbacks=self.get_langchain_callbacks(),\n )\n if hasattr(self, \"tools_metadata\"):\n tools = component_toolkit(component=self, metadata=self.tools_metadata).update_tools_metadata(tools=tools)\n\n return tools\n"
},
"context_id": {
"_input_type": "MessageTextInput",
"advanced": true,
"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": ""
},
"format_instructions": {
"_input_type": "MultilineInput",
"advanced": true,
"ai_enabled": false,
"copy_field": false,
"display_name": "Output Format Instructions",
"dynamic": false,
"info": "Generic Template for structured output formatting. Valid only with Structured response.",
"input_types": [
"Message"
],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"multiline": true,
"name": "format_instructions",
"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": "You are an AI that extracts structured JSON objects from unstructured text. Use a predefined schema with expected types (str, int, float, bool, dict). Extract ALL relevant instances that match the schema - if multiple patterns exist, capture them all. Fill missing or ambiguous values with defaults: null for missing values. Remove exact duplicates but keep variations that have different field values. Always return valid JSON in the expected format, never throw errors. If multiple objects can be extracted, return them all in the structured format."
},
"handle_parsing_errors": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Handle Parse Errors",
"dynamic": false,
"info": "Adds tool-execution retry as a safety net. `create_agent` already feeds tool-call validation errors back to the LLM automatically; this flag layers `ToolRetryMiddleware` on top so transient tool runtime failures are retried (max 2 retries).",
"input_types": [],
"list": false,
"list_add_label": "Add More",
"name": "handle_parsing_errors",
"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
},
"input_value": {
"_input_type": "MessageInput",
"advanced": false,
"display_name": "Input",
"dynamic": false,
"info": "The input provided by the user for the agent to process.",
"input_types": [
"Message"
],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"name": "input_value",
"override_skip": false,
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": true,
"trace_as_input": true,
"trace_as_metadata": true,
"track_in_telemetry": false,
"type": "str",
"value": ""
},
"max_iterations": {
"_input_type": "IntInput",
"advanced": true,
"display_name": "Max Iterations",
"dynamic": false,
"info": "Maximum number of model calls the agent can make before stopping (maps to `ModelCallLimitMiddleware.run_limit` on the create_agent path). Must be at least 1 \u2014 it is a safety cap, never 'unlimited'.",
"input_types": [],
"list": false,
"list_add_label": "Add More",
"name": "max_iterations",
"override_skip": false,
"placeholder": "",
"range_spec": {
"max": 128000,
"min": 1,
"step": 1,
"step_type": "int"
},
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"track_in_telemetry": true,
"type": "int",
"value": 15
},
"max_tokens": {
"_input_type": "IntInput",
"advanced": true,
"display_name": "Max Tokens",
"dynamic": false,
"info": "Maximum number of tokens to generate. Set to 0 for no explicit limit. Field name varies by provider.",
"input_types": [],
"list": false,
"list_add_label": "Add More",
"name": "max_tokens",
"override_skip": false,
"placeholder": "",
"range_spec": {
"max": 128000,
"min": 1,
"step": 1,
"step_type": "int"
},
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"track_in_telemetry": true,
"type": "int",
"value": 0
},
"model": {
"_input_type": "ModelInput",
"advanced": false,
"display_name": "Language Model",
"dynamic": false,
"external_options": {
"fields": {
"data": {
"node": {
"display_name": "Connect other models",
"icon": "CornerDownLeft",
"name": "connect_other_models"
}
}
}
},
"filters": {
"tool_calling": true
},
"info": "Select your model provider",
"input_types": [
"LanguageModel"
],
"list": false,
"list_add_label": "Add More",
"model_type": "language",
"name": "model",
"options": [
{
"icon": "GoogleGenerativeAI",
"metadata": {
"context_window": 1048576,
"cost_per_million_in": 1.5,
"cost_per_million_out": 9,
"created": 1779148800,
"default": true,
"deprecated": false,
"icon": "Google Generative AI",
"model_type": "llm",
"not_enabled_locally": true,
"not_supported": false,
"preview": false,
"reasoning": true,
"search": false,
"tool_calling": true,
"vision": true
},
"name": "gemini-3.5-flash",
"provider": "Google Generative AI"
}
],
"override_skip": false,
"placeholder": "Setup Provider",
"real_time_refresh": true,
"refresh_button": true,
"required": true,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_input": true,
"track_in_telemetry": false,
"type": "model",
"value": ""
},
"n_messages": {
"_input_type": "IntInput",
"advanced": true,
"display_name": "Number of Chat History Messages",
"dynamic": false,
"info": "Number of chat history messages to retrieve.",
"input_types": [],
"list": false,
"list_add_label": "Add More",
"name": "n_messages",
"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": 100
},
"output_schema": {
"_input_type": "TableInput",
"advanced": true,
"display_name": "Output Schema",
"dynamic": false,
"info": "Schema Validation: Define the structure and data types for structured output. No validation if no output schema.",
"input_types": [
"DataFrame",
"Table"
],
"is_list": true,
"list_add_label": "Add More",
"name": "output_schema",
"override_skip": false,
"placeholder": "",
"required": false,
"show": true,
"table_icon": "Table",
"table_schema": [
{
"default": "field",
"description": "Specify the name of the output field.",
"display_name": "Name",
"edit_mode": "inline",
"formatter": "text",
"name": "name",
"type": "str"
},
{
"default": "description of field",
"description": "Describe the purpose of the output field.",
"display_name": "Description",
"edit_mode": "popover",
"formatter": "text",
"name": "description",
"type": "str"
},
{
"default": "str",
"description": "Indicate the data type of the output field (e.g., str, int, float, bool, dict).",
"display_name": "Type",
"edit_mode": "inline",
"formatter": "text",
"name": "type",
"options": [
"str",
"int",
"float",
"bool",
"dict"
],
"type": "str"
},
{
"default": "False",
"description": "Set to True if this output field should be a list of the specified type.",
"display_name": "As List",
"edit_mode": "inline",
"formatter": "text",
"name": "multiple",
"type": "boolean"
}
],
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"track_in_telemetry": false,
"trigger_icon": "Table",
"trigger_text": "Open table",
"type": "table",
"value": []
},
"project_id": {
"_input_type": "StrInput",
"advanced": false,
"display_name": "watsonx Project ID",
"dynamic": false,
"info": "The project ID associated with the foundation model (IBM watsonx.ai only)",
"input_types": [],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"name": "project_id",
"override_skip": false,
"placeholder": "",
"required": false,
"show": false,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"track_in_telemetry": false,
"type": "str",
"value": ""
},
"stream": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Stream",
"dynamic": false,
"info": "Stream the response from the model. Streaming works only in Chat.",
"input_types": [],
"list": false,
"list_add_label": "Add More",
"name": "stream",
"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
},
"system_prompt": {
"_input_type": "MultilineInput",
"advanced": false,
"ai_enabled": false,
"copy_field": false,
"display_name": "Agent Instructions",
"dynamic": false,
"info": "System Prompt: Initial instructions and context provided to guide the agent's behavior. Supports dynamic placeholders: {current_date}, {model_name}, {optional_user_context}.",
"input_types": [
"Message"
],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"multiline": true,
"name": "system_prompt",
"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": "You are a Langflow custom-component generator. Given the user's request, write a complete, valid Langflow custom Component in Python: a class subclassing Component with inputs, outputs, and the method(s) that implement the outputs, following Langflow's conventions.\n\nUse the URL tool to fetch Langflow's reference code whenever you need the exact API/conventions. Fetch the references that match what you're building:\n- Component base class: https://raw.githubusercontent.com/langflow-ai/langflow/refs/heads/main/src/backend/base/langflow/custom/custom_component/component.py\n- Custom component example: https://raw.githubusercontent.com/langflow-ai/langflow/refs/heads/main/src/backend/base/langflow/components/custom_component/custom_component.py\n- Example components (inputs/outputs/tool patterns): structured output https://raw.githubusercontent.com/langflow-ai/langflow/refs/heads/main/src/lfx/src/lfx/components/llm_operations/structured_output.py , calculator https://raw.githubusercontent.com/langflow-ai/langflow/refs/heads/main/src/lfx/src/lfx/components/tools/calculator.py , file https://raw.githubusercontent.com/langflow-ai/langflow/refs/heads/main/src/lfx/src/lfx/components/files_and_knowledge/file.py\n\nUse your memory tool to recall earlier turns when the user iterates on a component. Return only the Python code unless the user asks for an explanation."
},
"tools": {
"_input_type": "HandleInput",
"advanced": false,
"display_name": "Tools",
"dynamic": false,
"info": "These are the tools that the agent can use to help with tasks.",
"input_types": [
"Tool"
],
"list": true,
"list_add_label": "Add More",
"name": "tools",
"override_skip": false,
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"trace_as_metadata": true,
"track_in_telemetry": false,
"type": "other",
"value": ""
}
},
"tool_mode": false
},
"selected_output": "response",
"showNode": true,
"type": "Agent"
},
"dragging": false,
"id": "Agent-I0nts",
"measured": {
"height": 431,
"width": 320
},
"position": {
"x": 2595.812486589649,
"y": 559.0945152239169
},
"positionAbsolute": {
"x": 2595.812486589649,
"y": 559.0945152239169
},
"selected": false,
"type": "genericNode"
},
{
"data": {
"description": "Fetch content from one or more web pages, following links recursively.",
"display_name": "URL",
"id": "URLComponent-P2u21",
"node": {
"base_classes": [
"Message",
"Table"
],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
"description": "Fetch content from one or more web pages, following links recursively.",
"display_name": "URL",
"documentation": "https://docs.langflow.org/url",
"edited": false,
"field_order": [
"urls",
"max_depth",
"prevent_outside",
"use_async",
"follow_redirects",
"format",
"timeout",
"headers",
"filter_text_html",
"continue_on_failure",
"check_response_status",
"autoset_encoding"
],
"frozen": false,
"icon": "layout-template",
"legacy": false,
"metadata": {
"code_hash": "a6429c7f9670",
"component_class_name": "URLComponent",
"dependencies": {
"dependencies": [
{
"name": "httpx",
"version": "0.28.1"
},
{
"name": "bs4",
"version": "4.15.0"
},
{
"name": "markitdown",
"version": "0.1.6"
},
{
"name": "lfx",
"version": null
}
],
"total_dependencies": 4
},
"module": "lfx.components.data_source.url.URLComponent"
},
"minimized": false,
"output_types": [],
"outputs": [
{
"allows_loop": false,
"cache": true,
"display_name": "Toolset",
"group_outputs": false,
"hidden": null,
"loop_types": null,
"method": "to_toolkit",
"name": "component_as_tool",
"options": null,
"required_inputs": null,
"selected": "Tool",
"tool_mode": true,
"types": [
"Tool"
],
"value": "__UNDEFINED__"
}
],
"pinned": false,
"template": {
"_type": "Component",
"autoset_encoding": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Autoset Encoding",
"dynamic": false,
"info": "If enabled, automatically sets the encoding of the request.",
"list": false,
"list_add_label": "Add More",
"name": "autoset_encoding",
"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
},
"check_response_status": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Check Response Status",
"dynamic": false,
"info": "If enabled, checks the response status of the request.",
"list": false,
"list_add_label": "Add More",
"name": "check_response_status",
"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
},
"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 importlib.util\nimport io\nimport os\nimport re\nimport zlib\nfrom urllib.parse import urljoin, urlparse\n\nimport httpx\nfrom bs4 import BeautifulSoup\nfrom markitdown import MarkItDown\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.helpers.data import safe_convert\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, Output, SliderInput, TableInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.services.deps import get_settings_service\nfrom lfx.utils.request_utils import get_user_agent\nfrom lfx.utils.ssrf_protection import SSRFProtectionError, is_ssrf_protection_enabled, validate_and_resolve_url\nfrom lfx.utils.ssrf_transport import create_ssrf_protected_client, pin_host_for_url\n\n# Constants\nDEFAULT_TIMEOUT = 30\nDEFAULT_MAX_DEPTH = 1\nDEFAULT_FORMAT = \"Text\"\n\n# HTTP status codes that carry a redirect Location header (RFC 9110).\nREDIRECT_STATUS_CODES = frozenset({301, 302, 303, 307, 308})\n\n# Maximum number of redirects to follow. Each hop is re-validated for SSRF safety;\n# matches httpx's default and the API Request component.\nMAX_REDIRECTS = 20\n\n# Default ports per scheme, used to compare redirect origins.\nDEFAULT_SCHEME_PORTS = {\"http\": 80, \"https\": 443}\n\n# Memory bounds for untrusted responses. Bodies are streamed and abandoned once the\n# per-response cap or the budget shared by every page of one fetch (all URLs and crawled\n# links) is exceeded, so a hostile server cannot exhaust memory with a huge or endless body.\n# Configurable via LANGFLOW_URL_COMPONENT_MAX_RESPONSE_BYTES / LANGFLOW_URL_COMPONENT_MAX_TOTAL_BYTES\n# (see RuntimeSettings); these are only the fallback used when the settings service isn't available.\nFALLBACK_MAX_RESPONSE_BYTES = 10 * 1024 * 1024\nFALLBACK_MAX_TOTAL_BYTES = 100 * 1024 * 1024\n\n# Only advertise codings we can decode with a strict output limit. Raw response bytes\n# are also capped before decoding, including compressed headers and trailing data.\nACCEPT_ENCODING = \"gzip, deflate\"\nALLOWED_CONTENT_ENCODINGS = frozenset({\"gzip\", \"deflate\"})\nZLIB_HEADER_BYTES = 2\n\n\nURL_REGEX = re.compile(\n r\"^(https?:\\/\\/)?\" r\"(www\\.)?\" r\"([a-zA-Z0-9.-]+)\" r\"(\\.[a-zA-Z]{2,})?\" r\"(:\\d+)?\" r\"(\\/[^\\s]*)?$\",\n re.IGNORECASE,\n)\n\nUSER_AGENT = None\n# Check if langflow is installed using importlib.util.find_spec(name))\nif importlib.util.find_spec(\"langflow\"):\n langflow_installed = True\n USER_AGENT = get_user_agent()\nelse:\n langflow_installed = False\n USER_AGENT = \"lfx\"\n\n\nclass URLComponent(Component):\n \"\"\"A component that loads and parses content from web pages recursively.\n\n This component allows fetching content from one or more URLs, with options to:\n - Control crawl depth\n - Prevent crawling outside the root domain\n - Use async loading for better performance\n - Extract either raw HTML or clean text\n - Configure request headers and timeouts\n \"\"\"\n\n display_name = \"URL\"\n description = \"Fetch content from one or more web pages, following links recursively.\"\n documentation: str = \"https://docs.langflow.org/url\"\n icon = \"layout-template\"\n name = \"URLComponent\"\n # Body bytes the current fetch may still read; reset by ``fetch_url_contents``.\n _bytes_remaining = FALLBACK_MAX_TOTAL_BYTES\n # Per-response and total byte caps for the current fetch; resolved from settings by\n # ``fetch_url_contents``. Kept as fallbacks here so the class works if that never runs.\n _max_response_bytes = FALLBACK_MAX_RESPONSE_BYTES\n _max_total_bytes = FALLBACK_MAX_TOTAL_BYTES\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs to crawl recursively, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n placeholder=\"Enter a URL...\",\n list_add_label=\"Add URL\",\n input_types=[\"Message\"],\n ),\n SliderInput(\n name=\"max_depth\",\n display_name=\"Depth\",\n info=(\n \"Controls how many 'clicks' away from the initial page the crawler will go:\\n\"\n \"- depth 1: only the initial page\\n\"\n \"- depth 2: initial page + all pages linked directly from it\\n\"\n \"- depth 3: initial page + direct links + links found on those direct link pages\\n\"\n \"Note: This is about link traversal, not URL path depth.\"\n ),\n value=DEFAULT_MAX_DEPTH,\n range_spec=RangeSpec(min=1, max=5, step=1),\n required=False,\n min_label=\" \",\n max_label=\" \",\n min_label_icon=\"None\",\n max_label_icon=\"None\",\n # slider_input=True\n ),\n BoolInput(\n name=\"prevent_outside\",\n display_name=\"Prevent Outside\",\n info=(\n \"If enabled, only crawls URLs within the same domain as the root URL. \"\n \"This helps prevent the crawler from going to external websites.\"\n ),\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"use_async\",\n display_name=\"Use Async\",\n info=(\n \"If enabled, uses asynchronous loading which can be significantly faster \"\n \"but might use more system resources.\"\n ),\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"follow_redirects\",\n display_name=\"Follow Redirects\",\n info=(\n \"If enabled, follows HTTP redirects such as http\u2192https or www/non-www \"\n \"normalization, which most sites rely on to serve their content. When SSRF \"\n \"protection is enabled (the default), every redirect hop is re-validated against \"\n \"the same blocked-IP denylist and DNS-pinned before it is fetched, so following \"\n \"redirects cannot be used to reach internal resources. Disable to capture the \"\n \"first response as-is without following redirects.\"\n ),\n value=True,\n required=False,\n advanced=True,\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=(\n \"Output Format. Use 'Text' to extract the text from the HTML, \"\n \"'Markdown' to parse the HTML into Markdown format, or 'HTML' \"\n \"for the raw HTML content.\"\n ),\n options=[\"Text\", \"HTML\", \"Markdown\"],\n value=DEFAULT_FORMAT,\n advanced=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Timeout for the request in seconds.\",\n value=DEFAULT_TIMEOUT,\n required=False,\n advanced=True,\n ),\n TableInput(\n name=\"headers\",\n display_name=\"Headers\",\n info=\"The headers to send with the request\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Header\",\n \"type\": \"str\",\n \"description\": \"Header name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Header value\",\n },\n ],\n value=[{\"key\": \"User-Agent\", \"value\": USER_AGENT}],\n advanced=True,\n input_types=[\"DataFrame\", \"Table\"],\n ),\n BoolInput(\n name=\"filter_text_html\",\n display_name=\"Filter Text/HTML\",\n info=\"If enabled, filters out text/css content type from the results.\",\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"continue_on_failure\",\n display_name=\"Continue on Failure\",\n info=\"If enabled, continues crawling even if some requests fail.\",\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"check_response_status\",\n display_name=\"Check Response Status\",\n info=\"If enabled, checks the response status of the request.\",\n value=False,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"autoset_encoding\",\n display_name=\"Autoset Encoding\",\n info=\"If enabled, automatically sets the encoding of the request.\",\n value=True,\n required=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Extracted Pages\", name=\"page_results\", method=\"fetch_content\"),\n Output(display_name=\"Raw Content\", name=\"raw_results\", method=\"fetch_content_as_message\", tool_mode=False),\n ]\n\n @staticmethod\n def _html_extractor(x: str) -> str:\n \"\"\"Extract raw HTML content.\"\"\"\n return x\n\n @staticmethod\n def _text_extractor(x: str) -> str:\n \"\"\"Extract clean text from HTML.\"\"\"\n return BeautifulSoup(x, \"lxml\").get_text()\n\n @staticmethod\n def _markdown_extractor(x: str) -> str:\n \"\"\"Convert HTML to Markdown format.\"\"\"\n stream = io.BytesIO(x.encode(\"utf-8\"))\n result = MarkItDown(enable_plugins=False).convert_stream(stream)\n return result.markdown\n\n @staticmethod\n def validate_url(url: str) -> bool:\n \"\"\"Validates if the given string matches URL pattern.\n\n Args:\n url: The URL string to validate\n\n Returns:\n bool: True if the URL is valid, False otherwise\n \"\"\"\n return bool(URL_REGEX.match(url))\n\n def ensure_url(self, url: str) -> tuple[str, list[str]]:\n \"\"\"Ensures the given string is a valid URL and returns validated IPs for DNS pinning.\n\n Args:\n url: The URL string to validate and normalize\n\n Returns:\n tuple[str, list[str]]: The normalized URL and list of validated IPs for DNS pinning\n\n Raises:\n ValueError: If the URL is invalid or blocked by SSRF protection\n \"\"\"\n url = url.strip()\n if not url.startswith((\"http://\", \"https://\")):\n url = \"https://\" + url\n\n if not self.validate_url(url):\n msg = f\"Invalid URL: {url}\"\n raise ValueError(msg)\n\n # ============================================================================\n # SSRF Protection with DNS Pinning\n # ============================================================================\n # This prevents DNS rebinding attacks by:\n # 1. Resolving DNS and validating IPs during security check\n # 2. Returning the validated IP addresses for DNS pinning\n # 3. Using a custom HTTP transport that forces use of the pinned IPs\n # 4. Ignoring any new DNS resolutions (prevents rebinding)\n #\n # Without DNS pinning, an attacker could:\n # - First DNS lookup: returns public IP (passes validation)\n # - Second DNS lookup: returns internal IP (bypasses protection)\n # - Attack succeeds: accesses internal services\n #\n # With DNS pinning:\n # - First DNS lookup: returns public IP (passes validation)\n # - IPs are pinned and returned\n # - HTTP request: uses pinned IPs directly (no new DNS lookup)\n # - Attack fails: even if DNS changes, we use the validated IPs\n # ============================================================================\n try:\n _validated_url, validated_ips = validate_and_resolve_url(url)\n except SSRFProtectionError as e:\n msg = f\"SSRF Protection: {e}\"\n raise ValueError(msg) from e\n else:\n # Log DNS pinning information for security auditing\n if validated_ips:\n logger.debug(f\"SSRF Protection: Using DNS pinning with {len(validated_ips)} validated IP(s) for {url}\")\n\n return url, validated_ips\n\n def _build_http_client(self, url: str, validated_ips: list[str]) -> httpx.AsyncClient:\n \"\"\"Create an HTTP client with DNS pinning for SSRF protection.\n\n Args:\n url: The request URL whose hostname will be pinned\n validated_ips: IPs validated by validate_and_resolve_url for this URL\n\n Returns:\n httpx.AsyncClient: A client with DNS pinning when SSRF protection is enabled\n \"\"\"\n hostname = pin_host_for_url(url) if is_ssrf_protection_enabled() and validated_ips else None\n if hostname:\n client = create_ssrf_protected_client(hostname=hostname, validated_ips=validated_ips)\n else:\n client = httpx.AsyncClient()\n # A client default, so a user-supplied Accept-Encoding header still takes precedence.\n client.headers[\"Accept-Encoding\"] = ACCEPT_ENCODING\n return client\n\n @staticmethod\n def _headers_for_redirect(headers: dict | None, current_url: str, next_url: str) -> dict | None:\n \"\"\"Drop sensitive headers when a redirect crosses to a different origin.\n\n Mirrors httpx's auto-follow behavior so manually following redirects does not\n leak credentials (Authorization / Cookie) to an origin other than the one the\n caller intended them for. Headers are kept only when the redirect stays on the\n same origin (scheme, host, port) or is a direct https upgrade of the same host\n on default ports - the exact cases where httpx keeps the Authorization header.\n \"\"\"\n if not headers:\n return headers\n current, nxt = urlparse(current_url), urlparse(next_url)\n current_port = current.port or DEFAULT_SCHEME_PORTS.get(current.scheme)\n next_port = nxt.port or DEFAULT_SCHEME_PORTS.get(nxt.scheme)\n same_origin = (current.scheme, current.hostname, current_port) == (nxt.scheme, nxt.hostname, next_port)\n https_upgrade = (\n current.hostname == nxt.hostname\n and current.scheme == \"http\"\n and nxt.scheme == \"https\"\n and current_port == DEFAULT_SCHEME_PORTS[\"http\"]\n and next_port == DEFAULT_SCHEME_PORTS[\"https\"]\n )\n if same_origin or https_upgrade:\n return headers\n sensitive = {\"authorization\", \"proxy-authorization\", \"cookie\"}\n return {k: v for k, v in headers.items() if k.lower() not in sensitive}\n\n @staticmethod\n def _resolve_byte_limit(env_var: str, setting_name: str, fallback: int) -> int:\n \"\"\"Resolve one byte cap from settings, or the environment when no service is available.\n\n The settings service applies Langflow's normal configuration precedence. Reading the\n process environment directly is only a fallback for standalone ``lfx`` execution.\n \"\"\"\n try:\n settings_service = get_settings_service()\n except Exception: # noqa: BLE001 - service registry may not be ready\n settings_service = None\n if settings_service is not None:\n return getattr(settings_service.settings, setting_name, fallback)\n\n env_value = os.getenv(env_var)\n if env_value is not None:\n try:\n value = int(env_value)\n except ValueError:\n logger.warning(f\"Ignoring invalid {env_var}={env_value!r}; using {fallback}\")\n return fallback\n if value > 0:\n return value\n logger.warning(f\"Ignoring non-positive {env_var}={env_value!r}; using {fallback}\")\n return fallback\n\n return fallback\n\n @classmethod\n def _resolve_byte_limits(cls) -> tuple[int, int]:\n \"\"\"Read the configurable per-response and total byte caps for the current fetch.\"\"\"\n return (\n cls._resolve_byte_limit(\n \"LANGFLOW_URL_COMPONENT_MAX_RESPONSE_BYTES\",\n \"url_component_max_response_bytes\",\n FALLBACK_MAX_RESPONSE_BYTES,\n ),\n cls._resolve_byte_limit(\n \"LANGFLOW_URL_COMPONENT_MAX_TOTAL_BYTES\",\n \"url_component_max_total_bytes\",\n FALLBACK_MAX_TOTAL_BYTES,\n ),\n )\n\n async def _read_bounded_text(self, response: httpx.Response) -> str:\n \"\"\"Read a streamed response body without exceeding the per-response or total byte budget.\n\n Encoded and decoded bytes are each bounded. The larger count is charged against the\n total budget even when the body is rejected, so a server that keeps answering with\n oversized bodies cannot make a crawl download without bound. Decoder errors charge\n the reserved output bound because zlib may discard output before raising.\n\n Raises:\n httpx.HTTPError: If the body is (or declares to be) larger than the remaining budget,\n or uses a content coding whose decompression cannot be bounded.\n \"\"\"\n codings = [c.strip().lower() for c in response.headers.get(\"content-encoding\", \"\").split(\",\")]\n codings = [c for c in codings if c not in {\"\", \"identity\"}]\n if len(codings) > 1 or (codings and codings[0] not in ALLOWED_CONTENT_ENCODINGS):\n msg = f\"Unsupported content encoding from {response.url}: {', '.join(codings)}\"\n raise httpx.HTTPError(msg)\n\n limit = min(self._max_response_bytes, self._bytes_remaining)\n too_large = f\"Response from {response.url} exceeds the {limit} byte limit\"\n declared = response.headers.get(\"content-length\", \"\")\n if declared.isdigit() and int(declared) > limit:\n raise httpx.HTTPError(too_large)\n\n body = bytearray()\n raw_bytes = decoded_bytes = 0\n coding = codings[0] if codings else None\n decompressor = (\n zlib.decompressobj(zlib.MAX_WBITS | 16 if coding == \"gzip\" else zlib.MAX_WBITS) if coding else None\n )\n first_decode = True\n deflate_prefix = b\"\"\n try:\n # HTTPX's automatic decoder can allocate unbounded output or retain an\n # endless tail in zlib.unused_data without yielding any decoded bytes.\n async for raw_chunk in response.aiter_raw():\n raw_bytes += len(raw_chunk)\n if raw_bytes > limit:\n raise httpx.HTTPError(too_large)\n chunk = raw_chunk\n if decompressor is not None:\n if decompressor.eof:\n msg = f\"Trailing data after compressed response from {response.url}\"\n raise httpx.DecodingError(msg)\n if coding == \"deflate\" and first_decode:\n # HTTP deflate may omit the zlib wrapper. Wait for enough\n # header bytes before trying the same fallback as HTTPX.\n chunk = deflate_prefix + chunk\n if len(chunk) < ZLIB_HEADER_BYTES:\n deflate_prefix = chunk\n continue\n max_output = limit - decoded_bytes + 1\n try:\n try:\n decoded = decompressor.decompress(chunk, max_output)\n except zlib.error:\n if coding != \"deflate\" or not first_decode:\n raise\n decompressor = zlib.decompressobj(-zlib.MAX_WBITS)\n decoded = decompressor.decompress(chunk, max_output)\n except zlib.error as exc:\n # zlib can produce output before detecting a bad checksum,\n # then raise without returning it. Charge the reserved bound.\n decoded_bytes = limit + 1\n msg = f\"Invalid compressed response from {response.url}\"\n raise httpx.DecodingError(msg) from exc\n first_decode = False\n decoded_bytes += len(decoded)\n if decoded_bytes > limit:\n raise httpx.HTTPError(too_large)\n if decompressor.unused_data:\n msg = f\"Trailing data after compressed response from {response.url}\"\n raise httpx.DecodingError(msg)\n chunk = decoded\n else:\n decoded_bytes += len(chunk)\n # Check decoded output before retaining it in the response body.\n body.extend(chunk)\n if decompressor is not None and not decompressor.eof:\n msg = f\"Incomplete compressed response from {response.url}\"\n raise httpx.DecodingError(msg)\n finally:\n # Charge encoded overhead as well as expanded output, including rejected\n # responses. Neither representation may bypass the shared fetch budget.\n self._bytes_remaining -= max(raw_bytes, decoded_bytes)\n # Same character decoding as ``response.text``. A complete zlib stream has\n # already emitted all its output, so no unbounded decoder flush is needed.\n return body.decode(response.encoding or \"utf-8\", errors=\"replace\")\n\n async def _process_response(self, response: httpx.Response) -> tuple[str, dict]:\n \"\"\"Turn a final (non-redirect) streamed response into its HTML content and metadata.\n\n Args:\n response: The HTTP response to process, opened with ``stream=True``\n\n Returns:\n tuple[str, dict]: The HTML content and metadata\n \"\"\"\n if self.check_response_status:\n response.raise_for_status()\n\n content_type = response.headers.get(\"content-type\", \"\").lower()\n\n # Filter out CSS files if requested\n if self.filter_text_html and \"text/css\" in content_type:\n logger.debug(f\"Skipping CSS file: {response.url}\")\n return \"\", {}\n\n # Get the HTML content\n html_content = await self._read_bounded_text(response)\n\n # Extract metadata\n metadata = {\n \"source\": str(response.url),\n \"title\": \"\",\n \"description\": \"\",\n \"content_type\": content_type,\n \"language\": \"\",\n }\n\n # Try to extract title and description from HTML\n try:\n soup = BeautifulSoup(html_content, \"lxml\")\n except Exception: # noqa: BLE001\n # Broad exception is acceptable here - metadata extraction is optional\n # and we don't want to fail the entire request if it fails\n logger.debug(f\"Failed to extract metadata from {response.url}\")\n else:\n if soup.title:\n metadata[\"title\"] = soup.title.string or \"\"\n\n # Try to get description from meta tags\n meta_desc = soup.find(\"meta\", attrs={\"name\": \"description\"})\n if meta_desc and meta_desc.get(\"content\"):\n metadata[\"description\"] = meta_desc[\"content\"]\n\n # Try to get language\n html_tag = soup.find(\"html\")\n if html_tag and html_tag.get(\"lang\"):\n metadata[\"language\"] = html_tag[\"lang\"]\n\n return html_content, metadata\n\n async def _fetch_with_revalidated_redirects(\n self, url: str, validated_ips: list[str], headers: dict\n ) -> tuple[str, dict]:\n \"\"\"Fetch ``url``, following redirects manually and re-validating every hop.\n\n This closes an SSRF bypass: with redirects enabled, httpx would otherwise\n auto-follow a redirect from a validated public URL to an internal address that\n the pinned transport never checked (the redirect target is a different host, so\n it is not in the pin map). Each redirect ``Location`` is resolved (relative\n locations included) and re-validated with ``ensure_url`` - the same private/\n loopback/link-local denylist and DNS pinning applied to the initial request -\n before any connection to it is made. A blocked hop raises ``ValueError`` (unless\n ``continue_on_failure``); the chain is capped at ``MAX_REDIRECTS`` hops.\n\n Args:\n url: The URL to fetch\n validated_ips: Validated IPs for DNS pinning of the initial URL\n headers: HTTP headers to send\n\n Returns:\n tuple[str, dict]: The HTML content and metadata\n \"\"\"\n current_url = url\n current_ips = validated_ips\n\n for _ in range(MAX_REDIRECTS + 1):\n # Stream so redirect bodies are never read and the final body is read bounded.\n async with (\n self._build_http_client(current_url, current_ips) as client,\n client.stream(\n \"GET\", current_url, headers=headers, timeout=self.timeout, follow_redirects=False\n ) as response,\n ):\n location = response.headers.get(\"location\")\n if response.status_code not in REDIRECT_STATUS_CODES or not location:\n # Not a redirect (or no Location header) - this is the final response.\n return await self._process_response(response)\n\n # Resolve relative redirects against the current URL.\n next_url = urljoin(current_url, location)\n\n # Re-validate the redirect target with the same SSRF denylist + DNS pinning.\n try:\n validated_next_url, current_ips = self.ensure_url(next_url)\n except (ValueError, SSRFProtectionError) as e:\n if self.continue_on_failure:\n logger.warning(f\"Skipping blocked or invalid redirect to {next_url}: {e}\")\n return \"\", {}\n msg = f\"SSRF Protection: blocked redirect to {next_url}: {e}\"\n raise ValueError(msg) from e\n\n headers = self._headers_for_redirect(headers, current_url, next_url)\n current_url = validated_next_url\n\n # Exhausted the redirect budget.\n if self.continue_on_failure:\n logger.warning(f\"Exceeded the maximum of {MAX_REDIRECTS} redirects while requesting {url}\")\n return \"\", {}\n msg = f\"SSRF Protection: exceeded the maximum of {MAX_REDIRECTS} redirects while requesting {url}\"\n raise ValueError(msg)\n\n async def _fetch_url_with_pinning(self, url: str, validated_ips: list[str], headers: dict) -> tuple[str, dict]:\n \"\"\"Fetch a single URL with DNS pinning protection, following redirects safely.\n\n Args:\n url: The URL to fetch\n validated_ips: Validated IPs for DNS pinning\n headers: HTTP headers to send\n\n Returns:\n tuple[str, dict]: The HTML content and metadata\n \"\"\"\n try:\n # When SSRF protection is enabled we must follow redirects manually so each hop\n # is re-validated and DNS-pinned; letting httpx auto-follow would connect to the\n # redirect target without pinning (different host, not in the pin map) and re-open\n # the SSRF hole that DNS pinning closes. With protection disabled there is no pin\n # to bypass, so redirects follow the requests httpx itself builds for each hop.\n if self.follow_redirects and is_ssrf_protection_enabled():\n return await self._fetch_with_revalidated_redirects(url, validated_ips, headers)\n\n async with self._build_http_client(url, validated_ips) as client:\n request = client.build_request(\"GET\", url, headers=headers, timeout=self.timeout)\n # httpx auto-follow reads every redirect body into memory without a limit, so hop\n # manually via ``next_request`` (same cookies, headers and redirect cap as httpx).\n for _ in range(MAX_REDIRECTS + 1):\n response = await client.send(request, stream=True, follow_redirects=False)\n try:\n if not (self.follow_redirects and response.next_request):\n return await self._process_response(response)\n request = response.next_request\n finally:\n await response.aclose()\n msg = \"Exceeded maximum allowed redirects.\"\n raise httpx.TooManyRedirects(msg, request=request)\n\n except httpx.HTTPError as e:\n if self.continue_on_failure:\n logger.warning(f\"Failed to fetch {url}: {e}\")\n return \"\", {}\n raise\n\n async def _crawl_recursive(\n self, start_url: str, validated_ips: list[str], headers: dict, visited: set, depth: int = 0\n ) -> list[dict]:\n \"\"\"Recursively crawl URLs with DNS pinning protection.\n\n Args:\n start_url: The URL to start crawling from\n validated_ips: Validated IPs for DNS pinning\n headers: HTTP headers to send\n visited: Set of already visited URLs\n depth: Current crawl depth\n\n Returns:\n list[dict]: List of documents with content and metadata\n \"\"\"\n if depth >= self.max_depth or start_url in visited:\n return []\n if self._bytes_remaining <= 0:\n logger.warning(f\"Skipping {start_url}: reached the {self._max_total_bytes} byte limit for this fetch\")\n return []\n\n visited.add(start_url)\n documents = []\n\n # Fetch the current URL\n html_content, metadata = await self._fetch_url_with_pinning(start_url, validated_ips, headers)\n\n # Redirects may have landed on a different canonical URL (http->https, www\n # normalization). Resolve relative links and the prevent_outside check against\n # the URL the content actually came from, and mark it visited so links back to\n # the canonical form are not re-crawled.\n base_url = metadata.get(\"source\") or start_url\n visited.add(base_url)\n\n if not html_content:\n return documents\n\n # Extract content based on format\n extractors = {\n \"HTML\": self._html_extractor,\n \"Markdown\": self._markdown_extractor,\n \"Text\": self._text_extractor,\n }\n extractor = extractors.get(self.format, self._text_extractor)\n extracted_content = extractor(html_content)\n\n # Add the document\n documents.append(\n {\n \"page_content\": extracted_content,\n \"metadata\": metadata,\n }\n )\n\n # If we haven't reached max depth, extract and follow links\n if depth < self.max_depth - 1:\n try:\n soup = BeautifulSoup(html_content, \"lxml\")\n links = soup.find_all(\"a\", href=True)\n\n for link in links:\n if self._bytes_remaining <= 0:\n break\n href = link[\"href\"]\n # Resolve relative URLs\n absolute_url = urljoin(base_url, href)\n\n # Skip if already visited\n if absolute_url in visited:\n continue\n\n # Check if we should prevent going outside the domain\n if self.prevent_outside:\n start_domain = urlparse(base_url).netloc\n link_domain = urlparse(absolute_url).netloc\n if start_domain != link_domain:\n continue\n\n # Validate and crawl the linked URL\n try:\n _, link_validated_ips = self.ensure_url(absolute_url)\n sub_docs = await self._crawl_recursive(\n absolute_url, link_validated_ips, headers, visited, depth + 1\n )\n documents.extend(sub_docs)\n except (ValueError, SSRFProtectionError) as e:\n if self.continue_on_failure:\n logger.warning(f\"Skipping {absolute_url}: {e}\")\n continue\n raise\n\n except Exception: # noqa: BLE001\n # Broad exception is acceptable here - link extraction is optional\n # and we don't want to fail the entire crawl if one page has issues\n logger.debug(f\"Failed to extract links from {start_url}\")\n\n return documents\n\n async def fetch_url_contents(self) -> list[dict]:\n \"\"\"Load documents from the configured URLs with SSRF protection and DNS pinning.\n\n This method implements comprehensive SSRF (Server-Side Request Forgery) protection\n using DNS pinning to prevent DNS rebinding attacks. Each URL is validated and its\n DNS resolution is pinned before any HTTP requests are made.\n\n Returns:\n list[dict]: List of documents with content and metadata\n\n Raises:\n ValueError: If no valid URLs are provided or if there's an error loading documents\n \"\"\"\n self._max_response_bytes, self._max_total_bytes = self._resolve_byte_limits()\n self._bytes_remaining = self._max_total_bytes\n try:\n # Validate all URLs and get their validated IPs for DNS pinning\n validated_urls = []\n first_validation_error: Exception | None = None\n for url in self.urls:\n if not url.strip():\n continue\n try:\n normalized_url, validated_ips = self.ensure_url(url)\n validated_urls.append((normalized_url, validated_ips))\n except (ValueError, SSRFProtectionError) as e:\n # Remember the first rejection so its real cause (for example an\n # SSRF block) can be surfaced if no URL survives validation,\n # instead of being hidden behind \"No valid URLs provided\".\n if first_validation_error is None:\n first_validation_error = e\n if self.continue_on_failure:\n logger.warning(f\"Skipping invalid URL {url}: {e}\")\n continue\n raise\n\n # Remove duplicates while preserving order\n seen = set()\n unique_validated_urls = []\n for url, ips in validated_urls:\n if url not in seen:\n seen.add(url)\n unique_validated_urls.append((url, ips))\n\n logger.debug(f\"Validated {len(unique_validated_urls)} unique URL(s)\")\n\n if not unique_validated_urls:\n # Surface the actual reason every candidate URL was rejected (e.g. an\n # SSRF block) rather than a generic message, so a security failure is\n # not silently swallowed when continue_on_failure is enabled.\n if first_validation_error is not None:\n raise first_validation_error\n msg = \"No valid URLs provided.\"\n raise ValueError(msg)\n\n # Prepare headers\n headers_dict = {header[\"key\"]: header[\"value\"] for header in self.headers if header[\"value\"] is not None}\n\n # Crawl all URLs\n all_docs = []\n for url, validated_ips in unique_validated_urls:\n logger.debug(f\"Crawling {url} with max_depth={self.max_depth}\")\n\n try:\n visited = set()\n docs = await self._crawl_recursive(url, validated_ips, headers_dict, visited, depth=0)\n\n if not docs:\n logger.warning(f\"No documents found for {url}\")\n continue\n\n logger.debug(f\"Found {len(docs)} document(s) from {url}\")\n all_docs.extend(docs)\n\n except httpx.HTTPError as e:\n if self.continue_on_failure:\n logger.warning(f\"Error loading documents from {url}: {e}\")\n continue\n msg = f\"Error loading documents from {url}: {e}\"\n raise ValueError(msg) from e\n\n if not all_docs:\n msg = \"No documents were successfully loaded from any URL\"\n raise ValueError(msg)\n\n # Convert to output format\n return [\n {\n \"text\": safe_convert(doc[\"page_content\"], clean_data=True),\n \"url\": doc[\"metadata\"].get(\"source\", \"\"),\n \"title\": doc[\"metadata\"].get(\"title\", \"\"),\n \"description\": doc[\"metadata\"].get(\"description\", \"\"),\n \"content_type\": doc[\"metadata\"].get(\"content_type\", \"\"),\n \"language\": doc[\"metadata\"].get(\"language\", \"\"),\n }\n for doc in all_docs\n ]\n\n except Exception as e:\n error_msg = e.message if hasattr(e, \"message\") else str(e)\n msg = f\"Error loading documents: {error_msg}\"\n logger.exception(msg)\n raise ValueError(msg) from e\n\n async def fetch_content(self) -> DataFrame:\n \"\"\"Convert the documents to a DataFrame.\"\"\"\n url_contents = await self.fetch_url_contents()\n return DataFrame(data=url_contents)\n\n async def fetch_content_as_message(self) -> Message:\n \"\"\"Convert the documents to a Message.\"\"\"\n url_contents = await self.fetch_url_contents()\n return Message(text=\"\\n\\n\".join([x[\"text\"] for x in url_contents]), data={\"data\": url_contents})\n"
},
"continue_on_failure": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Continue on Failure",
"dynamic": false,
"info": "If enabled, continues crawling even if some requests fail.",
"list": false,
"list_add_label": "Add More",
"name": "continue_on_failure",
"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
},
"filter_text_html": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Filter Text/HTML",
"dynamic": false,
"info": "If enabled, filters out text/css content type from the results.",
"list": false,
"list_add_label": "Add More",
"name": "filter_text_html",
"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
},
"follow_redirects": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Follow Redirects",
"dynamic": false,
"info": "If enabled, follows HTTP redirects such as http\u2192https or www/non-www normalization, which most sites rely on to serve their content. When SSRF protection is enabled (the default), every redirect hop is re-validated against the same blocked-IP denylist and DNS-pinned before it is fetched, so following redirects cannot be used to reach internal resources. Disable to capture the first response as-is without following redirects.",
"list": false,
"list_add_label": "Add More",
"name": "follow_redirects",
"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
},
"format": {
"_input_type": "DropdownInput",
"advanced": true,
"combobox": false,
"dialog_inputs": {},
"display_name": "Output Format",
"dynamic": false,
"external_options": {},
"info": "Output Format. Use 'Text' to extract the text from the HTML, 'Markdown' to parse the HTML into Markdown format, or 'HTML' for the raw HTML content.",
"name": "format",
"options": [
"Text",
"HTML",
"Markdown"
],
"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": "Text"
},
"headers": {
"_input_type": "TableInput",
"advanced": true,
"display_name": "Headers",
"dynamic": false,
"info": "The headers to send with the request",
"input_types": [
"DataFrame",
"Table"
],
"is_list": true,
"list_add_label": "Add More",
"load_from_db": false,
"name": "headers",
"override_skip": false,
"placeholder": "",
"required": false,
"show": true,
"table_icon": "Table",
"table_schema": [
{
"description": "Header name",
"display_name": "Header",
"name": "key",
"type": "str"
},
{
"description": "Header value",
"display_name": "Value",
"name": "value",
"type": "str"
}
],
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"track_in_telemetry": false,
"trigger_icon": "Table",
"trigger_text": "Open table",
"type": "table",
"value": [
{
"key": "User-Agent",
"value": "langflow"
}
]
},
"max_depth": {
"_input_type": "SliderInput",
"advanced": false,
"display_name": "Depth",
"dynamic": false,
"info": "Controls how many 'clicks' away from the initial page the crawler will go:\n- depth 1: only the initial page\n- depth 2: initial page + all pages linked directly from it\n- depth 3: initial page + direct links + links found on those direct link pages\nNote: This is about link traversal, not URL path depth.",
"max_label": " ",
"max_label_icon": "None",
"min_label": " ",
"min_label_icon": "None",
"name": "max_depth",
"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": 1
},
"prevent_outside": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Prevent Outside",
"dynamic": false,
"info": "If enabled, only crawls URLs within the same domain as the root URL. This helps prevent the crawler from going to external websites.",
"list": false,
"list_add_label": "Add More",
"name": "prevent_outside",
"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
},
"timeout": {
"_input_type": "IntInput",
"advanced": true,
"display_name": "Timeout",
"dynamic": false,
"info": "Timeout for the request in seconds.",
"list": false,
"list_add_label": "Add More",
"name": "timeout",
"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": 30
},
"tools_metadata": {
"_input_type": "ToolsInput",
"advanced": false,
"display_name": "Actions",
"dynamic": false,
"info": "Modify tool names and descriptions to help agents understand when to use each tool.",
"is_list": true,
"list_add_label": "Add More",
"name": "tools_metadata",
"override_skip": false,
"placeholder": "",
"real_time_refresh": true,
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"track_in_telemetry": false,
"type": "tools",
"value": [
{
"args": {
"urls": {
"default": "",
"description": "Enter one or more URLs to crawl recursively, by clicking the '+' button.",
"items": {
"type": "string"
},
"title": "Urls",
"type": "array"
}
},
"description": "Fetch content from one or more web pages, following links recursively.",
"display_description": "Fetch content from one or more web pages, following links recursively.",
"display_name": "fetch_content",
"name": "fetch_content",
"readonly": false,
"status": true,
"tags": [
"fetch_content"
]
}
]
},
"urls": {
"_input_type": "MessageTextInput",
"advanced": false,
"display_name": "URLs",
"dynamic": false,
"info": "Enter one or more URLs to crawl recursively, by clicking the '+' button.",
"input_types": [
"Message"
],
"list": true,
"list_add_label": "Add URL",
"load_from_db": false,
"name": "urls",
"override_skip": false,
"placeholder": "Enter a URL...",
"required": false,
"show": true,
"title_case": false,
"tool_mode": true,
"trace_as_input": true,
"trace_as_metadata": true,
"track_in_telemetry": false,
"type": "str",
"value": [
"https://docs.langflow.org/"
]
},
"use_async": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Use Async",
"dynamic": false,
"info": "If enabled, uses asynchronous loading which can be significantly faster but might use more system resources.",
"list": false,
"list_add_label": "Add More",
"name": "use_async",
"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": true
},
"selected_output": "page_results",
"showNode": true,
"type": "URLComponent"
},
"dragging": false,
"id": "URLComponent-P2u21",
"measured": {
"height": 299,
"width": 320
},
"position": {
"x": 2145.812486589649,
"y": 339.0945152239169
},
"positionAbsolute": {
"x": 2145.812486589649,
"y": 339.0945152239169
},
"selected": false,
"type": "genericNode"
},
{
"data": {
"id": "MemoryBase-lgwzl",
"node": {
"base_classes": [
"Table"
],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
"description": "Retrieve long-term memory from a Memory Base attached to this workflow. When 'Filter by Session' is off, queries run across all sessions.",
"display_name": "Memory Base",
"documentation": "",
"edited": false,
"field_order": [
"memory_base",
"search_query",
"top_k",
"include_metadata",
"filter_by_session"
],
"frozen": false,
"icon": "brain",
"last_updated": "2026-06-26T12:09:59.748Z",
"legacy": false,
"lf_version": "1.10.1rc3",
"metadata": {
"code_hash": "fddf83963c66",
"component_class_name": "MemoryBaseComponent",
"dependencies": {
"dependencies": [
{
"name": "numpy",
"version": "2.4.6"
},
{
"name": "lfx",
"version": null
},
{
"name": "langflow",
"version": null
},
{
"name": "sqlmodel",
"version": "0.0.46"
}
],
"total_dependencies": 4
},
"module": "lfx.components.files_and_knowledge.memory_retrieval.MemoryBaseComponent"
},
"minimized": false,
"output_types": [],
"outputs": [
{
"allows_loop": false,
"cache": true,
"display_name": "Toolset",
"group_outputs": false,
"hidden": null,
"loop_types": null,
"method": "to_toolkit",
"name": "component_as_tool",
"options": null,
"required_inputs": null,
"selected": "Tool",
"tool_mode": true,
"types": [
"Tool"
],
"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": "\"\"\"Memory Base retrieval component.\n\nQueries the vector store backing a Memory Base, scoped to the current flow's\nrequest session. Additional option to filter by session_id if the developer wants\nto turn that on. The component will auto filter based on session_id then.\n\nWhere that store lives comes from the Memory Base's ``knowledge_base`` row, not\nfrom disk: only a local-Chroma Memory Base resolves a filesystem path at all.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport uuid\nfrom collections.abc import Mapping\nfrom typing import TYPE_CHECKING, Any\n\nimport numpy as np\n\nfrom lfx.custom import Component\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.services.deps import session_scope\nfrom lfx.workflow.end_user_identity import end_user_id_from_scoped_session, serving_end_user_enabled\n\nif TYPE_CHECKING:\n from langflow.services.database.models.memory_base.model import MemoryBase\n from langflow.services.database.models.user.model import User\n from sqlmodel.ext.asyncio.session import AsyncSession\n\n from lfx.base.knowledge_bases.backends.base import BaseVectorStoreBackend\n\n\ndef _coerce_uuid(value: Any) -> uuid.UUID | None:\n if value is None:\n return None\n if isinstance(value, uuid.UUID):\n return value\n try:\n return uuid.UUID(str(value))\n except (ValueError, TypeError):\n return None\n\n\ndef _to_python_scalar(value: Any) -> Any:\n \"\"\"Convert numpy scalars (int64, float64, bool_, \u2026) to Python primitives.\n\n Chroma persists integer/float metadata as numpy scalars, which break JSON\n serialization when this component is consumed as an Agent tool \u2014 LangChain's\n tool-output path calls ``vars()`` / iterates the value, both of which fail\n on numpy C-extension scalars. Coerce at the boundary so downstream stays\n primitive-only.\n \"\"\"\n if isinstance(value, np.generic):\n return value.item()\n return value\n\n\ndef _session_filter_enabled(value: Any) -> bool:\n \"\"\"Parse serialized false values while keeping unknown values fail-closed.\"\"\"\n if isinstance(value, str):\n return value.strip().lower() not in {\"\", \"0\", \"false\", \"no\", \"off\"}\n return bool(value)\n\n\nclass MemoryBaseComponent(Component):\n display_name = \"Memory Base\"\n description = (\n \"Retrieve long-term memory from a Memory Base attached to this workflow. \"\n \"When 'Filter by Session' is off, queries run across all sessions.\"\n )\n icon = \"brain\"\n name = \"MemoryBase\"\n\n inputs = [\n DropdownInput(\n name=\"memory_base\",\n display_name=\"Memory Base\",\n info=\"Memory Base whose captured conversation history will be searched.\",\n required=True,\n options=[],\n refresh_button=True,\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"search_query\",\n display_name=\"Search Query\",\n info=\"Query used for semantic retrieval against the memory base.\",\n tool_mode=True,\n ),\n IntInput(\n name=\"top_k\",\n display_name=\"Top K Results\",\n info=\"Number of top results to return.\",\n value=5,\n advanced=True,\n required=False,\n ),\n BoolInput(\n name=\"include_metadata\",\n display_name=\"Include Metadata\",\n info=\"Include chunk metadata (session_id, sender, timestamp, \u2026) on each row.\",\n value=True,\n advanced=True,\n ),\n BoolInput(\n name=\"filter_by_session\",\n display_name=\"Filter by Session\",\n info=(\n \"If enabled, only memories from the current session will be retrieved. \"\n \"Disable to allow retrieval across every session ingested into this \"\n \"Memory Base (useful for cross-conversation recall).\"\n ),\n value=True,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(\n name=\"retrieve_data\",\n display_name=\"Results\",\n method=\"retrieve_memory\",\n info=(\n \"Returns matching memory chunks. Scoped to the current session by \"\n \"default; turn 'Filter by Session' off to retrieve across sessions.\"\n ),\n ),\n ]\n\n async def _policy_embedding_selection(self, selected: str) -> tuple[str, str]:\n \"\"\"Resolve an attached Memory Base's non-secret embedding metadata.\"\"\"\n flow_id = _coerce_uuid(self._get_runtime_or_frontend_node_attr(\"flow_id\"))\n if flow_id is None:\n msg = \"flow_id is not available on the graph context; Memory Base retrieval is unavailable.\"\n raise ValueError(msg)\n\n owner_user_id = _coerce_uuid(self.user_id)\n if owner_user_id is None:\n msg = \"user_id is not available on the graph context; Memory Base retrieval is unavailable.\"\n raise ValueError(msg)\n\n from langflow.api.utils.kb_helpers import resolve_embedding_selection\n from langflow.services.database.models.memory_base.model import MemoryBase\n from sqlmodel import select\n\n async with session_scope() as db:\n memory_base = (\n await db.exec(\n select(MemoryBase).where(\n MemoryBase.name == selected,\n MemoryBase.flow_id == flow_id,\n MemoryBase.user_id == owner_user_id,\n )\n )\n ).first()\n if memory_base is None:\n msg = f\"Memory Base '{selected}' is not attached to this flow.\"\n raise ValueError(msg)\n return await resolve_embedding_selection(user_id=memory_base.user_id, kb_name=memory_base.kb_name)\n\n async def _additional_model_provider_policy_ids(self, purpose, parameters=None) -> tuple[str, ...]:\n \"\"\"Discover the persisted embedding provider before runtime input hydration.\"\"\"\n from lfx.base.models.provider_registry import resolve_provider_id\n from lfx.services.model_provider_policy import ModelProviderPolicyError\n\n effective_parameters = parameters if isinstance(parameters, Mapping) else getattr(self, \"_parameters\", None)\n if not isinstance(effective_parameters, Mapping):\n effective_parameters = {}\n selected = effective_parameters.get(\"memory_base\", getattr(self, \"memory_base\", None))\n if not isinstance(selected, str) or not selected.strip():\n return ()\n\n provider, _model = await self._policy_embedding_selection(selected.strip())\n if not isinstance(provider, str) or not provider.strip():\n unknown_provider = \"unknown\"\n raise ModelProviderPolicyError(unknown_provider, purpose)\n return (resolve_provider_id(provider),)\n\n async def arequire_model_provider_policy(self, purpose, *, user_id=None, parameters=None) -> None:\n \"\"\"Capture the trusted actor context after the shared pre-hydration gate.\"\"\"\n await super().arequire_model_provider_policy(\n purpose,\n user_id=user_id,\n parameters=parameters,\n )\n\n from lfx.services.model_provider_policy import current_model_provider_policy_context\n\n actor_user_id = self.user_id if user_id is None else user_id\n policy_context = current_model_provider_policy_context()\n self._runtime_provider_policy_actor_id = actor_user_id\n self._runtime_provider_policy_attributes = (\n dict(policy_context.attributes)\n if policy_context is not None and str(policy_context.user_id) == str(actor_user_id)\n else {}\n )\n\n def _build_where_clause(self, *, session_id: str | None = None, end_user_id: str | None = None) -> dict | None:\n \"\"\"Compose the metadata filter based on opt-in filters and manual params.\n\n Emits a flat ``{key: value}`` dict, which is the contract every\n ``BaseVectorStoreBackend`` accepts: Chroma reads it as ``$eq`` shorthand\n and OpenSearch translates it into a bool query. Chroma's explicit\n ``{\"$eq\": ...}`` operator form is NOT portable \u2014 a remote backend would\n treat the operator dict as a literal value and silently match nothing.\n\n When ``filter_by_session`` is on, the session-id predicate already scopes to one\n end user (its ``<end_user>::<base>`` prefix). When it is off (cross-session recall)\n on the serving plane, ``end_user_id`` keeps the query within that one end user so it\n does not span every end user's chunks in the shared service-account store. Off /\n editor (``end_user_id`` None) adds neither predicate \u2014 all sessions, unchanged.\n \"\"\"\n predicates: dict = {}\n if _session_filter_enabled(self.filter_by_session) and session_id:\n predicates[\"session_id\"] = str(session_id)\n elif end_user_id:\n predicates[\"end_user_id\"] = str(end_user_id)\n\n return predicates or None\n\n async def update_build_config(self, build_config, field_value, field_name=None): # noqa: ARG002\n \"\"\"Populate Memory selections from the current developer's stored Memory Bases.\"\"\"\n if field_name != \"memory_base\":\n return build_config\n\n flow_id = _coerce_uuid(self._get_runtime_or_frontend_node_attr(\"flow_id\"))\n user_uuid = _coerce_uuid(self.user_id)\n if flow_id is None or user_uuid is None:\n build_config[\"memory_base\"][\"options\"] = []\n build_config[\"memory_base\"][\"value\"] = None\n return build_config\n\n from langflow.services.database.models.memory_base.model import MemoryBase\n from sqlmodel import select\n\n # At design time self.user_id == the flow developer == MB owner, so this\n # filters to the same set a Flow-lookup would return but without relying\n # on the Flow row being persisted yet.\n async with session_scope() as db:\n stmt = select(MemoryBase).where(\n MemoryBase.flow_id == flow_id,\n MemoryBase.user_id == user_uuid,\n )\n mbs = list((await db.exec(stmt)).all())\n\n options = sorted(mb.name for mb in mbs)\n build_config[\"memory_base\"][\"options\"] = options\n if build_config[\"memory_base\"].get(\"value\") not in options:\n build_config[\"memory_base\"][\"value\"] = None\n return build_config\n\n async def _resolve_attached_mb(\n self,\n db: AsyncSession,\n selected: str,\n flow_id: uuid.UUID,\n execution_user_id: uuid.UUID,\n ) -> tuple[MemoryBase, User]:\n \"\"\"Look up the MB row scoped to the exact flow execution principal.\"\"\"\n from langflow.services.database.models.memory_base.model import MemoryBase\n from langflow.services.database.models.user.crud import get_user_by_id\n from sqlmodel import select\n\n mb = (\n await db.exec(\n select(MemoryBase).where(\n MemoryBase.name == selected,\n MemoryBase.flow_id == flow_id,\n MemoryBase.user_id == execution_user_id,\n )\n )\n ).first()\n if mb is None:\n msg = f\"Memory Base '{selected}' is not attached to this flow.\"\n raise ValueError(msg)\n\n owner = await get_user_by_id(db, mb.user_id)\n if owner is None:\n msg = \"Memory Base owner account could not be resolved.\"\n raise ValueError(msg)\n return mb, owner\n\n async def _build_backend(\n self,\n owner: User,\n owner_username: str, # noqa: ARG002 - compatibility signature\n kb_name: str,\n ) -> BaseVectorStoreBackend:\n \"\"\"Construct the KB's configured backend, wired to its embedding function.\n\n The embedding config and the backend both resolve from the\n ``knowledge_base`` row, so nothing on disk is consulted to decide *where*\n this Memory Base lives. A local path is then derived only when that row\n says local Chroma; for OpenSearch, pgVector, or Chroma Cloud it stays\n ``None`` and the filesystem is never touched. That is what lets a\n Memory Base be queried \u2014 with the right embedding model \u2014 from a replica\n whose local disk never held the KB directory.\n \"\"\"\n # Re-resolve the persisted selection and actor policy immediately before\n # owner credential access. The graph-level preflight runs before input\n # hydration; this repeat also closes a metadata-change race between that\n # boundary and backend construction.\n from langflow.api.utils.kb_helpers import resolve_embedding_selection\n\n provider, model = await resolve_embedding_selection(user_id=owner.id, kb_name=kb_name)\n provider_policy = await self._resolve_runtime_embedding_policy(provider, owner.id)\n\n embedding_function = self._build_embeddings_with_policy(\n provider,\n model,\n owner_user_id=owner.id,\n provider_policy=provider_policy,\n )\n\n from langflow.api.utils.kb_helpers import backend_for_name\n\n backend = await backend_for_name(owner.id, kb_name, embedding_function=embedding_function)\n await backend.ensure_ready()\n return backend\n\n async def _resolve_runtime_embedding_policy(self, provider: str, owner_user_id: uuid.UUID):\n \"\"\"Refresh actor-scoped provider policy before owner credential lookup.\"\"\"\n from lfx.services.model_provider_policy import (\n ModelProviderPolicyPurpose,\n aresolve_model_provider_policy,\n current_model_provider_policy_context,\n )\n\n actor_user_id = getattr(self, \"_runtime_provider_policy_actor_id\", None)\n attributes = getattr(self, \"_runtime_provider_policy_attributes\", None)\n if actor_user_id is None:\n policy_context = current_model_provider_policy_context()\n if policy_context is not None:\n actor_user_id = policy_context.user_id\n attributes = dict(policy_context.attributes)\n else:\n actor_user_id = owner_user_id\n attributes = {}\n\n snapshot = await aresolve_model_provider_policy(\n user_id=actor_user_id,\n providers=[provider],\n purpose=ModelProviderPolicyPurpose.USE,\n attributes=attributes,\n )\n snapshot.require(provider)\n return snapshot\n\n @staticmethod\n def _build_embeddings_with_policy(provider: str, model: str, *, owner_user_id, provider_policy):\n \"\"\"Build with owner credentials while reusing the actor's trusted decision.\"\"\"\n from lfx.base.models.unified_models import get_embeddings\n from lfx.base.models.unified_models.class_registry import (\n EMBEDDING_PARAM_MAPPINGS,\n EMBEDDING_PROVIDER_CLASS_MAPPING,\n )\n\n embedding_class = EMBEDDING_PROVIDER_CLASS_MAPPING.get(provider)\n param_mapping = EMBEDDING_PARAM_MAPPINGS.get(provider)\n if not embedding_class or not param_mapping:\n msg = f\"Embedding provider '{provider}' is not registered\"\n raise ValueError(msg)\n selected_option = {\n \"name\": model,\n \"provider\": provider,\n \"category\": provider,\n \"icon\": provider,\n \"metadata\": {\n \"embedding_class\": embedding_class,\n \"param_mapping\": param_mapping,\n \"model_type\": \"embeddings\",\n },\n }\n return get_embeddings(\n model=[selected_option],\n user_id=owner_user_id,\n provider_policy=provider_policy,\n )\n\n def _format_results(self, results: list[tuple], backend: BaseVectorStoreBackend) -> DataFrame:\n \"\"\"Convert backend ``(doc, score)`` tuples into the component's DataFrame output.\n\n Metadata values are coerced from numpy scalars to Python primitives so the\n resulting DataFrame is JSON-serializable when the component is invoked as\n an Agent tool.\n \"\"\"\n data_list: list[Data] = []\n for doc, score in results:\n kwargs: dict = {\"content\": doc.page_content}\n if self.search_query:\n kwargs[\"_score\"] = _to_python_scalar(backend.normalize_score(score))\n if self.include_metadata:\n for key, value in (doc.metadata or {}).items():\n kwargs[key] = _to_python_scalar(value)\n data_list.append(Data(**kwargs))\n return DataFrame(data=data_list)\n\n async def retrieve_memory(self) -> DataFrame:\n \"\"\"Retrieve chunks from the selected Memory Base.\n\n Scoped to the current ``session_id`` when ``filter_by_session`` is true; when\n false, every chunk in the Memory Base is queryable so the agent can recall\n context from prior conversations across all sessions.\n \"\"\"\n session_id = getattr(self.graph, \"session_id\", None)\n filter_on = _session_filter_enabled(self.filter_by_session)\n if filter_on and not session_id:\n # Only required when filtering is on, since the value gates the where clause.\n msg = (\n \"A session_id is required on the flow request when 'Filter by Session' \"\n \"is enabled \u2014 disable the toggle to allow cross-session retrieval.\"\n )\n raise ValueError(msg)\n\n # Serving plane: the end-user id lives in the scoped session prefix. When filtering\n # by session it is already scoped; when doing cross-session recall it becomes the\n # sole end-user predicate (see _build_where_clause).\n end_user_id = end_user_id_from_scoped_session(session_id)\n if serving_end_user_enabled() and not filter_on and end_user_id is None:\n # Fail-closed: an anonymous serving caller has no end-user scope, so\n # cross-session recall over the shared service-account store would return every\n # end user's memory. Anonymous runs persist nothing and have no cross-session\n # memory to recall, so return empty rather than leak.\n logger.debug(\"MemoryBase cross-session recall blocked for anonymous serving caller\")\n return DataFrame(data=[])\n\n flow_id = _coerce_uuid(getattr(self.graph, \"flow_id\", None))\n if flow_id is None:\n msg = \"flow_id is not available on the graph context; Memory Base retrieval is unavailable.\"\n raise ValueError(msg)\n\n execution_user_id = _coerce_uuid(getattr(self.graph, \"user_id\", None))\n if execution_user_id is None:\n msg = \"user_id is not available on the graph context; Memory Base retrieval is unavailable.\"\n raise ValueError(msg)\n\n selected = self.memory_base\n if not selected:\n msg = \"No Memory Base is selected.\"\n raise ValueError(msg)\n\n async with session_scope() as db:\n mb, owner = await self._resolve_attached_mb(db, selected, flow_id, execution_user_id)\n owner_username = owner.username\n kb_name = mb.kb_name\n\n where = self._build_where_clause(session_id=session_id, end_user_id=end_user_id)\n\n from langflow.services.memory_base.kb_path_helpers import hash_session_id\n\n logger.debug(\n \"MemoryBase retrieval mb=%s session_hash=%s session_filter=%s top_k=%s\",\n selected,\n hash_session_id(session_id) if session_id else \"<none>\",\n where is not None,\n self.top_k,\n )\n\n if not self.search_query:\n # Embedding providers may reject empty input; skip the round-trip entirely.\n return DataFrame(data=[])\n\n backend = await self._build_backend(owner, owner_username, kb_name)\n try:\n results = await backend.similarity_search(\n query=self.search_query,\n k=self.top_k,\n filter=where,\n with_scores=True,\n )\n finally:\n await backend.teardown()\n return self._format_results(results, backend)\n"
},
"filter_by_session": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Filter by Session",
"dynamic": false,
"info": "If enabled, only memories from the current session will be retrieved. Disable to allow retrieval across every session ingested into this Memory Base (useful for cross-conversation recall).",
"list": false,
"list_add_label": "Add More",
"name": "filter_by_session",
"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
},
"include_metadata": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Include Metadata",
"dynamic": false,
"info": "Include chunk metadata (session_id, sender, timestamp, \u2026) on each row.",
"list": false,
"list_add_label": "Add More",
"name": "include_metadata",
"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
},
"memory_base": {
"_input_type": "DropdownInput",
"advanced": false,
"combobox": false,
"dialog_inputs": {},
"display_name": "Memory Base",
"dynamic": false,
"external_options": {},
"info": "Memory Base whose captured conversation history will be searched.",
"name": "memory_base",
"options": [],
"options_metadata": [],
"override_skip": false,
"placeholder": "",
"real_time_refresh": true,
"refresh_button": true,
"required": true,
"show": true,
"title_case": false,
"toggle": false,
"tool_mode": false,
"trace_as_metadata": true,
"track_in_telemetry": true,
"type": "str",
"value": null
},
"search_query": {
"_input_type": "MessageTextInput",
"advanced": false,
"display_name": "Search Query",
"dynamic": false,
"info": "Query used for semantic retrieval against the memory base.",
"input_types": [
"Message"
],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"name": "search_query",
"override_skip": false,
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": true,
"trace_as_input": true,
"trace_as_metadata": true,
"track_in_telemetry": false,
"type": "str",
"value": ""
},
"tools_metadata": {
"_input_type": "ToolsInput",
"advanced": false,
"display_name": "Actions",
"dynamic": false,
"info": "Modify tool names and descriptions to help agents understand when to use each tool.",
"is_list": true,
"list_add_label": "Add More",
"name": "tools_metadata",
"override_skip": false,
"placeholder": "",
"real_time_refresh": true,
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"track_in_telemetry": false,
"type": "tools",
"value": [
{
"args": {
"search_query": {
"default": "",
"description": "Query used for semantic retrieval against the memory base.",
"title": "Search Query",
"type": "string"
}
},
"description": "Returns matching memory chunks. Scoped to the current session by default; turn 'Filter by Session' off to retrieve across sessions.",
"display_description": "Returns matching memory chunks. Scoped to the current session by default; turn 'Filter by Session' off to retrieve across sessions.",
"display_name": "retrieve_memory",
"name": "retrieve_memory",
"readonly": false,
"status": true,
"tags": [
"retrieve_memory"
]
}
]
},
"top_k": {
"_input_type": "IntInput",
"advanced": true,
"display_name": "Top K Results",
"dynamic": false,
"info": "Number of top results to return.",
"list": false,
"list_add_label": "Add More",
"name": "top_k",
"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": 5
}
},
"tool_mode": true
},
"showNode": true,
"type": "MemoryBase"
},
"dragging": false,
"id": "MemoryBase-lgwzl",
"measured": {
"height": 325,
"width": 320
},
"position": {
"x": 2166.9279533188446,
"y": 684.8870482782742
},
"positionAbsolute": {
"x": 2145.812486589649,
"y": 779.0945152239169
},
"selected": false,
"type": "genericNode"
},
{
"data": {
"id": "note-rQsRu",
"node": {
"description": "# \ud83e\udde9 Custom Component Generator\n\nDescribe the component you want and an **Agent** writes a Langflow custom Component (Python). It pulls Langflow's reference code on demand and remembers the conversation so you can iterate.\n\n## Setup\n1. **Agent** - add your LLM key (any provider in the Model dropdown).\n2. **URL** and **Memory** tools need no key.\n\n## How it works\n1. **Chat Input \u2192 Agent** - your component request.\n2. The Agent uses the **URL** tool to fetch Langflow reference code (base class, examples) when it needs the exact conventions, and **Memory** to recall earlier turns.\n3. **Agent \u2192 Chat Output** - the generated Python. Ask follow-ups to refine it.\n\n## Run\nOpen **Playground**, describe the component you need, and run.",
"display_name": "",
"documentation": "",
"i18n_key": "template_notes.custom_component_generator.b1d3ed28",
"template": {
"backgroundColor": "blue"
}
},
"type": "note"
},
"dragging": false,
"height": 690,
"id": "note-rQsRu",
"measured": {
"height": 690,
"width": 510
},
"position": {
"x": 1616.0668865607895,
"y": 330.1707813903506
},
"positionAbsolute": {
"x": 1625.8124865896489,
"y": 499.0945152239169
},
"resizing": false,
"selected": false,
"type": "noteNode",
"width": 510
},
{
"data": {
"id": "ChatOutput-c7KeD",
"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,
"metadata": {
"code_hash": "0aab1c78e1dc",
"component_class_name": "ChatOutput",
"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,
"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 clean_data: bool = getattr(self, \"clean_data\", False)\n if isinstance(self.input_value, list):\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, clean_data=clean_data)\n"
},
"context_id": {
"_input_type": "MessageTextInput",
"advanced": true,
"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,
"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,
"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,
"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,
"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,
"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,
"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": false,
"type": "ChatOutput"
},
"dragging": false,
"id": "ChatOutput-c7KeD",
"measured": {
"height": 52,
"width": 192
},
"position": {
"x": 3059.551175469571,
"y": 928.7033169699224
},
"selected": false,
"type": "genericNode"
}
],
"viewport": {
"x": -1050.7027211589657,
"y": -155.08249741454517,
"zoom": 0.6848124482732146
}
},
"description": "Describe a Langflow component and an Agent generates its Python code. The Agent fetches Langflow reference code on demand with a URL tool and remembers the conversation with a Memory tool.",
"endpoint_name": null,
"id": "a6b1e90c-017b-44a1-a509-f8e8c86fc25e",
"is_component": false,
"last_tested_version": "1.10.1",
"locked": false,
"name": "Custom Component Generator",
"tags": [
"coding",
"web-scraping"
]
}How to use this workflow
- Review the pre-filled example and source files above.
- Copy the template or download
Custom Component Generator.json, then use it in your model, IDE, or workflow tool. - Compare your result with the sample output when one is provided.
Frequently Asked Questions
How do I use Workflow to Langflow Graph to Custom Component Generator?
Open the workflow on Promptally, review the pre-filled example, then copy the template or download Custom Component Generator.json and paste it into your model, IDE or workflow tool. Workflow to Langflow Graph to Custom Component Generator is a static blueprint with no dynamic variables. Copy it and use it as-is.
Is Workflow to Langflow Graph to Custom Component Generator free?
Yes. Workflow to Langflow Graph to Custom Component Generator is free to view, copy and download on Promptally without creating an account. It is published under the MIT license.
What variables does Workflow to Langflow Graph to Custom Component Generator need?
Workflow to Langflow Graph to Custom Component Generator is a static blueprint with no dynamic variables. Copy it and use it as-is.