Workflow to Langflow Graph to Twitter Thread Generator
Workflow to Langflow Graph to Twitter Thread Generator is a verified WORKFLOW blueprint on Promptally in Make.com & Langflow Graphs optimized for leading AI models. Multi-step automated node blueprint for Langflow Graph to Twitter Thread Generator. Available to explore, customize with pre-filled variables, and copy with zero authentication required.
Technical Specifications
| Specification | Value |
|---|---|
| Artifact type | WORKFLOW (Make.com & Langflow Graphs) |
| Compatible engines | Universal / Model Agnostic |
| Input variables | None (pre-filled) |
| Approximate tokens | 36,726 |
| Source file | Twitter Thread Generator.json |
| Access tier | Free public (MIT) |
Pre-Filled Template
{
"data": {
"edges": [
{
"animated": false,
"className": "",
"data": {
"sourceHandle": {
"dataType": "Prompt",
"id": "Prompt-DC9aA",
"name": "prompt",
"output_types": [
"Message"
]
},
"targetHandle": {
"fieldName": "system_prompt",
"id": "Agent-wh9WY",
"inputTypes": [
"Message"
],
"type": "str"
}
},
"id": "reactflow__edge-Prompt-DC9aA{\u0153dataType\u0153:\u0153Prompt\u0153,\u0153id\u0153:\u0153Prompt-DC9aA\u0153,\u0153name\u0153:\u0153prompt\u0153,\u0153output_types\u0153:[\u0153Message\u0153]}-Agent-wh9WY{\u0153fieldName\u0153:\u0153system_prompt\u0153,\u0153id\u0153:\u0153Agent-wh9WY\u0153,\u0153inputTypes\u0153:[\u0153Message\u0153],\u0153type\u0153:\u0153str\u0153}",
"selected": false,
"source": "Prompt-DC9aA",
"sourceHandle": "{\u0153dataType\u0153: \u0153Prompt\u0153, \u0153id\u0153: \u0153Prompt-DC9aA\u0153, \u0153name\u0153: \u0153prompt\u0153, \u0153output_types\u0153: [\u0153Message\u0153]}",
"target": "Agent-wh9WY",
"targetHandle": "{\u0153fieldName\u0153: \u0153system_prompt\u0153, \u0153id\u0153: \u0153Agent-wh9WY\u0153, \u0153inputTypes\u0153: [\u0153Message\u0153], \u0153type\u0153: \u0153str\u0153}"
},
{
"animated": false,
"className": "",
"data": {
"sourceHandle": {
"dataType": "ChatInput",
"id": "ChatInput-H3r3e",
"name": "message",
"output_types": [
"Message"
]
},
"targetHandle": {
"fieldName": "input_value",
"id": "Agent-wh9WY",
"inputTypes": [
"Message"
],
"type": "str"
}
},
"id": "reactflow__edge-ChatInput-H3r3e{\u0153dataType\u0153:\u0153ChatInput\u0153,\u0153id\u0153:\u0153ChatInput-H3r3e\u0153,\u0153name\u0153:\u0153message\u0153,\u0153output_types\u0153:[\u0153Message\u0153]}-Agent-wh9WY{\u0153fieldName\u0153:\u0153input_value\u0153,\u0153id\u0153:\u0153Agent-wh9WY\u0153,\u0153inputTypes\u0153:[\u0153Message\u0153],\u0153type\u0153:\u0153str\u0153}",
"selected": false,
"source": "ChatInput-H3r3e",
"sourceHandle": "{\u0153dataType\u0153: \u0153ChatInput\u0153, \u0153id\u0153: \u0153ChatInput-H3r3e\u0153, \u0153name\u0153: \u0153message\u0153, \u0153output_types\u0153: [\u0153Message\u0153]}",
"target": "Agent-wh9WY",
"targetHandle": "{\u0153fieldName\u0153: \u0153input_value\u0153, \u0153id\u0153: \u0153Agent-wh9WY\u0153, \u0153inputTypes\u0153: [\u0153Message\u0153], \u0153type\u0153: \u0153str\u0153}"
},
{
"animated": false,
"className": "",
"data": {
"sourceHandle": {
"dataType": "Agent",
"id": "Agent-wh9WY",
"name": "response",
"output_types": [
"Message"
]
},
"targetHandle": {
"fieldName": "input_value",
"id": "ChatOutput-imHi5",
"inputTypes": [
"Data",
"JSON",
"DataFrame",
"Table",
"Message"
],
"type": "other"
}
},
"id": "reactflow__edge-Agent-wh9WY{\u0153dataType\u0153:\u0153Agent\u0153,\u0153id\u0153:\u0153Agent-wh9WY\u0153,\u0153name\u0153:\u0153response\u0153,\u0153output_types\u0153:[\u0153Message\u0153]}-ChatOutput-imHi5{\u0153fieldName\u0153:\u0153input_value\u0153,\u0153id\u0153:\u0153ChatOutput-imHi5\u0153,\u0153inputTypes\u0153:[\u0153Data\u0153,\u0153JSON\u0153,\u0153DataFrame\u0153,\u0153Table\u0153,\u0153Message\u0153],\u0153type\u0153:\u0153other\u0153}",
"selected": false,
"source": "Agent-wh9WY",
"sourceHandle": "{\u0153dataType\u0153: \u0153Agent\u0153, \u0153id\u0153: \u0153Agent-wh9WY\u0153, \u0153name\u0153: \u0153response\u0153, \u0153output_types\u0153: [\u0153Message\u0153]}",
"target": "ChatOutput-imHi5",
"targetHandle": "{\u0153fieldName\u0153: \u0153input_value\u0153, \u0153id\u0153: \u0153ChatOutput-imHi5\u0153, \u0153inputTypes\u0153: [\u0153Data\u0153, \u0153JSON\u0153, \u0153DataFrame\u0153, \u0153Table\u0153, \u0153Message\u0153], \u0153type\u0153: \u0153other\u0153}"
}
],
"nodes": [
{
"data": {
"id": "ChatInput-H3r3e",
"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"
},
"minimized": true,
"output_types": [],
"outputs": [
{
"allows_loop": false,
"cache": true,
"display_name": "Chat Message",
"group_outputs": false,
"method": "message_response",
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"value": "__UNDEFINED__"
}
],
"pinned": false,
"template": {
"_type": "Component",
"code": {
"advanced": true,
"api_editable": false,
"dynamic": true,
"fileTypes": [],
"file_path": "",
"info": "",
"list": false,
"load_from_db": false,
"multiline": true,
"name": "code",
"password": false,
"placeholder": "",
"required": true,
"show": true,
"title_case": false,
"type": "code",
"value": "from lfx.base.data.utils import IMG_FILE_TYPES, TEXT_FILE_TYPES\nfrom lfx.base.io.chat import ChatComponent\nfrom lfx.inputs.inputs import BoolInput\nfrom lfx.io import (\n DropdownInput,\n FileInput,\n MessageTextInput,\n MultilineInput,\n Output,\n)\nfrom lfx.schema.image import get_file_paths\nfrom lfx.schema.message import Message\nfrom lfx.services.deps import get_settings_service\nfrom lfx.utils.constants import (\n MESSAGE_SENDER_AI,\n MESSAGE_SENDER_NAME_USER,\n MESSAGE_SENDER_USER,\n)\nfrom lfx.utils.file_path_security import (\n component_file_access_scopes,\n enforce_local_file_access,\n is_local_file_access_restricted,\n)\n\n\nclass ChatInput(ChatComponent):\n display_name = \"Chat Input\"\n description = \"Get chat inputs from the Playground.\"\n documentation: str = \"https://docs.langflow.org/chat-input-and-output\"\n icon = \"MessagesSquare\"\n name = \"ChatInput\"\n minimized = True\n\n inputs = [\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input Text\",\n value=\"\",\n info=\"Message to be passed as input.\",\n input_types=[],\n ),\n BoolInput(\n name=\"should_store_message\",\n display_name=\"Store Messages\",\n info=\"Store the message in the history.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"sender\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER],\n value=MESSAGE_SENDER_USER,\n info=\"Type of sender.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Name of the sender.\",\n value=MESSAGE_SENDER_NAME_USER,\n advanced=True,\n ),\n MessageTextInput(\n name=\"session_id\",\n display_name=\"Session ID\",\n info=\"The session ID of the chat. If empty, the current session ID parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"context_id\",\n display_name=\"Context ID\",\n info=\"The context ID of the chat. Adds an extra layer to the local memory.\",\n value=\"\",\n advanced=True,\n ),\n FileInput(\n name=\"files\",\n display_name=\"Files\",\n file_types=TEXT_FILE_TYPES + IMG_FILE_TYPES,\n info=\"Files to be sent with the message.\",\n advanced=True,\n is_list=True,\n temp_file=True,\n ),\n ]\n outputs = [\n Output(display_name=\"Chat Message\", name=\"message\", method=\"message_response\"),\n ]\n\n async def message_response(self) -> Message:\n # Ensure files is a list and filter out empty/None values\n files = self.files if self.files else []\n if files and not isinstance(files, list):\n files = [files]\n # Filter out None/empty values\n files = [f for f in files if f is not None and f != \"\"]\n\n # Build endpoints inject caller-controlled file references directly into\n # ChatInput. Resolve and validate local references before the Message can\n # later read them while constructing model attachment content.\n if files:\n settings_service = get_settings_service()\n storage_type = getattr(getattr(settings_service, \"settings\", None), \"storage_type\", \"local\")\n # The storage factory falls back to local storage for unsupported\n # values, so only the explicitly configured S3 backend may bypass\n # filesystem containment.\n if str(storage_type).lower() != \"s3\" and is_local_file_access_restricted():\n scope_ids = list(component_file_access_scopes(self))\n # Public executions replace graph.flow_id with a per-visitor\n # virtual ID. Only the server-populated source provenance may\n # restore the already-validated public attachment namespace.\n source_flow_id = getattr(self.graph, \"source_flow_id\", None)\n if source_flow_id:\n scope_ids.append(source_flow_id)\n for file_path in get_file_paths(files):\n enforce_local_file_access(file_path, scope_ids=scope_ids)\n\n session_id = self.session_id or self.graph.session_id or \"\"\n message = await Message.create(\n text=self.input_value,\n sender=self.sender,\n sender_name=self.sender_name,\n session_id=session_id,\n context_id=self.context_id,\n files=files,\n )\n if session_id and isinstance(message, Message) and self.should_store_message:\n stored_message = await self.send_message(\n message,\n )\n self.message.value = stored_message\n message = stored_message\n\n self.status = message\n return message\n"
},
"context_id": {
"_input_type": "MessageTextInput",
"advanced": true,
"display_name": "Context ID",
"dynamic": false,
"info": "The context ID of the chat. Adds an extra layer to the local memory.",
"input_types": [
"Message"
],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"name": "context_id",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"type": "str",
"value": ""
},
"files": {
"_input_type": "FileInput",
"advanced": true,
"display_name": "Files",
"dynamic": false,
"fileTypes": [
"csv",
"json",
"pdf",
"txt",
"md",
"mdx",
"yaml",
"yml",
"xml",
"html",
"htm",
"docx",
"py",
"sh",
"sql",
"js",
"ts",
"tsx",
"jpg",
"jpeg",
"png",
"bmp",
"image"
],
"file_path": "",
"info": "Files to be sent with the message.",
"list": true,
"list_add_label": "Add More",
"name": "files",
"placeholder": "",
"required": false,
"show": true,
"temp_file": true,
"title_case": false,
"trace_as_metadata": true,
"type": "file",
"value": ""
},
"input_value": {
"_input_type": "MultilineInput",
"advanced": false,
"copy_field": false,
"display_name": "Input Text",
"dynamic": false,
"info": "Message to be passed as input.",
"input_types": [],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"multiline": true,
"name": "input_value",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"type": "str",
"value": "**Objective:**\nCreate an engaging Twitter thread that narrates the innovative journey of our LangFlow project \u2014 highlighting how we built a specialized flow for generating dynamic prompts for other flows, culminating in a model specialized in writing tweets/threads.\n\n**Project Stages:**\n\n1. Development in LangFlow\n - Created a flow focused on generating dynamic prompts\n - System serves as the foundation for optimizing prompt generation in other flows\n\n2. Template Creation\n - Developed specific templates for tweets/threads\n - Focus on engagement and message clarity\n\n3. Results\n - 60% reduction in content creation time\n - Greater message consistency\n - Better social media engagement\n - Fully automated process\n\n**Thread Objectives:**\n - Educate about LangFlow's capabilities in content creation\n - Demonstrate the development process step by step\n - Inspire other developers to explore LangFlow\n - Strengthen the developer community"
},
"sender": {
"_input_type": "DropdownInput",
"advanced": true,
"combobox": false,
"dialog_inputs": {},
"display_name": "Sender Type",
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options_metadata": [],
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"toggle": false,
"tool_mode": false,
"trace_as_metadata": true,
"type": "str",
"value": "User"
},
"sender_name": {
"_input_type": "MessageTextInput",
"advanced": true,
"display_name": "Sender Name",
"dynamic": false,
"info": "Name of the sender.",
"input_types": [
"Message"
],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"name": "sender_name",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"type": "str",
"value": "User"
},
"session_id": {
"_input_type": "MessageTextInput",
"advanced": true,
"display_name": "Session ID",
"dynamic": false,
"info": "The session ID of the chat. If empty, the current session ID parameter will be used.",
"input_types": [
"Message"
],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"name": "session_id",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"type": "str",
"value": ""
},
"should_store_message": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Store Messages",
"dynamic": false,
"info": "Store the message in the history.",
"list": false,
"list_add_label": "Add More",
"name": "should_store_message",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"type": "bool",
"value": true
}
},
"tool_mode": false
},
"type": "ChatInput"
},
"dragging": false,
"id": "ChatInput-H3r3e",
"measured": {
"height": 207,
"width": 320
},
"position": {
"x": 1569.265764896729,
"y": 1144.6704425327823
},
"positionAbsolute": {
"x": 863.3241377184722,
"y": 1053.9324095084933
},
"selected": false,
"type": "genericNode"
},
{
"data": {
"description": "Display a chat message in the Playground.",
"display_name": "Chat Output",
"id": "ChatOutput-imHi5",
"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"
},
"minimized": true,
"output_types": [],
"outputs": [
{
"allows_loop": false,
"cache": true,
"display_name": "Output Message",
"group_outputs": false,
"method": "message_response",
"name": "message",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"value": "__UNDEFINED__"
}
],
"pinned": false,
"template": {
"_type": "Component",
"clean_data": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Basic Clean Data",
"dynamic": false,
"info": "Whether to clean data before converting to string.",
"list": false,
"list_add_label": "Add More",
"name": "clean_data",
"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,
"list_add_label": "Add More",
"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": "HandleInput",
"advanced": false,
"display_name": "Inputs",
"dynamic": false,
"info": "Message to be passed as output.",
"input_types": [
"Data",
"JSON",
"DataFrame",
"Table",
"Message"
],
"list": false,
"list_add_label": "Add More",
"name": "input_value",
"placeholder": "",
"required": true,
"show": true,
"title_case": false,
"trace_as_metadata": true,
"type": "other",
"value": ""
},
"sender": {
"_input_type": "DropdownInput",
"advanced": true,
"combobox": false,
"dialog_inputs": {},
"display_name": "Sender Type",
"dynamic": false,
"info": "Type of sender.",
"name": "sender",
"options": [
"Machine",
"User"
],
"options_metadata": [],
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"toggle": 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,
"list_add_label": "Add More",
"load_from_db": false,
"name": "sender_name",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"type": "str",
"value": "AI"
},
"session_id": {
"_input_type": "MessageTextInput",
"advanced": true,
"display_name": "Session ID",
"dynamic": false,
"info": "The session ID of the chat. If empty, the current session ID parameter will be used.",
"input_types": [
"Message"
],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"name": "session_id",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"type": "str",
"value": ""
},
"should_store_message": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Store Messages",
"dynamic": false,
"info": "Store the message in the history.",
"list": false,
"list_add_label": "Add More",
"name": "should_store_message",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"type": "bool",
"value": true
}
},
"tool_mode": false
},
"showNode": false,
"type": "ChatOutput"
},
"dragging": false,
"id": "ChatOutput-imHi5",
"measured": {
"height": 52,
"width": 192
},
"position": {
"x": 2663.1409807478503,
"y": 1049.1470151171268
},
"positionAbsolute": {
"x": 2470.223353127597,
"y": 1055.4039338762416
},
"selected": false,
"type": "genericNode"
},
{
"data": {
"description": "Create a prompt template with dynamic variables.",
"display_name": "Prompt Template",
"id": "Prompt-DC9aA",
"node": {
"base_classes": [
"Message"
],
"beta": false,
"conditional_paths": [],
"custom_fields": {
"template": [
"PROFILE_TYPE",
"PROFILE_DETAILS",
"CONTENT_GUIDELINES",
"TONE_AND_STYLE",
"OUTPUT_FORMAT",
"OUTPUT_LANGUAGE"
]
},
"description": "Create a prompt template with dynamic variables.",
"display_name": "Prompt Template",
"documentation": "",
"edited": false,
"field_order": [
"template",
"use_double_brackets",
"tool_placeholder"
],
"frozen": false,
"icon": "prompts",
"legacy": false,
"lf_version": "1.10.1rc3",
"metadata": {
"code_hash": "e3714ffe5d15",
"component_class_name": "PromptComponent",
"dependencies": {
"dependencies": [
{
"name": "lfx",
"version": null
}
],
"total_dependencies": 1
},
"module": "lfx.components.models_and_agents.prompt.PromptComponent"
},
"minimized": false,
"output_types": [],
"outputs": [
{
"allows_loop": false,
"cache": true,
"display_name": "Prompt",
"group_outputs": false,
"method": "build_prompt",
"name": "prompt",
"selected": "Message",
"tool_mode": true,
"types": [
"Message"
],
"value": "__UNDEFINED__"
}
],
"pinned": false,
"template": {
"CONTENT_GUIDELINES": {
"advanced": false,
"display_name": "CONTENT_GUIDELINES",
"dynamic": false,
"field_type": "str",
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message"
],
"list": false,
"load_from_db": false,
"multiline": true,
"name": "CONTENT_GUIDELINES",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"type": "str",
"value": "- Thread must be 5-7 tweets long - Each tweet should be self-contained but flow naturally to the next - Include relevant technical details while keeping language accessible - Use emojis sparingly but effectively - Include a clear call-to-action in the final tweet - Highlight key benefits and innovative aspects - Maintain professional but engaging tone"
},
"OUTPUT_FORMAT": {
"advanced": false,
"display_name": "OUTPUT_FORMAT",
"dynamic": false,
"field_type": "str",
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message"
],
"list": false,
"load_from_db": false,
"multiline": true,
"name": "OUTPUT_FORMAT",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"type": "str",
"value": "thread"
},
"OUTPUT_LANGUAGE": {
"advanced": false,
"display_name": "OUTPUT_LANGUAGE",
"dynamic": false,
"field_type": "str",
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message"
],
"list": false,
"load_from_db": false,
"multiline": true,
"name": "OUTPUT_LANGUAGE",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"type": "str",
"value": "English"
},
"PROFILE_DETAILS": {
"advanced": false,
"display_name": "PROFILE_DETAILS",
"dynamic": false,
"field_type": "str",
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message"
],
"list": false,
"load_from_db": false,
"multiline": true,
"name": "PROFILE_DETAILS",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"type": "str",
"value": "- Tech startup focused on AI/ML innovation - Active in open-source community - Experienced in building developer tools - Known for clear technical communication - Engaged audience of developers and AI enthusiasts"
},
"PROFILE_TYPE": {
"advanced": false,
"display_name": "PROFILE_TYPE",
"dynamic": false,
"field_type": "str",
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message"
],
"list": false,
"load_from_db": false,
"multiline": true,
"name": "PROFILE_TYPE",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"type": "str",
"value": "Tech Company / AI Developer Platform"
},
"TONE_AND_STYLE": {
"advanced": false,
"display_name": "TONE_AND_STYLE",
"dynamic": false,
"field_type": "str",
"fileTypes": [],
"file_path": "",
"info": "",
"input_types": [
"Message"
],
"list": false,
"load_from_db": false,
"multiline": true,
"name": "TONE_AND_STYLE",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"type": "str",
"value": "- Professional yet approachable - Technical but accessible - Enthusiastic about innovation - Educational and informative - Collaborative and community-focused - Clear and concise - Solution-oriented"
},
"_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 typing import Any\n\nfrom lfx.base.prompts.api_utils import process_prompt_template\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.input_mixin import FieldTypes\nfrom lfx.inputs.inputs import DefaultPromptField\nfrom lfx.io import BoolInput, MessageTextInput, Output, PromptInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.template.utils import update_template_values\nfrom lfx.utils.mustache_security import validate_mustache_template\n\n\nclass PromptComponent(Component):\n display_name: str = \"Prompt Template\"\n description: str = \"Create a prompt template with dynamic variables.\"\n documentation: str = \"https://docs.langflow.org/components-prompts\"\n icon = \"prompts\"\n trace_type = \"prompt\"\n name = \"Prompt Template\"\n\n inputs = [\n PromptInput(name=\"template\", display_name=\"Template\"),\n BoolInput(\n name=\"use_double_brackets\",\n display_name=\"Use Double Brackets\",\n value=False,\n advanced=True,\n info=\"Use {{variable}} syntax instead of {variable}.\",\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"tool_placeholder\",\n display_name=\"Tool Placeholder\",\n tool_mode=True,\n advanced=True,\n show=False,\n info=\"A placeholder input for tool mode.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Prompt\", name=\"prompt\", method=\"build_prompt\"),\n ]\n\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:\n \"\"\"Update the template field type based on the selected mode.\"\"\"\n if field_name == \"use_double_brackets\":\n # Change the template field type based on mode\n is_mustache = field_value is True\n if is_mustache:\n build_config[\"template\"][\"type\"] = FieldTypes.MUSTACHE_PROMPT.value\n else:\n build_config[\"template\"][\"type\"] = FieldTypes.PROMPT.value\n\n # Re-process the template to update variables when mode changes\n template_value = build_config.get(\"template\", {}).get(\"value\", \"\")\n if template_value:\n # Ensure custom_fields is properly initialized\n if \"custom_fields\" not in build_config:\n build_config[\"custom_fields\"] = {}\n\n # Clean up fields from the OLD mode before processing with NEW mode\n # This ensures we don't keep fields with wrong syntax even if validation fails\n old_custom_fields = build_config[\"custom_fields\"].get(\"template\", [])\n for old_field in list(old_custom_fields):\n # Remove the field from custom_fields and template\n if old_field in old_custom_fields:\n old_custom_fields.remove(old_field)\n build_config.pop(old_field, None)\n\n # Try to process template with new mode to add new variables\n # If validation fails, at least we cleaned up old fields\n try:\n # Validate mustache templates for security\n if is_mustache:\n validate_mustache_template(template_value)\n\n # Re-process template with new mode to add new variables\n _ = process_prompt_template(\n template=template_value,\n name=\"template\",\n custom_fields=build_config[\"custom_fields\"],\n frontend_node_template=build_config,\n is_mustache=is_mustache,\n )\n except ValueError as e:\n # If validation fails, we still updated the mode and cleaned old fields\n # User will see error when they try to save\n logger.debug(\"Template validation failed during mode switch\", exc_info=e)\n return build_config\n\n async def build_prompt(self) -> Message:\n use_double_brackets = self.use_double_brackets if hasattr(self, \"use_double_brackets\") else False\n template_format = \"mustache\" if use_double_brackets else \"f-string\"\n prompt = await Message.from_template_and_variables(template_format=template_format, **self._attributes)\n self.status = prompt.text\n return prompt\n\n def _update_template(self, frontend_node: dict):\n prompt_template = frontend_node[\"template\"][\"template\"][\"value\"]\n use_double_brackets = frontend_node[\"template\"].get(\"use_double_brackets\", {}).get(\"value\", False)\n is_mustache = use_double_brackets is True\n\n try:\n # Validate mustache templates for security\n if is_mustache:\n validate_mustache_template(prompt_template)\n\n custom_fields = frontend_node[\"custom_fields\"]\n frontend_node_template = frontend_node[\"template\"]\n _ = process_prompt_template(\n template=prompt_template,\n name=\"template\",\n custom_fields=custom_fields,\n frontend_node_template=frontend_node_template,\n is_mustache=is_mustache,\n )\n except ValueError as e:\n # If validation fails, don't add variables but allow component to be created\n logger.debug(\"Template validation failed in _update_template\", exc_info=e)\n return frontend_node\n\n async def update_frontend_node(self, new_frontend_node: dict, current_frontend_node: dict):\n \"\"\"This function is called after the code validation is done.\"\"\"\n frontend_node = await super().update_frontend_node(new_frontend_node, current_frontend_node)\n template = frontend_node[\"template\"][\"template\"][\"value\"]\n use_double_brackets = frontend_node[\"template\"].get(\"use_double_brackets\", {}).get(\"value\", False)\n is_mustache = use_double_brackets is True\n\n try:\n # Validate mustache templates for security\n if is_mustache:\n validate_mustache_template(template)\n\n # Kept it duplicated for backwards compatibility\n _ = process_prompt_template(\n template=template,\n name=\"template\",\n custom_fields=frontend_node[\"custom_fields\"],\n frontend_node_template=frontend_node[\"template\"],\n is_mustache=is_mustache,\n )\n except ValueError as e:\n # If validation fails, don't add variables but allow component to be updated\n logger.debug(\"Template validation failed in update_frontend_node\", exc_info=e)\n # Now that template is updated, we need to grab any values that were set in the current_frontend_node\n # and update the frontend_node with those values\n update_template_values(new_template=frontend_node, previous_template=current_frontend_node[\"template\"])\n return frontend_node\n\n def _get_fallback_input(self, **kwargs):\n return DefaultPromptField(**kwargs)\n"
},
"template": {
"_input_type": "PromptInput",
"advanced": false,
"display_name": "Template",
"dynamic": false,
"info": "",
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"name": "template",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_input": true,
"type": "prompt",
"value": "<Instructions Structure>\nIntroduce the task of generating tweets or tweet threads based on the provided inputs\n\nExplain each input variable:\n\n{{PROFILE_TYPE}}\n\n{{PROFILE_DETAILS}}\n\n{{CONTENT_GUIDELINES}}\n\n{{TONE_AND_STYLE}}\n\n{{CONTEXT}}\n\n{{OUTPUT_FORMAT}}\n\n{{OUTPUT_LANGUAGE}}\n\nProvide step-by-step instructions on how to analyze the inputs to determine if a single tweet or thread is appropriate\n\nGive guidance on generating tweet content that aligns with the profile, guidelines, tone, style, and context\n\nExplain how to format the output based on the {{OUTPUT_FORMAT}} value\n\nProvide tips for creating engaging, coherent tweet content\n\n</Instructions Structure>\n\n<Instructions>\nYou are an AI tweet generator that can create standalone tweets or multi-tweet threads based on a variety of inputs about the desired content. Here are the key inputs you will use to generate the tweet(s):\n\n<profile_type>\n\n{PROFILE_TYPE}\n\n</profile_type>\n\n<profile_details>\n\n{PROFILE_DETAILS}\n\n</profile_details>\n\n<content_guidelines>\n\n{CONTENT_GUIDELINES}\n\n</content_guidelines>\n\n<tone_and_style>\n\n{TONE_AND_STYLE}\n\n</tone_and_style>\n\n<output_format>\n\n{OUTPUT_FORMAT}\n\n</output_format>\n\n\n<output_language>\n\n{OUTPUT_LANGUAGE}\n\n</output_language>\n\nTo generate the appropriate tweet(s), follow these steps:\n\n<output_determination>\n\nCarefully analyze the {{PROFILE_TYPE}}, {{PROFILE_DETAILS}}, {{CONTENT_GUIDELINES}}, {{TONE_AND_STYLE}}, and {{CONTEXT}} to determine the depth and breadth of content needed.\n\nIf the {{OUTPUT_FORMAT}} is \"single_tweet\", plan to convey the key information in a concise, standalone tweet.\n\nIf the {{OUTPUT_FORMAT}} is \"thread\" or if the content seems too complex for a single tweet, outline a series of connected tweets that flow together to cover the topic.\n\n</output_determination>\n\n<content_generation>\n\nBrainstorm tweet content that aligns with the {{PROFILE_TYPE}} and {{PROFILE_DETAILS}}, adheres to the {{CONTENT_GUIDELINES}}, matches the {{TONE_AND_STYLE}}, and incorporates the {{CONTEXT}}.\n\nFor a single tweet, craft the most engaging, informative message possible within the 280 character limit.\n\nFor a thread, break down the content into distinct yet connected tweet-sized chunks. Ensure each tweet flows logically into the next to maintain reader engagement. Use transitional phrases as needed to link tweets.\n\n</content_generation>\n\n<formatting>\nFormat the output based on the {{OUTPUT_FORMAT}}:\n\nFor a single tweet, provide the content.\n\nFor a thread, include each tweet inside numbered markdown list.\n\n</formatting> <tips>\nFocus on creating original, engaging content that provides value to the intended audience.\n\nOptimize the tweet(s) for the 280 character limit. Be concise yet impactful.\n\nMaintain a consistent voice that matches the {{TONE_AND_STYLE}} throughout the tweet(s).\n\nInclude calls-to-action or questions to drive engagement when appropriate.\n\nDouble check that the final output aligns with the {{PROFILE_DETAILS}} and {{CONTENT_GUIDELINES}}.\n\n</tips>\n\nNow create a Tweet or Twitter Thread for this context:\n\n"
},
"tool_placeholder": {
"_input_type": "MessageTextInput",
"advanced": true,
"display_name": "Tool Placeholder",
"dynamic": false,
"info": "A placeholder input for tool mode.",
"input_types": [
"Message"
],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"name": "tool_placeholder",
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": true,
"trace_as_input": true,
"trace_as_metadata": true,
"type": "str",
"value": ""
},
"use_double_brackets": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Use Double Brackets",
"dynamic": false,
"info": "Use {{variable}} syntax instead of {variable}.",
"list": false,
"list_add_label": "Add More",
"name": "use_double_brackets",
"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": true,
"type": "bool",
"value": false
}
},
"tool_mode": false
},
"type": "Prompt"
},
"dragging": false,
"id": "Prompt-DC9aA",
"measured": {
"height": 757,
"width": 320
},
"position": {
"x": 1564.836735497639,
"y": 320.3318517095275
},
"positionAbsolute": {
"x": 1697.1682096049744,
"y": 675.4022940880462
},
"selected": false,
"type": "genericNode"
},
{
"data": {
"_connectionMode": false,
"id": "Agent-wh9WY",
"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": false
},
"add_current_date_tool": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Current Date",
"dynamic": false,
"info": "If true, will add a tool to the agent that returns the current date.",
"input_types": [],
"list": false,
"list_add_label": "Add More",
"name": "add_current_date_tool",
"override_skip": false,
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"track_in_telemetry": true,
"type": "bool",
"value": false
},
"api_key": {
"_input_type": "SecretStrInput",
"advanced": true,
"display_name": "API Key",
"dynamic": false,
"info": "Overrides global provider settings. Leave blank to use your pre-configured API Key.",
"input_types": [],
"load_from_db": true,
"name": "api_key",
"override_skip": false,
"password": true,
"placeholder": "",
"real_time_refresh": true,
"required": false,
"show": true,
"title_case": false,
"track_in_telemetry": false,
"type": "str",
"value": ""
},
"base_url_ibm_watsonx": {
"_input_type": "DropdownInput",
"advanced": false,
"combobox": true,
"dialog_inputs": {},
"display_name": "watsonx API Endpoint",
"dynamic": false,
"external_options": {},
"info": "The base URL of the API (IBM watsonx.ai only)",
"input_types": [],
"name": "base_url_ibm_watsonx",
"options": [
"https://us-south.ml.cloud.ibm.com",
"https://eu-de.ml.cloud.ibm.com",
"https://eu-gb.ml.cloud.ibm.com",
"https://au-syd.ml.cloud.ibm.com",
"https://jp-tok.ml.cloud.ibm.com",
"https://ca-tor.ml.cloud.ibm.com"
],
"options_metadata": [],
"override_skip": false,
"placeholder": "",
"real_time_refresh": true,
"required": false,
"show": false,
"title_case": false,
"toggle": false,
"tool_mode": false,
"trace_as_metadata": true,
"track_in_telemetry": true,
"type": "str",
"value": "https://us-south.ml.cloud.ibm.com"
},
"code": {
"advanced": true,
"api_editable": false,
"dynamic": true,
"fileTypes": [],
"file_path": "",
"info": "",
"list": false,
"load_from_db": false,
"multiline": true,
"name": "code",
"password": false,
"placeholder": "",
"required": true,
"show": true,
"title_case": false,
"type": "code",
"value": "from __future__ import annotations\n\nimport uuid\nfrom collections.abc import Mapping\nfrom contextlib import contextmanager\nfrom datetime import datetime, timezone\nfrom typing import TYPE_CHECKING, Any, cast\n\nfrom langchain.agents import create_agent\nfrom langchain.agents.middleware import (\n HumanInTheLoopMiddleware,\n ModelCallLimitMiddleware,\n ToolRetryMiddleware,\n)\nfrom langgraph.types import Command\n\nfrom lfx.components.models_and_agents.agent_helpers.graph_event_adapter import (\n adapt_graph_events_to_executor_shape,\n)\nfrom lfx.components.models_and_agents.agent_helpers.messages_input_builder import (\n build_initial_messages,\n)\nfrom lfx.components.models_and_agents.agent_helpers.placeholder_corrective_middleware import (\n WatsonXPlaceholderMiddleware,\n)\nfrom lfx.components.models_and_agents.agent_helpers.single_tool_call_middleware import (\n SingleToolCallMiddleware,\n)\nfrom lfx.components.models_and_agents.agent_helpers.tool_approval import ToolApprovalMixin\nfrom lfx.components.models_and_agents.agent_helpers.tool_call_id_middleware import ToolCallIDMiddleware\nfrom lfx.components.models_and_agents.memory import MemoryComponent, _safe_graph_user_id, aget_agent_chat_history\n\nif TYPE_CHECKING:\n from collections.abc import Awaitable, Callable\n\n from langchain_core.tools import Tool\n\n from lfx.schema.log import OnTokenFunctionType, SendMessageFunctionType\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.callback import AgentAsyncHandler\nfrom lfx.base.agents.default_system_prompt import DEFAULT_SYSTEM_PROMPT_TEMPLATE\nfrom lfx.base.agents.events import AgentPausedError, ExceptionWithMessageError, process_agent_events\nfrom lfx.base.agents.token_callback import TokenUsageCallbackHandler\nfrom lfx.base.agents.utils import get_chat_output_sender_name\nfrom lfx.base.constants import STREAM_INFO_TEXT\nfrom lfx.base.models.unified_models import (\n get_language_model_options,\n get_llm,\n handle_model_input_update,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.agentics.helpers.model_config import validate_model_selection\nfrom lfx.components.helpers import CalculatorComponent, CurrentDateComponent\nfrom lfx.components.langchain_utilities.ibm_granite_handler import is_watsonx_model\nfrom lfx.components.langchain_utilities.tool_calling import ToolCallingAgentComponent\nfrom lfx.custom.custom_component.component import get_component_toolkit\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, ModelInput, StrInput\nfrom lfx.io import IntInput, MessageTextInput, MultilineInput, Output, SecretStrInput, TableInput\nfrom lfx.log.logger import logger\nfrom lfx.memory import delete_message\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\nfrom lfx.utils.constants import MESSAGE_SENDER_AI\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\ndef _agent_base_inputs():\n \"\"\"Return base inputs tailored to AgentComponent's create_agent path.\n\n `get_base_inputs()` returns a shared list \u2014 replace, don't mutate. We drop\n inputs that are no-ops here and override info text on the inputs whose\n semantics shifted under create_agent.\n\n `verbose` is dropped because the create_agent event stream already surfaces\n every agent step via the \"Agent Steps\" content blocks; the legacy boolean\n has nothing to toggle. Saved flows that still carry a `verbose` value just\n ignore it on load (the schema no longer declares it).\n \"\"\"\n drop = {\"verbose\"}\n overrides = {\n \"handle_parsing_errors\": BoolInput(\n name=\"handle_parsing_errors\",\n display_name=\"Handle Parse Errors\",\n value=True,\n advanced=True,\n info=(\n \"Adds tool-execution retry as a safety net. `create_agent` already \"\n \"feeds tool-call validation errors back to the LLM automatically; \"\n \"this flag layers `ToolRetryMiddleware` on top so transient tool \"\n \"runtime failures are retried (max 2 retries).\"\n ),\n ),\n \"max_iterations\": IntInput(\n name=\"max_iterations\",\n display_name=\"Max Iterations\",\n value=15,\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n info=(\n \"Maximum number of model calls the agent can make before stopping \"\n \"(maps to `ModelCallLimitMiddleware.run_limit` on the create_agent \"\n \"path). Must be at least 1 \u2014 it is a safety cap, never 'unlimited'.\"\n ),\n ),\n }\n return [overrides.get(inp.name, inp) for inp in LCToolsAgentComponent.get_base_inputs() if inp.name not in drop]\n\n\ndef _extract_text_content(value) -> str:\n \"\"\"Pull a string payload from a Message-like, AIMessage-like, or string value.\"\"\"\n if isinstance(value, str):\n return value\n text = getattr(value, \"text\", None)\n if isinstance(text, str):\n return text\n content = getattr(value, \"content\", None)\n if isinstance(content, str):\n return content\n return str(value) if value is not None else \"\"\n\n\n@contextmanager\ndef _suppress_send_message(component: Any):\n \"\"\"Temporarily replace component.send_message with a no-op for the duration of the block.\n\n Used during the structured-output prompt fallback: run_agent streams the agent's\n final answer through self.send_message (correct for message_response), but in\n json_response the orchestrator parses that text into structured Data which the\n downstream Chat Output emits \u2014 leaving the original emission in place produces a\n duplicate message in the playground. The original method is always restored on exit,\n even when the wrapped call raises.\n \"\"\"\n original = component.send_message\n\n async def _noop(message, *_args, **_kwargs):\n return message\n\n component.send_message = _noop\n try:\n yield\n finally:\n component.send_message = original\n\n\nclass AgentComponent(ToolApprovalMixin, ToolCallingAgentComponent):\n display_name: str = \"Agent\"\n description: str = \"Define the agent's instructions, then enter a task to complete using tools.\"\n documentation: str = \"https://docs.langflow.org/agents\"\n icon = \"bot\"\n beta = False\n name = \"Agent\"\n\n memory_inputs = [set_advanced_true(component_input) for component_input in MemoryComponent().inputs]\n\n inputs = [\n ModelInput(\n name=\"model\",\n display_name=\"Language Model\",\n info=\"Select your model provider\",\n real_time_refresh=True,\n required=True,\n # Agents require tool calling \u2014 the filter is honored by\n # ``handle_model_input_update`` so models that can't run with\n # tools never reach the picker (and any saved selection that\n # no longer satisfies the constraint is auto-replaced).\n filters={\"tool_calling\": True},\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Overrides global provider settings. Leave blank to use your pre-configured API Key.\",\n real_time_refresh=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"base_url_ibm_watsonx\",\n display_name=\"watsonx API Endpoint\",\n info=\"The base URL of the API (IBM watsonx.ai only)\",\n options=IBM_WATSONX_URLS,\n value=IBM_WATSONX_URLS[0],\n combobox=True,\n show=False,\n real_time_refresh=True,\n ),\n StrInput(\n name=\"project_id\",\n display_name=\"watsonx Project ID\",\n info=\"The project ID associated with the foundation model (IBM watsonx.ai only)\",\n show=False,\n required=False,\n ),\n MultilineInput(\n name=\"system_prompt\",\n display_name=\"Agent Instructions\",\n info=(\n \"System Prompt: Initial instructions and context provided to guide the agent's behavior. \"\n \"Supports dynamic placeholders: {current_date}, {model_name}, {optional_user_context}.\"\n ),\n value=DEFAULT_SYSTEM_PROMPT_TEMPLATE,\n advanced=False,\n ),\n MessageTextInput(\n name=\"context_id\",\n display_name=\"Context ID\",\n info=\"The context ID of the chat. Adds an extra layer to the local memory.\",\n value=\"\",\n advanced=True,\n ),\n IntInput(\n name=\"n_messages\",\n display_name=\"Number of Chat History Messages\",\n value=100,\n info=\"Number of chat history messages to retrieve.\",\n advanced=True,\n show=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Set to 0 for no explicit limit. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=0, max=128000, step=1, step_type=\"int\"),\n ),\n MultilineInput(\n name=\"format_instructions\",\n display_name=\"Output Format Instructions\",\n info=\"Generic Template for structured output formatting. Valid only with Structured response.\",\n value=(\n \"You are an AI that extracts structured JSON objects from unstructured text. \"\n \"Use a predefined schema with expected types (str, int, float, bool, dict). \"\n \"Extract ALL relevant instances that match the schema - if multiple patterns exist, capture them all. \"\n \"Fill missing or ambiguous values with defaults: null for missing values. \"\n \"Remove exact duplicates but keep variations that have different field values. \"\n \"Always return valid JSON in the expected format, never throw errors. \"\n \"If multiple objects can be extracted, return them all in the structured format.\"\n ),\n advanced=True,\n ),\n TableInput(\n name=\"output_schema\",\n display_name=\"Output Schema\",\n info=(\n \"Schema Validation: Define the structure and data types for structured output. \"\n \"No validation if no output schema.\"\n ),\n advanced=True,\n required=False,\n value=[],\n table_schema=[\n {\n \"name\": \"name\",\n \"display_name\": \"Name\",\n \"type\": \"str\",\n \"description\": \"Specify the name of the output field.\",\n \"default\": \"field\",\n \"edit_mode\": EditMode.INLINE,\n },\n {\n \"name\": \"description\",\n \"display_name\": \"Description\",\n \"type\": \"str\",\n \"description\": \"Describe the purpose of the output field.\",\n \"default\": \"description of field\",\n \"edit_mode\": EditMode.POPOVER,\n },\n {\n \"name\": \"type\",\n \"display_name\": \"Type\",\n \"type\": \"str\",\n \"edit_mode\": EditMode.INLINE,\n \"description\": (\"Indicate the data type of the output field (e.g., str, int, float, bool, dict).\"),\n \"options\": [\"str\", \"int\", \"float\", \"bool\", \"dict\"],\n \"default\": \"str\",\n },\n {\n \"name\": \"multiple\",\n \"display_name\": \"As List\",\n \"type\": \"boolean\",\n \"description\": \"Set to True if this output field should be a list of the specified type.\",\n \"default\": \"False\",\n \"edit_mode\": EditMode.INLINE,\n },\n ],\n ),\n *_agent_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=STREAM_INFO_TEXT,\n value=True,\n advanced=True,\n ),\n BoolInput(\n name=\"add_current_date_tool\",\n display_name=\"Current Date\",\n advanced=True,\n info=\"If true, will add a tool to the agent that returns the current date.\",\n value=True,\n ),\n BoolInput(\n name=\"add_calculator_tool\",\n display_name=\"Calculator\",\n advanced=True,\n info=(\n \"If true, adds a zero-config arithmetic calculator tool to the agent \"\n \"(safe: only +, -, *, /, ** operators via AST).\"\n ),\n value=True,\n ),\n ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\n Output(\n name=\"structured_response\",\n display_name=\"Structured Response\",\n method=\"json_response\",\n types=[\"Data\"],\n ),\n ]\n\n async def _additional_model_provider_policy_ids(self, purpose, parameters=None) -> tuple[str, ...]:\n \"\"\"Gate the provider selector retained by legacy Agent/ALTK flows.\"\"\"\n _ = purpose\n from lfx.base.models.provider_registry import resolve_provider_id\n\n effective_parameters = parameters if isinstance(parameters, Mapping) else getattr(self, \"_parameters\", None)\n if not isinstance(effective_parameters, Mapping):\n effective_parameters = {}\n selected_model = effective_parameters.get(\"model\", getattr(self, \"model\", None))\n if selected_model:\n # The shared Component hook already handles a selected ModelInput;\n # connected model objects are gated by their upstream vertex.\n return ()\n legacy_provider = effective_parameters.get(\"agent_llm\", getattr(self, \"agent_llm\", None))\n if not isinstance(legacy_provider, str) or not legacy_provider.strip() or legacy_provider == \"Custom\":\n return ()\n return (resolve_provider_id(legacy_provider),)\n\n async def _filter_legacy_provider_options(self, build_config: Mapping[str, Any]) -> None:\n \"\"\"Filter ALTK/legacy Agent provider choices through the active scope.\"\"\"\n from lfx.services.model_provider_policy import ModelProviderPolicyPurpose, aresolve_model_provider_policy\n\n provider_field = build_config.get(\"agent_llm\")\n if not isinstance(provider_field, dict):\n return\n options = provider_field.get(\"options\")\n if not isinstance(options, list):\n return\n candidates = [option for option in options if isinstance(option, str) and option != \"Custom\"]\n if not candidates:\n return\n snapshot = await aresolve_model_provider_policy(\n user_id=self.user_id,\n providers=candidates,\n purpose=ModelProviderPolicyPurpose.CONFIGURE,\n )\n allowed = set(snapshot.filter(candidates))\n keep_indexes = [index for index, option in enumerate(options) if option == \"Custom\" or option in allowed]\n provider_field[\"options\"] = [options[index] for index in keep_indexes]\n metadata = provider_field.get(\"options_metadata\")\n if isinstance(metadata, list) and len(metadata) == len(options):\n provider_field[\"options_metadata\"] = [metadata[index] for index in keep_indexes]\n\n def _resolve_selected_model(self):\n \"\"\"Resolve the selected model, including legacy agent_llm/model_name inputs.\"\"\"\n try:\n from langchain_core.language_models import BaseLanguageModel\n\n if isinstance(self.model, BaseLanguageModel):\n return self.model\n except ImportError:\n pass\n\n if isinstance(self.model, list) and self.model:\n return self.model\n\n legacy_provider = getattr(self, \"agent_llm\", None)\n legacy_model_name = getattr(self, \"model_name\", None)\n if not legacy_provider or not legacy_model_name:\n return self.model\n\n options = get_language_model_options(user_id=self.user_id)\n for option in options:\n if option.get(\"provider\") == legacy_provider and option.get(\"name\") == legacy_model_name:\n return [option]\n\n return [\n {\n \"name\": legacy_model_name,\n \"provider\": legacy_provider,\n \"metadata\": {},\n }\n ]\n\n def _get_max_tokens_value(self):\n \"\"\"Return the user-supplied max_tokens or None when unset/zero.\"\"\"\n val = getattr(self, \"max_tokens\", None)\n if val in {\"\", 0}:\n return None\n return val\n\n def _get_llm(self):\n \"\"\"Override parent to include max_tokens from the Agent's input field.\n\n Streaming is mandatory for AgentComponent: ``runnable.astream_events(v2)`` only\n emits ``on_chat_model_stream`` chunks when the underlying chat model is\n instantiated with ``streaming=True``. Unlike the LanguageModel component (where\n ``stream`` is a user-facing toggle), the Agent has no opt-out \u2014 the toggle is\n kept in the UI for backwards compatibility but is intentionally ignored here.\n Without ``stream=True``, the chat model accumulates the whole response and\n only emits ``on_chat_model_end``, silently disabling the Playground's live-\n typing view and breaking the streaming contract on the /events surface.\n \"\"\"\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\n stream=True,\n max_tokens=self._get_max_tokens_value(),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n overrides=getattr(self, \"_model_overrides\", None),\n )\n\n async def get_agent_requirements(self):\n \"\"\"Get the agent requirements for the agent.\"\"\"\n from langchain_core.tools import StructuredTool\n\n from lfx.services.model_provider_policy import ModelProviderPolicyPurpose\n\n await self.arequire_model_provider_policy(ModelProviderPolicyPurpose.USE)\n\n selected_model = self._resolve_selected_model()\n try:\n from langchain_core.language_models import BaseLanguageModel\n\n is_connected_model = isinstance(selected_model, BaseLanguageModel)\n except ImportError:\n is_connected_model = False\n\n if not is_connected_model:\n validate_model_selection(selected_model)\n\n # Ensure _get_llm() uses the resolved model (e.g. from legacy agent_llm/model_name)\n self.model = selected_model\n llm_model = self._get_llm()\n if llm_model is None:\n msg = \"No language model selected. Please choose a model to proceed.\"\n raise ValueError(msg)\n\n # Get memory data\n self.chat_history = await self.get_memory_data()\n await logger.adebug(f\"Retrieved {len(self.chat_history)} chat history messages\")\n if isinstance(self.chat_history, Message):\n self.chat_history = [self.chat_history]\n\n # Add current date tool if enabled\n if self.add_current_date_tool:\n if not isinstance(self.tools, list): # type: ignore[has-type]\n self.tools = []\n current_date_tool = (await CurrentDateComponent(**self.get_base_args()).to_toolkit()).pop(0)\n\n if not isinstance(current_date_tool, StructuredTool):\n msg = \"CurrentDateComponent must be converted to a StructuredTool\"\n raise TypeError(msg)\n # Skip if an externally-connected tool already provides the same name.\n # Duplicate tool names are rejected by Anthropic/Gemini with HTTP 400.\n if not any(getattr(t, \"name\", None) == current_date_tool.name for t in self.tools):\n self.tools.append(current_date_tool)\n\n # Add calculator tool if enabled (zero-config arithmetic)\n if getattr(self, \"add_calculator_tool\", False):\n if not isinstance(self.tools, list): # type: ignore[has-type]\n self.tools = []\n calculator_tool = (await CalculatorComponent(**self.get_base_args()).to_toolkit()).pop(0)\n\n if not isinstance(calculator_tool, StructuredTool):\n msg = \"CalculatorComponent must be converted to a StructuredTool\"\n raise TypeError(msg)\n # Skip if an externally-connected tool already provides the same name.\n # Duplicate tool names are rejected by Anthropic/Gemini with HTTP 400.\n if not any(getattr(t, \"name\", None) == calculator_tool.name for t in self.tools):\n self.tools.append(calculator_tool)\n\n # Set shared callbacks for tracing the tools used by the agent\n self.set_tools_callbacks(self.tools, self._get_shared_callbacks())\n\n return llm_model, self.chat_history, self.tools\n\n def _get_resolved_model_name(self) -> str:\n \"\"\"Best-effort human-readable model name for {model_name} injection.\"\"\"\n try:\n from langchain_core.language_models import BaseLanguageModel\n\n if isinstance(self.model, BaseLanguageModel):\n return type(self.model).__name__\n except ImportError:\n pass\n\n if isinstance(self.model, list) and self.model:\n first = self.model[0]\n if isinstance(first, dict):\n name = first.get(\"name\")\n if isinstance(name, str) and name:\n return name\n\n legacy_model_name = getattr(self, \"model_name\", None)\n if isinstance(legacy_model_name, str) and legacy_model_name:\n return legacy_model_name\n return \"\"\n\n def _inject_dynamic_prompt_values(self, prompt: Any | None) -> str | None:\n \"\"\"Replace known env placeholders in the system prompt.\n\n Handles {current_date}, {model_name}, and {optional_user_context} (the\n last one ships with the structured DEFAULT_SYSTEM_PROMPT_TEMPLATE and\n is currently unused at the AgentComponent layer, so it resolves to \"\").\n Uses str.replace (not str.format) so user prompts containing literal\n braces such as JSON examples ({\"key\": 1}) never break the agent.\n\n `system_prompt` is a connectable MultilineInput, so the value can arrive\n as a Message (e.g. a Prompt node wired in). Normalize it to text first \u2014\n a raw Message has no `.replace` and used to crash the agent build.\n \"\"\"\n if prompt is None:\n return None\n prompt = _extract_text_content(prompt)\n if not prompt:\n return prompt\n replacements = {\n # Day precision keeps the system prompt byte-identical across turns so\n # providers can serve the chat history from their prompt cache.\n \"{current_date}\": datetime.now(tz=timezone.utc).date().isoformat(),\n \"{model_name}\": self._get_resolved_model_name(),\n \"{optional_user_context}\": \"\",\n }\n for placeholder, value in replacements.items():\n prompt = prompt.replace(placeholder, value)\n return prompt\n\n def create_agent_runnable(self, *, allow_interrupts: bool = True):\n \"\"\"Build the LangGraph `CompiledStateGraph` via `langchain.agents.create_agent`.\n\n Replaces the legacy `AgentExecutor` runnable inherited from\n `ToolCallingAgentComponent`. Other agent components (tool_calling, csv, json,\n openapi, sql*, vector_store_router) keep the legacy path \u2014 only AgentComponent\n runs on the new graph API.\n\n `max_iterations` and `handle_parsing_errors` (legacy AgentExecutor knobs) are\n translated to LangGraph middleware. Without that translation those user inputs\n would silently become no-ops on the new API.\n\n Provider notes:\n - WatsonX/Granite work natively with create_agent \u2014 `ChatWatsonx.bind_tools`\n handles tool_choice correctly. The legacy `create_granite_agent` path was\n dropped because it hardcoded `tool_choice='required'`, which the WatsonX\n API now rejects.\n - Ollama and other small/local models often emit malformed tool args. The\n ToolRetryMiddleware (default `retry_on=(Exception,)`, `on_failure='continue'`)\n catches Pydantic ValidationErrors from bad args and feeds the error back\n to the LLM as a retry signal, so the agent recovers gracefully.\n \"\"\"\n llm = self._get_llm()\n tools = self.tools or []\n\n # Eager bind_tools validation. `create_agent(...)` is lazy \u2014 without this,\n # an LLM that doesn't support tool calling fails on the first user message\n # instead of when the user wires up the component, which is a much worse UX.\n # Gated on a non-empty tools list so a no-tool Agent on a plain chat model\n # (which legitimately has no `bind_tools`) isn't shut out at flow-build time.\n # Providers signal \"no tool calling\" inconsistently \u2014 `NotImplementedError`\n # (langchain default), `AttributeError` (no `bind_tools` attr), or `TypeError`\n # (signature mismatch). Treat all three as the same UX failure.\n if tools:\n try:\n llm.bind_tools(tools)\n except (NotImplementedError, AttributeError, TypeError) as exc:\n # Include the underlying error so a broken tool schema or a\n # provider implementation bug is not silently disguised as a\n # \"model can't call tools\" UX error.\n msg = (\n f\"{self.display_name} does not support tool calling, \"\n \"or one of the connected tools failed to bind. \"\n \"Please connect a tool-calling capable language model and \"\n f\"verify your tools are well-formed. Underlying error: {exc!s}\"\n )\n raise NotImplementedError(msg) from exc\n\n middleware = self._build_middleware(llm, allow_interrupts=allow_interrupts)\n checkpointer = self._build_agent_checkpointer() if allow_interrupts else None\n return create_agent(\n model=llm,\n tools=tools,\n system_prompt=self.system_prompt or \"\",\n middleware=middleware or None,\n checkpointer=checkpointer,\n )\n\n def _compute_recursion_limit(self) -> int:\n \"\"\"Derive the LangGraph recursion_limit from the user-set max_iterations.\n\n Mirrors the clamp in `_build_middleware` (max(1, max_iterations)) so a\n saved 0 or negative value cannot under-cap the graph below one full\n iteration.\n\n A tool-calling cycle visits the model-call limiter's before_model and\n after_model nodes, the model, and tools. When any tool requires approval,\n HumanInTheLoopMiddleware adds another after_model node to every cycle,\n even if the model repeatedly calls an ungated tool. Leave transition\n room so the model-call limiter can end the run first.\n \"\"\"\n raw = getattr(self, \"max_iterations\", None)\n run_limit = max(1, int(raw)) if raw is not None else 15\n steps_per_cycle = 4 + bool(self._gated_interrupt_on())\n return run_limit * steps_per_cycle + 10\n\n def _build_middleware(self, llm: Any, *, allow_interrupts: bool = True) -> list:\n # `llm` is passed in (rather than re-fetched via `self._get_llm()`)\n # because some providers do credential resolution / client instantiation\n # lazily on each call. The caller \u2014 `create_agent_runnable` \u2014 already\n # resolved it once for `bind_tools`, so reuse that instance here.\n middleware: list = []\n # LangChain accepts missing IDs on AIMessage.tool_calls, but LangGraph's\n # invalid-call path and ToolRetryMiddleware both require a string when\n # they construct an error ToolMessage. Normalize at the model boundary\n # so either recovery path can return the error to the model instead of\n # crashing the flow.\n if self.tools:\n middleware.append(ToolCallIDMiddleware())\n max_iterations = getattr(self, \"max_iterations\", None)\n if max_iterations is not None:\n # `max_iterations` is a safety cap, not an \"unlimited\" toggle. A saved\n # 0 or negative value (falsy) must NOT silently drop the limiter and\n # allow an unbounded model/tool loop \u2014 clamp it to a real minimum.\n run_limit = max(1, int(max_iterations))\n middleware.append(ModelCallLimitMiddleware(run_limit=run_limit))\n # ToolRetryMiddleware only matters when there ARE tools to retry. Attaching\n # it on a no-tools agent inflates the compiled graph and adds per-invocation\n # middleware overhead for nothing, which is a measurable contributor to\n # trivial-prompt latency (QA UI-003).\n if getattr(self, \"handle_parsing_errors\", False) and self.tools:\n middleware.append(ToolRetryMiddleware(max_retries=2))\n # WatsonX models have two known platform quirks; both still reproduce on\n # the current API, so we keep the protections from the legacy\n # `create_granite_agent` path.\n # 1. Multi-tool-call assistant turns are rejected (\"This model only\n # supports single tool-calls at once!\"). Clamp to one per turn.\n # 2. Tool args occasionally come back as literal placeholder strings\n # (e.g. `<result-from-search>`). Re-invoke once with a corrective\n # SystemMessage.\n # Order: SingleToolCallMiddleware first (outermost) so the clamp is\n # applied to the final response, including any corrective re-invoke\n # produced by WatsonXPlaceholderMiddleware.\n if is_watsonx_model(llm):\n middleware.append(SingleToolCallMiddleware())\n middleware.append(WatsonXPlaceholderMiddleware())\n # Human-in-the-loop: attach only when a tool is gated AND interrupts are allowed\n # (the structured-output path disables them), keeping ungated flows unchanged.\n interrupt_on = self._gated_interrupt_on() if allow_interrupts else {}\n if interrupt_on:\n middleware.append(HumanInTheLoopMiddleware(interrupt_on=interrupt_on))\n return middleware\n\n async def run_agent(self, agent) -> Message:\n \"\"\"Run the LangGraph `CompiledStateGraph` and return the final agent Message.\n\n Overrides the legacy `LCAgentComponent.run_agent` (which builds an\n `{\"input\": str, \"chat_history\": [...]}` dict for `AgentExecutor`). The graph\n wants `{\"messages\": [BaseMessage, ...]}`. The event stream is wrapped with\n `adapt_graph_events_to_executor_shape` so the legacy `process_agent_events`\n (in `lfx.base.agents.events`) can be reused unchanged.\n \"\"\"\n messages = build_initial_messages(\n input_value=self.input_value,\n chat_history=getattr(self, \"chat_history\", None),\n )\n input_dict = {\"messages\": messages}\n\n agent_message = self._build_initial_agent_message()\n token_usage_handler = TokenUsageCallbackHandler()\n\n # Stream tokens to the event manager when running inside the Langflow runtime.\n # This is what powers the live-typing view in the chat UI.\n on_token_callback: OnTokenFunctionType | None = None\n if getattr(self, \"_event_manager\", None):\n on_token_callback = cast(\"OnTokenFunctionType\", self._event_manager.on_token)\n\n # Align LangGraph's `recursion_limit` with `max_iterations` so the\n # middleware cap (ModelCallLimitMiddleware) is what bounds the loop \u2014\n # not LangGraph's default 25-step guard, which fires at ~12 model+tool\n # iterations and raises a raw GraphRecursionError (QA UI-009/UI-010).\n # Include the extra after_model node when tool approval is configured.\n recursion_limit = self._compute_recursion_limit()\n\n agent_config: dict[str, Any] = {\n \"callbacks\": [\n AgentAsyncHandler(self.log),\n token_usage_handler,\n *self._get_shared_callbacks(),\n ],\n \"recursion_limit\": recursion_limit,\n }\n # The durable checkpointer keys on the per-run thread_id; it must be in the\n # astream_events config (not just create_agent) for both initial run and resume.\n thread_id = self._agent_thread_id()\n get_pending_interrupt = None\n stream_input: Any = input_dict\n # A checkpointer is only present when the agent was built with interrupts enabled\n # (the structured-output path builds without one); never probe its state otherwise.\n interrupts_enabled = getattr(agent, \"checkpointer\", None) is not None\n if interrupts_enabled and thread_id and self._gated_interrupt_on():\n agent_config[\"configurable\"] = {\"thread_id\": thread_id}\n get_pending_interrupt = self._pending_interrupt_getter(agent, agent_config)\n if self._has_candidate_decision(thread_id):\n # The injected decision must match the pending interrupt's nonce, so read\n # the interrupt first; a matched decision resumes the checkpointed thread.\n value, interrupt_id = await self._read_pending_interrupt(agent, agent_config)\n decision = self._injected_agent_decision(thread_id, interrupt_id)\n if decision is not None:\n action_requests = (value or {}).get(\"action_requests\") or []\n stream_input = Command(\n resume={\"decisions\": self._build_resume_decisions(decision, action_requests)}\n )\n stream = adapt_graph_events_to_executor_shape(\n agent.astream_events(stream_input, config=agent_config, version=\"v2\")\n )\n try:\n result = await process_agent_events(\n stream,\n agent_message,\n cast(\"SendMessageFunctionType\", self.send_message),\n on_token_callback,\n get_pending_interrupt=get_pending_interrupt,\n )\n except AgentPausedError as e:\n # Why: retract the empty partial bubble (leaks as \"[]\"); the HITL card supersedes it, resume re-emits it.\n paused_message = e.agent_message or agent_message\n msg_id = paused_message.get_id()\n if msg_id:\n await delete_message(id_=msg_id)\n await self._send_message_event(paused_message, category=\"remove_message\")\n return self._suspend_for_tool_approval(e.request, paused_message)\n except ExceptionWithMessageError as e:\n # Drop the half-stored partial message from the DB (only if it was\n # actually persisted) and tell the frontend to remove the stale bubble.\n if hasattr(e, \"agent_message\"):\n msg_id = e.agent_message.get_id()\n if msg_id:\n await delete_message(id_=msg_id)\n await self._send_message_event(e.agent_message, category=\"remove_message\")\n # exception(), not error(f\"...{e}\"): this class's str() interpolates the model's\n # partial completion, and passing the exception rather than formatting it is what\n # gives the exported record an error.type to triage on.\n logger.exception(\"Agent run failed after a partial message was emitted\")\n raise\n\n usage_data = token_usage_handler.get_usage()\n if usage_data:\n self._token_usage = usage_data\n result.properties.usage = usage_data\n # Only round-trip the DB when the message was stored (Chat Output wired).\n # `_should_skip_message=True` leaves `result.get_id()` empty; persisting\n # then would create a phantom row.\n if result.get_id():\n stored_result = await self._update_stored_message(result)\n await self._send_message_event(stored_result)\n result = stored_result\n\n self.status = result\n return result\n\n def _build_initial_agent_message(self) -> Message:\n \"\"\"Construct the placeholder agent Message that `process_agent_events` mutates.\"\"\"\n if hasattr(self, \"graph\"):\n session_id = self.graph.session_id\n elif hasattr(self, \"_session_id\"):\n session_id = self._session_id\n else:\n session_id = None\n\n sender_name = get_chat_output_sender_name(self) or self.display_name or \"AI\"\n return Message(\n sender=MESSAGE_SENDER_AI,\n sender_name=sender_name,\n properties={\"icon\": \"Bot\", \"state\": \"partial\"},\n # `text=\"\"` sentinel so MessageTable's no_content check accepts\n # an in-flight agent message whose content_blocks haven't been\n # populated yet. Mirrors ChatInput's convention.\n text=\"\",\n # Flat chronological event log; see lfx.base.agents.events.\n content_blocks=[],\n session_id=session_id or uuid.uuid4(),\n )\n\n def _selected_model_remediation_context(self) -> tuple[str | None, str | None, Any | None]:\n \"\"\"Return provider/name plus a connected model target, when present.\"\"\"\n try:\n selected = self._resolve_selected_model()\n if isinstance(selected, list) and selected and isinstance(selected[0], dict):\n return selected[0].get(\"provider\"), selected[0].get(\"name\"), None\n\n from langchain_core.language_models import BaseLanguageModel\n\n if isinstance(selected, BaseLanguageModel):\n model_name = None\n for attr in (\"model_name\", \"model\", \"model_id\"):\n value = getattr(selected, attr, None)\n if isinstance(value, str) and value:\n model_name = value\n break\n return self._connected_model_provider(selected), model_name, selected\n except (AttributeError, TypeError, ValueError, KeyError, ImportError):\n pass\n return None, None, None\n\n def _connected_model_provider(self, model: Any) -> str | None:\n \"\"\"Resolve a connected model's provider from the source component.\n\n Runtime model classes are not provider identities: OpenAI, OpenRouter,\n and compatible endpoints can all produce ``ChatOpenAI``. The incoming\n model edge preserves the source component, so prefer its explicit\n provider override or selected-model metadata, then its provider display\n name. If the Agent is used without graph provenance, leave the provider\n unknown so provider-scoped remediations remain disabled.\n \"\"\"\n vertex = getattr(self, \"_vertex\", None)\n if vertex is None:\n return None\n source_id = vertex.get_incoming_edge_by_target_param(\"model\")\n if not source_id:\n return None\n source = vertex.graph.get_vertex(source_id)\n component = getattr(source, \"custom_component\", None)\n if component is None:\n return None\n\n candidate = getattr(component, \"provider\", None)\n\n if not isinstance(candidate, str) or not candidate:\n selected_model = getattr(component, \"model\", None)\n if isinstance(selected_model, list) and selected_model and isinstance(selected_model[0], dict):\n candidate = selected_model[0].get(\"provider\")\n\n if not isinstance(candidate, str) or not candidate:\n candidate = getattr(component, \"display_name\", None)\n if not isinstance(candidate, str) or not candidate:\n return None\n\n from lfx.base.models.unified_models.provider_queries import get_model_provider_metadata\n\n provider_metadata = get_model_provider_metadata().get(candidate, {})\n expected_class = provider_metadata.get(\"mapping\", {}).get(\"model_class\")\n model_classes = {base.__name__ for base in type(model).__mro__}\n return candidate if expected_class in model_classes else None\n\n async def _run_agent_with_model_remediation(\n self,\n run_once: Callable[[], Awaitable[Message]],\n ) -> Message:\n \"\"\"Run one Agent operation, retrying safe provider-validation failures.\n\n Registered remediations must identify request-validation failures that\n occur before tool execution. A failure that can happen after a tool runs\n must instead retry at the model-call boundary to avoid repeating tool\n side effects.\n \"\"\"\n from lfx.base.models.model_remediation import apply_overrides_to_model, find_remediation, remember\n\n provider, model_name, connected_model = self._selected_model_remediation_context()\n applied: set[str] = set()\n while True:\n try:\n result = await run_once()\n except (ValueError, TypeError, KeyError):\n await logger.aexception(\"Agent run failed\")\n raise\n except Exception as exc:\n # run_agent may wrap the provider error in ExceptionWithMessageError, whose\n # str() carries the model's partial completion. Matching on it locally is fine;\n # logging it is not, so the record below passes the exception instead.\n error_text = f\"{exc} {getattr(exc, '__cause__', '') or ''}\"\n remediation = find_remediation(error_text, provider, already_applied=applied)\n if remediation is None:\n await logger.aexception(\"Agent run failed with no remediation available\")\n raise\n if connected_model is not None and not apply_overrides_to_model(connected_model, remediation.overrides):\n await logger.aerror(\n f\"model.remediation.unapplied name={remediation.name} provider={provider} model={model_name}\"\n )\n raise\n applied.add(remediation.name)\n if connected_model is None:\n self._model_overrides = {\n **(getattr(self, \"_model_overrides\", None) or {}),\n **remediation.overrides,\n }\n else:\n # Connected outputs are shared objects across downstream flow\n # branches. The mutation is intentionally flow-scoped: sibling\n # branches holding this model will see the matched override too.\n await logger.adebug(\n f\"model.remediation.shared_model name={remediation.name} provider={provider} model={model_name}\"\n )\n await logger.awarning(\n f\"model.remediation.applied name={remediation.name} provider={provider} model={model_name}\"\n )\n else:\n if connected_model is None and getattr(self, \"_model_overrides\", None):\n remember(provider, model_name, self._model_overrides)\n return result\n\n async def message_response(self) -> Message:\n async def _run_once() -> Message:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n self.set(\n llm=llm_model,\n tools=self.tools or [],\n chat_history=self.chat_history,\n input_value=self.input_value,\n system_prompt=self._inject_dynamic_prompt_values(self.system_prompt),\n )\n agent = self.create_agent_runnable()\n return await self.run_agent(agent)\n\n result = await self._run_agent_with_model_remediation(_run_once)\n self._agent_result = result\n return result\n\n async def json_response(self) -> Data:\n \"\"\"Produce structured Data via native LLM structured output, with prompt-based fallback.\n\n Native path (no tools, llm has with_structured_output) bypasses the agent loop and\n returns provider-validated JSON. When tools are attached, falls back to running the\n agent with a schema-augmented system prompt and parsing the final message content.\n \"\"\"\n from lfx.components.models_and_agents.structured_output.structured_output_orchestrator import (\n orchestrate_structured_output,\n )\n\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n except (ValueError, TypeError) as exc:\n await logger.aexception(\"json_response.requirements_failed\")\n return Data(data={\"content\": \"\", \"error\": str(exc)})\n\n injected_system_prompt = self._inject_dynamic_prompt_values(getattr(self, \"system_prompt\", \"\") or \"\") or \"\"\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n output_schema = getattr(self, \"output_schema\", None) or []\n has_tools = bool(self.tools)\n\n async def _run_agent_for_fallback(augmented_prompt: str) -> str:\n first_attempt = True\n\n async def _run_once() -> Message:\n nonlocal first_attempt, llm_model\n if first_attempt:\n first_attempt = False\n else:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n self.set(\n llm=llm_model,\n tools=self.tools or [],\n chat_history=self.chat_history,\n input_value=self.input_value,\n system_prompt=augmented_prompt,\n )\n # Structured output cannot suspend mid-parse: disable tool-approval interrupts.\n agent_runnable = self.create_agent_runnable(allow_interrupts=False)\n return await self.run_agent(agent_runnable)\n\n with _suppress_send_message(self):\n result = await self._run_agent_with_model_remediation(_run_once)\n return _extract_text_content(result)\n\n try:\n return await orchestrate_structured_output(\n llm=llm_model,\n output_schema=output_schema,\n system_prompt=injected_system_prompt,\n format_instructions=format_instructions,\n input_value=_extract_text_content(self.input_value),\n run_prompt_fallback=_run_agent_for_fallback,\n prefer_native=not has_tools,\n )\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as exc:\n await logger.aexception(\"json_response.orchestration_failed\")\n return Data(data={\"content\": \"\", \"error\": str(exc)})\n\n async def get_memory_data(self):\n # Scope by flow_id so default playground session names (e.g. \"New Session 0\")\n # cannot leak chat history across unrelated flows. See issue #13059.\n # The helper also returns [] when n_messages == 0, preserving the\n # explicit \"memory disabled\" contract from MemoryComponent.retrieve_messages.\n messages = await aget_agent_chat_history(\n session_id=self.graph.session_id,\n flow_id=getattr(self.graph, \"flow_id\", None),\n context_id=self.context_id,\n n_messages=self.n_messages,\n user_id=_safe_graph_user_id(self),\n )\n return [\n message for message in messages if getattr(message, \"id\", None) != getattr(self.input_value, \"id\", None)\n ]\n\n def update_input_types(self, build_config: dotdict) -> dotdict:\n \"\"\"Update input types for all fields in build_config.\"\"\"\n for key, value in build_config.items():\n if isinstance(value, dict):\n if value.get(\"input_types\") is None:\n build_config[key][\"input_types\"] = []\n elif hasattr(value, \"input_types\") and value.input_types is None:\n value.input_types = []\n return build_config\n\n async def update_build_config(\n self,\n build_config: dotdict,\n field_value: list[dict],\n field_name: str | None = None,\n ) -> dotdict:\n from lfx.services.model_provider_policy import ModelProviderPolicyPurpose\n\n policy_parameters = dict(getattr(self, \"_parameters\", {}) or {})\n if field_name:\n policy_parameters[field_name] = field_value\n await self.arequire_model_provider_policy(\n ModelProviderPolicyPurpose.CONFIGURE,\n parameters=policy_parameters,\n )\n # Update model options with caching (for all field changes).\n # The tool-calling constraint lives on the ModelInput's ``filters``\n # field (declared above); ``handle_model_input_update`` reads it\n # and applies the filter to both the dropdown options and the\n # sticky-default re-injection path.\n build_config = handle_model_input_update(\n component=self,\n build_config=dict(build_config),\n field_value=field_value,\n field_name=field_name,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n # Validate required keys. `verbose` was dropped from the input set\n # (see `_agent_base_inputs` \u2014 the create_agent event stream already\n # surfaces every step), so it is intentionally NOT required here.\n # Saved flows that still carry a `verbose` value just ignore it on\n # load.\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"add_calculator_tool\",\n \"system_prompt\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n ]\n missing_keys = [key for key in default_keys if key not in build_config]\n if missing_keys:\n msg = f\"Missing required keys in build_config: {missing_keys}\"\n raise ValueError(msg)\n await self._filter_legacy_provider_options(build_config)\n return dotdict({k: v.to_dict() if hasattr(v, \"to_dict\") else v for k, v in build_config.items()})\n\n async def _get_tools(self) -> list[Tool]:\n component_toolkit = get_component_toolkit()\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n # here we do not use the shared callbacks as we are exposing the agent as a tool\n callbacks=self.get_langchain_callbacks(),\n )\n if hasattr(self, \"tools_metadata\"):\n tools = component_toolkit(component=self, metadata=self.tools_metadata).update_tools_metadata(tools=tools)\n\n return tools\n"
},
"context_id": {
"_input_type": "MessageTextInput",
"advanced": true,
"display_name": "Context ID",
"dynamic": false,
"info": "The context ID of the chat. Adds an extra layer to the local memory.",
"input_types": [
"Message"
],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"name": "context_id",
"override_skip": false,
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"track_in_telemetry": false,
"type": "str",
"value": ""
},
"format_instructions": {
"_input_type": "MultilineInput",
"advanced": true,
"ai_enabled": false,
"copy_field": false,
"display_name": "Output Format Instructions",
"dynamic": false,
"info": "Generic Template for structured output formatting. Valid only with Structured response.",
"input_types": [
"Message"
],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"multiline": true,
"name": "format_instructions",
"override_skip": false,
"password": false,
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_input": true,
"trace_as_metadata": true,
"track_in_telemetry": false,
"type": "str",
"value": "You are an AI that extracts structured JSON objects from unstructured text. Use a predefined schema with expected types (str, int, float, bool, dict). Extract ALL relevant instances that match the schema - if multiple patterns exist, capture them all. Fill missing or ambiguous values with defaults: null for missing values. Remove exact duplicates but keep variations that have different field values. Always return valid JSON in the expected format, never throw errors. If multiple objects can be extracted, return them all in the structured format."
},
"handle_parsing_errors": {
"_input_type": "BoolInput",
"advanced": true,
"display_name": "Handle Parse Errors",
"dynamic": false,
"info": "Adds tool-execution retry as a safety net. `create_agent` already feeds tool-call validation errors back to the LLM automatically; this flag layers `ToolRetryMiddleware` on top so transient tool runtime failures are retried (max 2 retries).",
"input_types": [],
"list": false,
"list_add_label": "Add More",
"name": "handle_parsing_errors",
"override_skip": false,
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"track_in_telemetry": true,
"type": "bool",
"value": true
},
"input_value": {
"_input_type": "MessageInput",
"advanced": false,
"display_name": "Input",
"dynamic": false,
"info": "The input provided by the user for the agent to process.",
"input_types": [
"Message"
],
"list": false,
"list_add_label": "Add More",
"load_from_db": false,
"name": "input_value",
"override_skip": false,
"placeholder": "",
"required": false,
"show": true,
"title_case": false,
"tool_mode": true,
"trace_as_input": true,
"trace_as_metadata": true,
"track_in_telemetry": false,
"type": "str",
"value": ""
},
"max_iterations": {
"_input_type": "IntInput",
"advanced": true,
"display_name": "Max Iterations",
"dynamic": false,
"info": "Maximum number of model calls the agent can make before stopping (maps to `ModelCallLimitMiddleware.run_limit` on the create_agent path). Must be at least 1 \u2014 it is a safety cap, never 'unlimited'.",
"input_types": [],
"list": false,
"list_add_label": "Add More",
"name": "max_iterations",
"override_skip": false,
"placeholder": "",
"range_spec": {
"max": 128000,
"min": 1,
"step": 1,
"step_type": "int"
},
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"track_in_telemetry": true,
"type": "int",
"value": 15
},
"max_tokens": {
"_input_type": "IntInput",
"advanced": true,
"display_name": "Max Tokens",
"dynamic": false,
"info": "Maximum number of tokens to generate. Set to 0 for no explicit limit. Field name varies by provider.",
"input_types": [],
"list": false,
"list_add_label": "Add More",
"name": "max_tokens",
"override_skip": false,
"placeholder": "",
"range_spec": {
"max": 128000,
"min": 1,
"step": 1,
"step_type": "int"
},
"required": false,
"show": true,
"title_case": false,
"tool_mode": false,
"trace_as_metadata": true,
"track_in_telemetry": true,
"type": "int",
"value": 0
},
"model": {
"_input_type": "ModelInput",
"advanced": false,
"display_name": "Language Model",
"dynamic": false,
"external_options": {
"fields": {
"data": {
"node": {
"display_name": "Connect other models",
"icon": "CornerDownLeft",
"name": "connect_other_models"
}
}
}
},
"filters": {
"tool_calling": true
},
"info": "Select your model provider",
"input_types": [
"LanguageModel"
],
"list": false,
"list_add_label": "Add More",
"model_type": "language",
"name": "model",
"options": [
{
"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"
}
],
"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 an expert social media copywriter that generates engaging tweets and Twitter threads. Follow the detailed instructions provided to you (profile, content guidelines, tone, output format, and language) and craft content for the context the user supplies. Keep each tweet within 280 characters, make the thread flow naturally, and end with a clear call-to-action."
},
"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-wh9WY",
"measured": {
"height": 431,
"width": 320
},
"position": {
"x": 2182.7940856919763,
"y": 761.5508208654511
},
"positionAbsolute": {
"x": 2182.7940856919763,
"y": 761.5508208654511
},
"selected": false,
"type": "genericNode"
},
{
"data": {
"id": "note-o4Lrl",
"node": {
"description": "# \ud83d\udc26 Twitter Thread Generator\n\nTurn a project brief into an engaging multi-tweet **thread**. Rebuilt on the new **Agent** core component.\n\n## Setup\n1. **Agent** - add your LLM key (any provider in the Model dropdown).\n2. **Prompt Template** - tune the thread rules: PROFILE_TYPE, PROFILE_DETAILS, CONTENT_GUIDELINES, TONE_AND_STYLE, OUTPUT_FORMAT, OUTPUT_LANGUAGE.\n\n## How it works\n1. **Prompt \u2192 Agent (System Prompt)** - the instructions + style guidelines steer the Agent.\n2. **Chat Input \u2192 Agent (Input)** - your project context / what the thread is about.\n3. **Agent \u2192 Chat Output** - the finished tweet or numbered thread.\n\n## Run\nOpen **Playground**, type (or keep) the context in Chat Input, and run.",
"display_name": "",
"documentation": "",
"i18n_key": "template_notes.twitter_thread_generator.7e45a5a9",
"template": {
"backgroundColor": "blue"
}
},
"type": "note"
},
"dragging": false,
"height": 704,
"id": "note-o4Lrl",
"measured": {
"height": 704,
"width": 592
},
"position": {
"x": 799.0466461945499,
"y": 656.3476167888024
},
"positionAbsolute": {
"x": 1217.1682096049744,
"y": 635.4022940880462
},
"resizing": false,
"selected": false,
"type": "noteNode",
"width": 592
}
],
"viewport": {
"x": -456.7545943064715,
"y": -161.47899185100908,
"zoom": 0.6198403453974115
}
},
"description": "Generate an engaging tweet or Twitter thread from a project brief. An Agent (replacing the Language Model) follows the Prompt's style guidelines as its system prompt and writes from the Chat Input context. Core components only, bundle-free.",
"endpoint_name": null,
"id": "faa77bb1-37aa-4a3f-96cb-de9f78ebd586",
"is_component": false,
"last_tested_version": "1.10.1rc3",
"locked": false,
"name": "Twitter Thread Generator",
"tags": [
"chatbots",
"content-generation"
]
}How to use this workflow
- Review the pre-filled example and source files above.
- Copy the template or download
Twitter Thread Generator.json, then use it in your model, IDE, or workflow tool. - Compare your result with the sample output when one is provided.
Frequently Asked Questions
How do I use Workflow to Langflow Graph to Twitter Thread Generator?
Open the workflow on Promptally, review the pre-filled example, then copy the template or download Twitter Thread Generator.json and paste it into your model, IDE or workflow tool. Workflow to Langflow Graph to Twitter Thread Generator is a static blueprint with no dynamic variables. Copy it and use it as-is.
Is Workflow to Langflow Graph to Twitter Thread Generator free?
Yes. Workflow to Langflow Graph to Twitter Thread Generator is free to view, copy and download on Promptally without creating an account. It is published under the MIT license.
What variables does Workflow to Langflow Graph to Twitter Thread Generator need?
Workflow to Langflow Graph to Twitter Thread Generator is a static blueprint with no dynamic variables. Copy it and use it as-is.