Workflow to Langflow Graph to Memory Chatbot
Workflow to Langflow Graph to Memory Chatbot 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 Memory Chatbot. 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 | 39,604 |
| Source file | Memory Chatbot.json |
| Access tier | Free public (MIT) |
Pre-Filled Template
{
"data": {
"edges": [
{
"animated": false,
"className": "",
"data": {
"sourceHandle": {
"dataType": "ChatInput",
"id": "ChatInput-oQkLS",
"name": "message",
"output_types": [
"Message"
]
},
"targetHandle": {
"fieldName": "input_value",
"id": "Agent-AlLub",
"inputTypes": [
"Message"
],
"type": "str"
}
},
"id": "reactflow__edge-ChatInput-oQkLS{\u0153dataType\u0153:\u0153ChatInput\u0153,\u0153id\u0153:\u0153ChatInput-oQkLS\u0153,\u0153name\u0153:\u0153message\u0153,\u0153output_types\u0153:[\u0153Message\u0153]}-Agent-AlLub{\u0153fieldName\u0153:\u0153input_value\u0153,\u0153id\u0153:\u0153Agent-AlLub\u0153,\u0153inputTypes\u0153:[\u0153Message\u0153],\u0153type\u0153:\u0153str\u0153}",
"selected": false,
"source": "ChatInput-oQkLS",
"sourceHandle": "{\u0153dataType\u0153: \u0153ChatInput\u0153, \u0153id\u0153: \u0153ChatInput-oQkLS\u0153, \u0153name\u0153: \u0153message\u0153, \u0153output_types\u0153: [\u0153Message\u0153]}",
"target": "Agent-AlLub",
"targetHandle": "{\u0153fieldName\u0153: \u0153input_value\u0153, \u0153id\u0153: \u0153Agent-AlLub\u0153, \u0153inputTypes\u0153: [\u0153Message\u0153], \u0153type\u0153: \u0153str\u0153}"
},
{
"animated": false,
"className": "",
"data": {
"sourceHandle": {
"dataType": "MemoryBase",
"id": "MemoryBase-xePih",
"name": "component_as_tool",
"output_types": [
"Tool"
]
},
"targetHandle": {
"fieldName": "tools",
"id": "Agent-AlLub",
"inputTypes": [
"Tool"
],
"type": "other"
}
},
"id": "reactflow__edge-MemoryBase-xePih{\u0153dataType\u0153:\u0153MemoryBase\u0153,\u0153id\u0153:\u0153MemoryBase-xePih\u0153,\u0153name\u0153:\u0153component_as_tool\u0153,\u0153output_types\u0153:[\u0153Tool\u0153]}-Agent-AlLub{\u0153fieldName\u0153:\u0153tools\u0153,\u0153id\u0153:\u0153Agent-AlLub\u0153,\u0153inputTypes\u0153:[\u0153Tool\u0153],\u0153type\u0153:\u0153other\u0153}",
"selected": false,
"source": "MemoryBase-xePih",
"sourceHandle": "{\u0153dataType\u0153: \u0153MemoryBase\u0153, \u0153id\u0153: \u0153MemoryBase-xePih\u0153, \u0153name\u0153: \u0153component_as_tool\u0153, \u0153output_types\u0153: [\u0153Tool\u0153]}",
"target": "Agent-AlLub",
"targetHandle": "{\u0153fieldName\u0153: \u0153tools\u0153, \u0153id\u0153: \u0153Agent-AlLub\u0153, \u0153inputTypes\u0153: [\u0153Tool\u0153], \u0153type\u0153: \u0153other\u0153}"
},
{
"animated": false,
"className": "",
"data": {
"sourceHandle": {
"dataType": "Agent",
"id": "Agent-AlLub",
"name": "response",
"output_types": [
"Message"
]
},
"targetHandle": {
"fieldName": "input_value",
"id": "ChatOutput-65zjS",
"inputTypes": [
"Data",
"JSON",
"DataFrame",
"Table",
"Message"
],
"type": "str"
}
},
"id": "reactflow__edge-Agent-AlLub{\u0153dataType\u0153:\u0153Agent\u0153,\u0153id\u0153:\u0153Agent-AlLub\u0153,\u0153name\u0153:\u0153response\u0153,\u0153output_types\u0153:[\u0153Message\u0153]}-ChatOutput-65zjS{\u0153fieldName\u0153:\u0153input_value\u0153,\u0153id\u0153:\u0153ChatOutput-65zjS\u0153,\u0153inputTypes\u0153:[\u0153Data\u0153,\u0153JSON\u0153,\u0153DataFrame\u0153,\u0153Table\u0153,\u0153Message\u0153],\u0153type\u0153:\u0153str\u0153}",
"selected": false,
"source": "Agent-AlLub",
"sourceHandle": "{\u0153dataType\u0153: \u0153Agent\u0153, \u0153id\u0153: \u0153Agent-AlLub\u0153, \u0153name\u0153: \u0153response\u0153, \u0153output_types\u0153: [\u0153Message\u0153]}",
"target": "ChatOutput-65zjS",
"targetHandle": "{\u0153fieldName\u0153: \u0153input_value\u0153, \u0153id\u0153: \u0153ChatOutput-65zjS\u0153, \u0153inputTypes\u0153: [\u0153Data\u0153, \u0153JSON\u0153, \u0153DataFrame\u0153, \u0153Table\u0153, \u0153Message\u0153], \u0153type\u0153: \u0153str\u0153}"
}
],
"nodes": [
{
"data": {
"id": "ChatInput-oQkLS",
"node": {
"base_classes": [
"Message"
],
"beta": false,
"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",
"legacy": false,
"lf_version": "1.10.1rc3",
"metadata": {
"code_hash": "45974e6b1909",
"component_class_name": "ChatInput",
"dependencies": {
"dependencies": [
{
"name": "lfx",
"version": null
}
],
"total_dependencies": 1
},
"module": "lfx.components.input_output.chat.ChatInput"
},
"output_types": [],
"outputs": [
{
"allows_loop": false,
"cache": true,
"display_name": "Chat Message",
"group_outputs": false,
"method": "message_response",
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"value": "__UNDEFINED__"
}
],
"pinned": false,
"template": {
"_type": "Component",
"code": {
"advanced": true,
"api_editable": false,
"dynamic": true,
"fileTypes": [],
"file_path": "",
"info": "",
"list": false,
"load_from_db": false,
"multiline": true,
"name": "code",
"password": false,
"placeholder": "",
"required": true,
"show": true,
"title_case": false,
"type": "code",
"value": "from lfx.base.data.utils import IMG_FILE_TYPES, TEXT_FILE_TYPES\nfrom lfx.base.io.chat import ChatComponent\nfrom lfx.inputs.inputs import BoolInput\nfrom lfx.io import (\n DropdownInput,\n FileInput,\n MessageTextInput,\n MultilineInput,\n Output,\n)\nfrom lfx.schema.image import get_file_paths\nfrom lfx.schema.message import Message\nfrom lfx.services.deps import get_settings_service\nfrom lfx.utils.constants import (\n MESSAGE_SENDER_AI,\n MESSAGE_SENDER_NAME_USER,\n MESSAGE_SENDER_USER,\n)\nfrom lfx.utils.file_path_security import (\n component_file_access_scopes,\n enforce_local_file_access,\n is_local_file_access_restricted,\n)\n\n\nclass ChatInput(ChatComponent):\n display_name = \"Chat Input\"\n description = \"Get chat inputs from the Playground.\"\n documentation: str = \"https://docs.langflow.org/chat-input-and-output\"\n icon = \"MessagesSquare\"\n name = \"ChatInput\"\n minimized = True\n\n inputs = [\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input Text\",\n value=\"\",\n info=\"Message to be passed as input.\",\n input_types=[],\n ),\n BoolInput(\n name=\"should_store_message\",\n display_name=\"Store Messages\",\n info=\"Store the message in the history.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER],\n value=MESSAGE_SENDER_USER,\n info=\"Type of sender.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Name of the sender.\",\n value=MESSAGE_SENDER_NAME_USER,\n advanced=True,\n ),\n MessageTextInput(\n name=\"session_id\",\n display_name=\"Session ID\",\n info=\"The session ID of the chat. If empty, the current session ID parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"context_id\",\n display_name=\"Context ID\",\n info=\"The context ID of the chat. Adds an extra layer to the local memory.\",\n value=\"\",\n advanced=True,\n ),\n FileInput(\n name=\"files\",\n display_name=\"Files\",\n file_types=TEXT_FILE_TYPES + IMG_FILE_TYPES,\n info=\"Files to be sent with the message.\",\n advanced=True,\n is_list=True,\n temp_file=True,\n ),\n ]\n outputs = [\n Output(display_name=\"Chat Message\", name=\"message\", method=\"message_response\"),\n ]\n\n async def message_response(self) -> Message:\n # Ensure files is a list and filter out empty/None values\n files = self.files if self.files else []\n if files and not isinstance(files, list):\n files = [files]\n # Filter out None/empty values\n files = [f for f in files if f is not None and f != \"\"]\n\n # Build endpoints inject caller-controlled file references directly into\n # ChatInput. Resolve and validate local references before the Message can\n # later read them while constructing model attachment content.\n if files:\n settings_service = get_settings_service()\n storage_type = getattr(getattr(settings_service, \"settings\", None), \"storage_type\", \"local\")\n # The storage factory falls back to local storage for unsupported\n # values, so only the explicitly configured S3 backend may bypass\n # filesystem containment.\n if str(storage_type).lower() != \"s3\" and is_local_file_access_restricted():\n scope_ids = list(component_file_access_scopes(self))\n # Public executions replace graph.flow_id with a per-visitor\n # virtual ID. Only the server-populated source provenance may\n # restore the already-validated public attachment namespace.\n source_flow_id = getattr(self.graph, \"source_flow_id\", None)\n if source_flow_id:\n scope_ids.append(source_flow_id)\n for file_path in get_file_paths(files):\n enforce_local_file_access(file_path, scope_ids=scope_ids)\n\n session_id = self.session_id or self.graph.session_id or \"\"\n message = await Message.create(\n text=self.input_value,\n sender=self.sender,\n sender_name=self.sender_name,\n session_id=session_id,\n context_id=self.context_id,\n files=files,\n )\n if session_id and isinstance(message, Message) and self.should_store_message:\n stored_message = await self.send_message(\n message,\n )\n self.message.value = stored_message\n message = stored_message\n\n self.status = message\n return message\n"
},
"context_id": {
"_input_type": "MessageTextInput",
"advanced": true,
"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,
"name": "files",
"placeholder": "",
"required": false,
"show": true,
"temp_file": true,
"title_case": false,
"trace_as_metadata": true,
"type": "file",
"value": ""
},
"input_value": {
"_input_type": "MultilineInput",
"advanced": false,
"display_name": "Input Text",
"dynamic": false,
"info": "Message to be passed as input.",
"input_types": [],
"list": false,
"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,
"display_name": "Sender Type",
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"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,
"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,
"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,
"name": "should_store_message",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"trace_as_metadata": true,
"type": "bool",
"value": true
}
},
"tool_mode": false
},
"selected_output": "message",
"type": "ChatInput"
},
"dragging": false,
"id": "ChatInput-oQkLS",
"measured": {
"height": 206,
"width": 320
},
"position": {
"x": 2153.952010643146,
"y": 864.4333264931514
},
"positionAbsolute": {
"x": 2321.5543981677606,
"y": 374.0457826421628
},
"selected": false,
"type": "genericNode"
},
{
"data": {
"description": "Display a chat message in the Playground.",
"display_name": "Chat Output",
"id": "ChatOutput-65zjS",
"node": {
"base_classes": [
"Message"
],
"beta": false,
"conditional_paths": [],
"custom_fields": {},
"description": "Display a chat message in the Playground.",
"display_name": "Chat Output",
"documentation": "",
"edited": false,
"field_order": [
"input_value",
"should_store_message",
"sender",
"sender_name",
"session_id",
"context_id",
"data_template",
"clean_data"
],
"frozen": false,
"icon": "MessagesSquare",
"legacy": false,
"lf_version": "1.10.1rc3",
"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"
},
"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",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": 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",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"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,
"load_from_db": false,
"name": "data_template",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"type": "str",
"value": "{text}"
},
"input_value": {
"_input_type": "MessageInput",
"advanced": false,
"display_name": "Inputs",
"dynamic": false,
"info": "Message to be passed as output.",
"input_types": [
"Data",
"JSON",
"DataFrame",
"Table",
"Message"
],
"list": false,
"load_from_db": false,
"name": "input_value",
"placeholder": "",
"required": true,
"show": true,
"title_case": false,
"trace_as_input": true,
"trace_as_metadata": true,
"type": "str",
"value": ""
},
"sender": {
"_input_type": "DropdownInput",
"advanced": true,
"combobox": false,
"display_name": "Sender Type",
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": 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,
"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": "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,
"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,
"name": "should_store_message",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"trace_as_metadata": true,
"type": "bool",
"value": true
}
},
"tool_mode": false
},
"type": "ChatOutput"
},
"dragging": false,
"id": "ChatOutput-65zjS",
"measured": {
"height": 206,
"width": 320
},
"position": {
"x": 3104.328520123746,
"y": 840.598176705712
},
"positionAbsolute": {
"x": 3101.965731391458,
"y": 776.4408905693839
},
"selected": false,
"type": "genericNode"
},
{
"data": {
"id": "note-HK9YV",
"node": {
"description": "# \ud83d\udcd6 README\nThis flow is a chatbot with **long-term memory**. An **Agent** answers the user, and a **Memory Base** component lets it **store and recall facts across conversations** \u2014 so it remembers what you told it even in a brand-new chat session.\n\n## Quick start\n1. Configure your **Model Provider** with your API credentials in the **Agent**.\n2. Open the **Playground**, and then tell the agent something about yourself (e.g. your name).\n3. Start a **new chat session** in the Playground, and then ask, `what is my name`. The agent recalls it from the **Memory Base** \u2014 not just from the current session's history.\n\n## About the Memory Base component\nThe **Agent** and **Language Model** components have built-in chat memory for the **current session**, enabled by default. **Memory Base** goes further: it persists **long-term memories** to a memory store and lets the agent retrieve them **across sessions within this flow for the same execution principal**. The agent uses Memory Base as a tool \u2014 writing new memories as you chat and reading the relevant ones when it answers.\n\nIn this flow, **Memory Base** has session filtering turned **off**, so memories are shared across sessions \u2014 that's why a new chat can still recall your name.\n\nUse **Memory Base** when you need memory that outlives a single chat session \u2014 personalization, user facts, or supplying memories from other chats in this flow. For session-only context, the built-in chat memory is enough. For more information, see [Memory](https://docs.langflow.org/memory).\n",
"display_name": "",
"documentation": "",
"i18n_key": "template_notes.memory_chatbot.2afd499a",
"template": {}
},
"type": "note"
},
"dragging": false,
"height": 772,
"id": "note-HK9YV",
"measured": {
"height": 772,
"width": 529
},
"position": {
"x": 1462.894231659186,
"y": 345.8660745626851
},
"positionAbsolute": {
"x": 1512.8976594415833,
"y": 312.9558305744385
},
"resizing": false,
"selected": false,
"style": {
"height": 736,
"width": 324
},
"type": "noteNode",
"width": 529
},
{
"data": {
"_connectionMode": false,
"id": "Agent-AlLub",
"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-23T18:53:59.793Z",
"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": true
},
"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": true
},
"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": [
{
"category": "OpenAI",
"icon": "OpenAI",
"metadata": {
"api_key_param": "api_key",
"context_length": 128000,
"max_tokens_field_name": "max_tokens",
"model_class": "ChatOpenAI",
"model_name_param": "model",
"reasoning_models": [
"gpt-5.5-pro"
]
},
"name": "gpt-5.5-pro",
"provider": "OpenAI"
},
{
"category": "OpenAI",
"icon": "OpenAI",
"metadata": {
"api_key_param": "api_key",
"context_length": 128000,
"max_tokens_field_name": "max_tokens",
"model_class": "ChatOpenAI",
"model_name_param": "model",
"reasoning_models": [
"gpt-5.5"
]
},
"name": "gpt-5.5",
"provider": "OpenAI"
},
{
"category": "OpenAI",
"icon": "OpenAI",
"metadata": {
"api_key_param": "api_key",
"context_length": 128000,
"max_tokens_field_name": "max_tokens",
"model_class": "ChatOpenAI",
"model_name_param": "model",
"reasoning_models": [
"gpt-5.4-nano"
]
},
"name": "gpt-5.4-nano",
"provider": "OpenAI"
},
{
"category": "OpenAI",
"icon": "OpenAI",
"metadata": {
"api_key_param": "api_key",
"context_length": 128000,
"max_tokens_field_name": "max_tokens",
"model_class": "ChatOpenAI",
"model_name_param": "model",
"reasoning_models": [
"gpt-5.4-mini"
]
},
"name": "gpt-5.4-mini",
"provider": "OpenAI"
},
{
"category": "OpenAI",
"icon": "OpenAI",
"metadata": {
"api_key_param": "api_key",
"context_length": 128000,
"max_tokens_field_name": "max_tokens",
"model_class": "ChatOpenAI",
"model_name_param": "model",
"reasoning_models": [
"gpt-5.4"
]
},
"name": "gpt-5.4",
"provider": "OpenAI"
},
{
"category": "Google Generative AI",
"icon": "GoogleGenerativeAI",
"metadata": {
"api_key_param": "google_api_key",
"context_length": 128000,
"max_tokens_field_name": "max_output_tokens",
"model_class": "ChatGoogleGenerativeAIFixed",
"model_name_param": "model"
},
"name": "gemini-3.5-flash",
"provider": "Google Generative AI"
},
{
"category": "Google Generative AI",
"icon": "GoogleGenerativeAI",
"metadata": {
"api_key_param": "google_api_key",
"context_length": 128000,
"max_tokens_field_name": "max_output_tokens",
"model_class": "ChatGoogleGenerativeAIFixed",
"model_name_param": "model"
},
"name": "gemini-3.1-flash-lite",
"provider": "Google Generative AI"
},
{
"category": "Google Generative AI",
"icon": "GoogleGenerativeAI",
"metadata": {
"api_key_param": "google_api_key",
"context_length": 128000,
"max_tokens_field_name": "max_output_tokens",
"model_class": "ChatGoogleGenerativeAIFixed",
"model_name_param": "model"
},
"name": "gemma-4-31b-it",
"provider": "Google Generative AI"
},
{
"category": "Google Generative AI",
"icon": "GoogleGenerativeAI",
"metadata": {
"api_key_param": "google_api_key",
"context_length": 128000,
"max_tokens_field_name": "max_output_tokens",
"model_class": "ChatGoogleGenerativeAIFixed",
"model_name_param": "model"
},
"name": "gemma-4-26b-a4b-it",
"provider": "Google Generative AI"
},
{
"category": "Ollama",
"icon": "Ollama",
"metadata": {
"api_key_param": "api_key",
"base_url_param": "base_url",
"context_length": 128000,
"max_tokens_field_name": "max_tokens",
"model_class": "ChatOllama",
"model_name_param": "model"
},
"name": "qwen2.5:0.5b",
"provider": "Ollama"
},
{
"category": "Ollama",
"icon": "Ollama",
"metadata": {
"api_key_param": "api_key",
"base_url_param": "base_url",
"context_length": 128000,
"max_tokens_field_name": "max_tokens",
"model_class": "ChatOllama",
"model_name_param": "model"
},
"name": "qwen2.5:7b",
"provider": "Ollama"
},
{
"category": "IBM WatsonX",
"icon": "WatsonxAI",
"metadata": {
"api_key_param": "apikey",
"context_length": 128000,
"max_tokens_field_name": "max_tokens",
"model_class": "ChatWatsonx",
"model_name_param": "model_id",
"project_id_param": "project_id",
"url_param": "url"
},
"name": "ibm/granite-4-h-small",
"provider": "IBM WatsonX"
},
{
"category": "IBM WatsonX",
"icon": "WatsonxAI",
"metadata": {
"api_key_param": "apikey",
"context_length": 128000,
"max_tokens_field_name": "max_tokens",
"model_class": "ChatWatsonx",
"model_name_param": "model_id",
"project_id_param": "project_id",
"url_param": "url"
},
"name": "ibm/granite-8b-code-instruct",
"provider": "IBM WatsonX"
},
{
"category": "IBM WatsonX",
"icon": "WatsonxAI",
"metadata": {
"api_key_param": "apikey",
"context_length": 128000,
"max_tokens_field_name": "max_tokens",
"model_class": "ChatWatsonx",
"model_name_param": "model_id",
"project_id_param": "project_id",
"url_param": "url"
},
"name": "ibm/granite-guardian-3-8b",
"provider": "IBM WatsonX"
},
{
"category": "IBM WatsonX",
"icon": "WatsonxAI",
"metadata": {
"api_key_param": "apikey",
"context_length": 128000,
"max_tokens_field_name": "max_tokens",
"model_class": "ChatWatsonx",
"model_name_param": "model_id",
"project_id_param": "project_id",
"url_param": "url"
},
"name": "meta-llama/llama-3-3-70b-instruct",
"provider": "IBM WatsonX"
},
{
"category": "IBM WatsonX",
"icon": "WatsonxAI",
"metadata": {
"api_key_param": "apikey",
"context_length": 128000,
"max_tokens_field_name": "max_tokens",
"model_class": "ChatWatsonx",
"model_name_param": "model_id",
"project_id_param": "project_id",
"url_param": "url"
},
"name": "meta-llama/llama-4-maverick-17b-128e-instruct-fp8",
"provider": "IBM WatsonX"
}
],
"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 friendly conversational assistant with long-term memory.\n\nA Memory Base is attached as a tool and holds what you've learned about the user across\nALL past conversations. Use it like this:\n- When a message depends on what you already know about the user (their name, preferences,\n past topics or decisions), FIRST call the memory tool with a short, focused query\n (e.g. \"user's name\", \"user's preferences\") and base your answer on what it returns.\n- If the memory has nothing relevant, say you don't know that yet and ask \u2014 never invent\n or guess a memory.\n- For questions that don't need past context, just answer directly.\n\nYou don't need to save anything: new information the user shares is written to the Memory\nBase automatically. Keep replies concise and natural, and don't mention the memory tool\nunless asked."
},
"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-AlLub",
"measured": {
"height": 430,
"width": 320
},
"position": {
"x": 2638.9257253925325,
"y": 506.12025672778464
},
"selected": true,
"type": "genericNode"
},
{
"data": {
"id": "MemoryBase-xePih",
"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-23T18:54:25.543Z",
"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-xePih",
"measured": {
"height": 324,
"width": 320
},
"position": {
"x": 2152.2185184733175,
"y": 486.59797677086067
},
"selected": false,
"type": "genericNode"
}
],
"viewport": {
"x": -983.1103016047296,
"y": -184.85675138853026,
"zoom": 0.7198813686005942
}
},
"description": "Create a chatbot that saves and references previous messages, enabling the model to maintain context throughout the conversation.",
"endpoint_name": null,
"id": "ea80d83d-67d1-4d89-84a9-014b6b97c8a7",
"is_component": false,
"last_tested_version": "1.10.1rc3",
"locked": false,
"name": "Memory Chatbot",
"tags": [
"chatbots",
"openai",
"assistants"
]
}How to use this workflow
- Review the pre-filled example and source files above.
- Copy the template or download
Memory Chatbot.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 Memory Chatbot?
Open the workflow on Promptally, review the pre-filled example, then copy the template or download Memory Chatbot.json and paste it into your model, IDE or workflow tool. Workflow to Langflow Graph to Memory Chatbot is a static blueprint with no dynamic variables. Copy it and use it as-is.
Is Workflow to Langflow Graph to Memory Chatbot free?
Yes. Workflow to Langflow Graph to Memory Chatbot 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 Memory Chatbot need?
Workflow to Langflow Graph to Memory Chatbot is a static blueprint with no dynamic variables. Copy it and use it as-is.